list initialization of aggregates: when can it invoke copy constructor?
Asked Answered
S

2

7

Consider the following code:

struct A {
  int x;
};

int main() {
  A a;
  A b{a};
}

Is this program well-formed at C++11 standard? In my copy of N3797 it says

8.5.4 List initialization [dcl.init.list]

3: List-initialization of an object or reference of type T is defined as follows:
- If T is an aggregate, aggregate initialization is performed (8.5.1).
- Otherwise, if T is a specialization of std::initializer_list<E>, ...
- Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen using overload resolution. If a narrowing conversion is required to convert any of the types, the program is ill-formed.
- Otherwise, if the initializer list has a single element of type E and either T is not a reference type or it is reference-related to E, the object or reference is initialized from that element; if a narrowing conversion is required to convert the element to T, the program is ill-formed.
- Otherwise, if T is a reference type, a pr-value temporary of the type reference by T is copy-list-initialized or direct-list-initialized, depending on the kind of initialization for the reference, and the reference is bound to that temporary.
- Otherwise, if the initializer list has no elements, the object is value-initialized.
- Otherwise, the program is ill-formed.

The point of the example is, the type is an aggregate, but list-initialization is supposed to invoke the copy constructor. On gcc 4.8 and gcc 4.9, at C++11 standard, it fails:

main.cpp: In function ‘int main()’:
main.cpp:7:8: error: cannot convert ‘A’ to ‘int’ in initialization
   A b{a};
        ^

and says A is not convertible to int or similar, because aggregate initialization fails. On gcc 5.4, it works fine at C++11 standard.

On clang you get similar errors with clang-3.5, 3.6, and it starts working at clang-3.7.

I understand that it is well-formed at C++14 standard, and that it was mentioned in a defect report here.

However, what I don't understand is why this was considered a defect in the standard.

When the standard writes,

"If X, foo-initialization is performed. Otherwise, if Y, bar-initialization is performed, .... Otherwise, the program is ill-formed.",

doesn't this mean that if X holds, but foo-initialization cannot be performed, then we should check if Y holds, and then attempt bar-initialization?

This would make the example work, because when aggregate initialization fails, we don't match std::initializer_list, and the next condition we match is "T is a class type", and then we consider constructors.

Note that this does seem to be how it works in this modified example

struct A {
  int x;
};

int main() {
  A a;
  const A & ref;
  A b{ref};
}

All the same compilers treat this the same way as the earlier example, at C++11 and C++14 standards. But it seems that the modified wording from the CWG defect record doesn't apply to this case. It reads:

If T is a class type and the initializer list has a single element of type cv T or a class type derived from T, the object is initialized from that element.

http://open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1467

But in the second code example, the initializer list technically contains const T &. So I don't see how it would work unless after aggregate initialization fails, we are supposed to attempt constructors.

Am I wrong? Is it not supposed to attempt constructors after aggregate initialization fails?

Here's a related example:

#include <iostream>

struct B {
  int x;

  operator int() const { return 2; }
};

int main() {
  B b{1};
  B c{b};
  std::cout << c.x << std::endl;
}

In clang-3.6, gcc-4.8, gcc-4.9, it prints 2, and in clang-3.7, gcc-5.0 it prints 1.

Assuming I'm wrong, and at C++11 standard, list initialization of an aggregate is supposed to be aggregate initialization and nothing else, until the new wording in the defect report is introduced, is it a bug that this happens even when I select -std=c++11 on the newer compilers?

Sou answered 17/8, 2016 at 20:27 Comment(0)
D
3

When the standard writes,

"If X, foo-initialization is performed. Otherwise, if Y, bar-initialization is performed, ...

doesn't this mean that if X holds, but foo-initialization cannot be performed, then we should check if Y holds, and then attempt bar-initialization?

No. If X holds we perform foo-initialization. If that fails the program is ill-formed.

