Clarifying [dcl.init.ref]/5.2.2.1

129 views
Skip to first unread message

bogdan

unread,
Aug 15, 2016, 1:36:38 PM8/15/16
to ISO C++ Standard - Discussion
The current version:

   [...] The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference. For this direct-initialization, user-defined conversions are not considered.

I think this is incomplete: if T1 is a class type, overload resolution may choose a constructor of T1 to perform the initialization; whether that can be called "the conversion function" whose "result" is then used to direct-initialize the reference is debatable. Besides, what happens in this case is quite different from what happens when a conversion function is actually involved: in the latter case, it's not the final result of the complete conversion sequence that is subsequently used, but strictly what the conversion function returns (as discussed in a previous thread).

Here's my proposal for clarifying it:

[...] An expression is synthesized as follows:
  • If the non-reference copy-initialization would use a constructor of T1, the expression is a prvalue that specifies the same computation as that initialization.
  • Otherwise, the expression is the same as a call to the conversion function as described for the non-reference copy-initialization. [ Note: The type of the expression may be different from cv1 T1 in this case. — end note ]
The synthesized expression is then used to direct-initialize the reference. For this direct-initialization, user-defined conversions are not considered.

How does this sound?

Belloc

unread,
Aug 15, 2016, 7:23:12 PM8/15/16
to ISO C++ Standard - Discussion
I believe the case in question would fall in [dcl.init.ref]/(5.1.2) or (5.2.1.2), not (5.2.2.1).

bogdan

unread,
Aug 16, 2016, 5:52:02 AM8/16/16
to ISO C++ Standard - Discussion


I think a case like

struct A
{
   A
(int) { }
};

A
&& a = 7;


is handled by 5.2.2.1, isn't it?

bogdan

unread,
Aug 16, 2016, 7:21:46 AM8/16/16
to ISO C++ Standard - Discussion

Going through this once more, I just realized that the cross reference to the whole of [dcl.init] in the existing text does include [dcl.init]/17.6.3, which defines what the result of the conversion function is in case of a constructor. A bit obscure, but consistent, so my proposed change doesn't seem so useful after all.

Usefulness of the change notwithstanding, there's a question about cv-qualification: in the constructor case, the current specification will eventually bind the reference to a temporary of type T1, not cv1 T1. This is inconsistent with the case when T1 is a non-class type, where 5.2.2.2 (when it works correctly) creates a temporary of type cv1 T1. I think for indirect binding it makes sense that the type of the reference dictates the type of the temporary, cv-qualification included, in all cases. What is the intended behaviour in this case?

Belloc

unread,
Aug 16, 2016, 10:41:13 AM8/16/16
to ISO C++ Standard - Discussion


On Tuesday, August 16, 2016 at 8:21:46 AM UTC-3, bogdan wrote:

Going through this once more, I just realized that the cross reference to the whole of [dcl.init] in the existing text does include [dcl.init]/17.6.3, which defines what the result of the conversion function is in case of a constructor. A bit obscure, but consistent, so my proposed change doesn't seem so useful after all.

[dcl.init]/(17.6.3) doesn't apply when the destination type is a reference type. See (17.2).

In reference to your example, the initialization is first handled by (5.2.2.1). Then, the algorithm starts from the beginning to direct-initialize the reference as if using:

A&& a{ A(7)};

which is then handled by (5.2.1.1).

bogdan

unread,
Aug 16, 2016, 11:56:41 AM8/16/16
to ISO C++ Standard - Discussion

On Tuesday, August 16, 2016 at 5:41:13 PM UTC+3, Belloc wrote:
[dcl.init]/(17.6.3) doesn't apply when the destination type is a reference type. See (17.2).


It does apply when 5.2.2.1 says

   [...] user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion (8.6, 13.3.1.4, 13.3.1.5) [...]



In reference to your example, the initialization is first handled by (5.2.2.1). Then, the algorithm starts from the beginning to direct-initialize the reference as if using:

A&& a{ A(7)};

which is then handled by (5.2.1.1).


That's the gist of it, yes, but without the braces - that would introduce the additional step in [dcl.init.list]/3.7.

Anyway, we're discussing the first step, which you agree is handled by 5.2.2.1.

Belloc

unread,
Aug 16, 2016, 3:27:54 PM8/16/16
to ISO C++ Standard - Discussion


On Tuesday, August 16, 2016 at 12:56:41 PM UTC-3, bogdan wrote:


That's the gist of it, yes, but without the braces - that would introduce the additional step in [dcl.init.list]/3.7.

Anyway, we're discussing the first step, which you agree is handled by 5.2.2.1.


I don't understand which additional step are you talking about here. The reference is just direct-initialized with the expression A(7), with, or without braces. 

Belloc

unread,
Aug 16, 2016, 3:39:09 PM8/16/16
to ISO C++ Standard - Discussion


On Tuesday, August 16, 2016 at 12:56:41 PM UTC-3, bogdan wrote:

It does apply when 5.2.2.1 says

   [...] user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion (8.6, 13.3.1.4, 13.3.1.5) [...]

I don't understand the reason why you insist [dcl.init]/(17.6.3) would apply here. Note that this code is well-formed:

struct A
{
     A
(int) { }
private:
    A
(const A&);
};


A
&& a = 7;

That is, no copy-initialization occurs, when the target is a reference type.

bogdan

unread,
Aug 16, 2016, 4:59:36 PM8/16/16
to ISO C++ Standard - Discussion

On Tuesday, August 16, 2016 at 10:27:54 PM UTC+3, Belloc wrote:
I don't understand which additional step are you talking about here. The reference is just direct-initialized with the expression A(7), with, or without braces. 


You gave an example of direct-list-initialization there, and that's not what the wording says; it says direct-initialize. List-initializing the reference doesn't go straight to [dcl.init.ref], it goes through [dcl.init.list] first, hence the additional step I mentioned. I did say that the gist of the example was correct, but I also pointed out where it wasn't exact, that's all.

Thiago Macieira

unread,
Aug 16, 2016, 5:45:36 PM8/16/16
to std-dis...@isocpp.org
On terça-feira, 16 de agosto de 2016 12:39:08 PDT Belloc wrote:
> A&& a = 7;
>
> That is, no copy-initialization occurs, when the target is a reference type.

Because the above is equivalent to:

A tmp = 7;
A &&a = tmp;

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center

bogdan

unread,
Aug 16, 2016, 5:50:11 PM8/16/16
to ISO C++ Standard - Discussion

On Tuesday, August 16, 2016 at 10:39:09 PM UTC+3, Belloc wrote:
On Tuesday, August 16, 2016 at 12:56:41 PM UTC-3, bogdan wrote:

It does apply when 5.2.2.1 says

   [...] user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion (8.6, 13.3.1.4, 13.3.1.5) [...]

I don't understand the reason why you insist [dcl.init]/(17.6.3) would apply here. Note that this code is well-formed:


The reason is one line above, in the quote.

We're analyzing A&& a = 7;:
  • We go to [dcl.init.ref]/5.2.2.1; it tells us to consider the following invented non-reference copy-initialization: A a = 7. We're considering it, we're not going through with the initialization of an object of type A. We pretend we're doing it, in order to determine the user-defined conversion to apply to 7, but while pretending we still have to follow the rules.
  • Where are the rules for that non-reference initialization? [dcl.init]/17.6.3.
  • Which sends us to [over.match.copy], which chooses the constructor.
  • Back to 17.6.3, which specifies how that constructor call is seen as a prvalue expresssion of type A.
  • Back to 5.2.2.1, which tells us that the synthesized prvalue ("the result of the call to the conversion function") is used to direct-initialize the reference.

Belloc

unread,
Aug 17, 2016, 9:08:22 AM8/17/16
to ISO C++ Standard - Discussion


On Tuesday, August 16, 2016 at 6:50:11 PM UTC-3, bogdan wrote: (emphasis is mine)

The reason is one line above, in the quote.

We're analyzing A&& a = 7;:
  • We go to [dcl.init.ref]/5.2.2.1; it tells us to consider the following invented non-reference copy-initialization: A a = 7. We're considering it, we're not going through with the initialization of an object of type A. We pretend we're doing it, in order to determine the user-defined conversion to apply to 7, but while pretending we still have to follow the rules.
  • Where are the rules for that non-reference initialization? [dcl.init]/17.6.3.
  • Which sends us to [over.match.copy], which chooses the constructor.
  • Back to 17.6.3, which specifies how that constructor call is seen as a prvalue expresssion of type A.
  • Back to 5.2.2.1, which tells us that the synthesized prvalue ("the result of the call to the conversion function") is used to direct-initialize the reference.

The rules are clearly stated in [dcl.init.ref]/(5.2.2.1)  (emphasis is mine):

(5.2.2.1) — If T1 or T2 is a class type and T1 is not reference-related to T2, user-defined conversions are
                   considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined
                   conversion (8.6, 13.3.1.4, 13.3.1.5); the program is ill-formed if the corresponding non-reference
                   copy-initialization would be ill-formed. The result of the call to the conversion function, as
                   described for the non-reference copy-initialization, is then used to direct-initialize the reference.
                   For this direct-initialization, user-defined conversions are not considered.

Given this quote, the following snippet should be ill-formed, as the copy-constructor is not available, but it's not. It compiles in clang, and GCC, for both C++1z and C++14:

struct A
{
     A
(int) { }
private:
    A
(const A&);
};
A
&& a = 7;

For me, there are only two options: either [dcl.init.ref]/(5.2.2.1) is wrong, or the compiler have bugs. All the rest is pure mystification. In my humble opinion, [dcl.init.ref]/(5.2.2.1) is wrong and I would bet compiler implementers don't give a s**t to copy-initialization, when they ascertain the well-formedness of the code above.
 