Damper answered 17/8, 2016 at 20:32 Comment(2)
So what do you think about the struct B example? You think gcc and clang are using some C++14 feature that shouldn't be available in C++11 standard?Sou
@ChrisBeck: I think they've decided that this was a clear defect in the C++11 standard, and they have implemented the fixed version. I suspect the only reason this never got to "resolved" was that they have started doing C++ releases much faster, so rather than fixing defects in the old standards, they just fix it in the latest.Damper
L
4

When the standard writes,

"If X, foo-initialization is performed. Otherwise, if Y, bar-initialization is performed, .... Otherwise, the program is ill-formed.",

doesn't this mean that if X holds, but foo-initialization cannot be performed, then we should check if Y holds, and then attempt bar-initialization?

No, it does not. Think of it like actual code:

T *p = ...;
if(p)
{
  p->Something();
}
else
{ ... }

p isn't NULL. That doesn't mean it's a valid pointer either. If p points to a destroyed object, p->Something() failing will not cause you to skip to the else. You had your chance to protect the call in the condition.

So you get undefined behavior.

The same goes here. If X, do A. That doesn't imply what happens if A fails; it tell you to do it. If it can't be done... you're screwed.

Loehr answered 17/8, 2016 at 20:33 Comment(7)
So you think in my example with struct B, gcc-5 should behave the same as gcc-4.8 and gcc-4.9, when std=c++11?Sou
@ChrisBeck: That's a different question. That's a question of whether defect fixes should be retroactively applied to existing standards. And the answer to that one is, generally speaking, yes. That's what a defect report is; it's a bug in the standard. Bugs should be fixed, and you shouldn't have to say you're using the next version to get them, right?Loehr
I mean there is a difference between a bug in the implementation of the standard, and a defect in the wording of the standard. The former, yeah that should be resolved by a release incrementing only the patch-level, ideally. But if I say std=c++11 I generally assume they are trying to implement N3797. Otherwise it means I can't really know what the compiler will try to do unless I read all the standards. Or I have to tell people "you can't use clang after version 3.6, or unfortunately it will compile but the intializations won't work like intended and you'll see 1 instead of 2"Sou
Maybe I didn't understand what "defect reports" are in C++. Are defect reports, a problem with release X that we hope to address in next release, or do they essentially have bugfix releases of previously released standards? I thought that basically the C++11 standard doesn't change after it's published, and any defect reports are discussion items for C++14 etc.Sou
@ChrisBeck: Defect reports are bug in the standard. The committee does not make bugfix releases of the whole standard, but they do make it clear when they've approved a defect report and exactly what the fix is in terms of wording.Loehr
I see. So on the defect report page, it says "Issues with DR, accepted, DRWP, and WP status are NOT part of the International Standard for C++. They are provided for informational purposes only, as an indication of the intent of the Committee." And when I ask the compiler for --std=c++11, it means, C++ as described in C++11 standard document, plus any defect reports which clarify the intent of the standards committee? Or something like this?Sou
I guess it's extremely unlikely that this change would actually break a program designed for c++11 -- my example is pretty artificial, and an aggregate can't contain a copy of itself, so A b{a}; just would have failed with the older compilers, unless these goofy implicit conversions are involved. Maybe if the aggregate contains a reference to itself or something, but that's also pretty artificial.Sou
D
3

When the standard writes,

"If X, foo-initialization is performed. Otherwise, if Y, bar-initialization is performed, ...

doesn't this mean that if X holds, but foo-initialization cannot be performed, then we should check if Y holds, and then attempt bar-initialization?

No. If X holds we perform foo-initialization. If that fails the program is ill-formed.

Damper answered 17/8, 2016 at 20:32 Comment(2)
So what do you think about the struct B example? You think gcc and clang are using some C++14 feature that shouldn't be available in C++11 standard?Sou
@ChrisBeck: I think they've decided that this was a clear defect in the C++11 standard, and they have implemented the fixed version. I suspect the only reason this never got to "resolved" was that they have started doing C++ releases much faster, so rather than fixing defects in the old standards, they just fix it in the latest.Damper

© 2022 - 2024 — McMap. All rights reserved.