bogdan

unread,
Aug 17, 2016, 11:35:18 AM8/17/16
to ISO C++ Standard - Discussion

My reply was to your first sentence "I don't understand the reason why you insist [dcl.init]/(17.6.3) would apply here". I hope the reason for which that paragraph applies here is now clear and we are in agreement.

Regarding the well-formedness of your last example, yes, as far as I can tell, that initialization is ill-formed in C++14 and compilers are wrong to accept it. I guess they don't implement DR1604 in its entirety.

Note that in C++17 the code is well-formed, as the non-reference copy-initialization no longer involves the copy constructor (guaranteed copy elision).

Belloc

unread,
Aug 18, 2016, 6:19:01 PM8/18/16
to ISO C++ Standard - Discussion


On Wednesday, August 17, 2016 at 12:35:18 PM UTC-3, bogdan wrote:

Regarding the well-formedness of your last example, yes, as far as I can tell, that initialization is ill-formed in C++14 and compilers are wrong to accept it. I guess they don't implement DR1604 in its entirety.

Note that in C++17 the code is well-formed, as the non-reference copy-initialization no longer involves the copy constructor (guaranteed copy elision).

As far as I can see, they didn't implement DR 1604 because it didn't matter at the time. And it still doesn't matter in C++17, even if we disregard P0135R1. Why it doesn't matter? Because [dcl.inti.ref]/(5.2.2.1) creates a temporary and binds this temporary to the reference. What difference would it make, whether this temporary is created with a copy, or with direct-initialization? By ignoring copy-initialization in [dcl.init.ref]/(5.2.2.1), compilers are(were) just avoiding the type of confusing error messages, that would have to be emitted, in case the copy constructor was unavailable or deleted for the target type. For example, the snippet below (used as an example in DR 1604) was already well-formed by both clang and CGC, in C++11, and have been maintained like this, since then.

struct X {
    X
(int) {}
    X
(X const &) = delete;
};


void f() {
    X
const &x = 0;
}

In summary, what I'm disputing here is why do we need to refer to the term "copy-initialization" in [dcl.init.ref]/(5.2.2.1)?

bogdan

unread,
Aug 18, 2016, 8:45:40 PM8/18/16
to ISO C++ Standard - Discussion


Using copy-initialization in there does matter, and compilers definitely don't ignore that. Example:

struct A
{
   
explicit A(int) { }
};

A
&& a(7);

All four compilers that I tested reject this, which is consistent with the intuitive meaning of explicit in such a declaration: if you want an object of type A to be constructed from an int, you have to explicitly request it.

The same goes for conversion functions that are used in the context of 5.2.2.1:

struct A
{
   
explicit operator int() { return 7; }
};

long&& i(A{});

Unanimously rejected as well.

Belloc

unread,
Aug 19, 2016, 2:48:42 PM8/19/16
to ISO C++ Standard - Discussion


On Thursday, August 18, 2016 at 9:45:40 PM UTC-3, bogdan wrote:

Using copy-initialization in there does matter, and compilers definitely don't ignore that. Example:

struct A
{
   
explicit A(int) { }
};

A
&& a(7);

All four compilers that I tested reject this, which is consistent with the intuitive meaning of explicit in such a declaration: if you want an object of type A to be constructed from an int, you have to explicitly request it.

The same goes for conversion functions that are used in the context of 5.2.2.1:

struct A
{
   
explicit operator int() { return 7; }
};

long&& i(A{});

Unanimously rejected as well.


Congratulations. I can't argue against these two examples.

bogdan

unread,
Aug 23, 2016, 7:50:02 PM8/23/16
to ISO C++ Standard - Discussion

On Tuesday, August 16, 2016 at 2:21:46 PM UTC+3, bogdan wrote:
Usefulness of the change notwithstanding, there's a question about cv-qualification: in the constructor case, the current specification will eventually bind the reference to a temporary of type T1, not cv1 T1. This is inconsistent with the case when T1 is a non-class type, where 5.2.2.2 (when it works correctly) creates a temporary of type cv1 T1. I think for indirect binding it makes sense that the type of the reference dictates the type of the temporary, cv-qualification included, in all cases. What is the intended behaviour in this case?


While thinking about how the above compares to list-initialization of references, I noticed that list-initialization seems to exhibit the same anomaly that 5.2.2.1 as modified by DR1571 intends to avoid:

struct A
{
   
operator volatile int&();
};

const int& a1 = {A{}.operator volatile int&()}; // ill-formed
const int& a2 = {A{}};                          // well-formed

Is the use of list-initialization syntax considered enough of a reason for this to be acceptable? Any deeper reason that I can't see?

bogdan

unread,
Aug 24, 2016, 10:09:38 AM8/24/16
to ISO C++ Standard - Discussion

It looks like CWG1996 might be relevant here. Is the plan to bring reference list-initialization in line with the non-list kind, including the above issue?

Reply all
Reply to author
Forward
0 new messages