Uniform initialization fails to copy when object has no data members
Asked Answered
H

2

10

In updating some code to use uniform initialization, I thought it would be a drop-in modern substitue for the now "old style" parentheses style. I know this isn't always the case (obvious example, vector<int>) but I've stumbled over another difference that I don't understand.

class Object {
    public:
        Object() = default;
        Object(const Object&) = default;
};

int main() {
    Object o;
    Object copy{o}; // error
    Object copy2(o); // OK
}

fails to compile under clang3.5 with the error: (also fails under gcc)

error: excess elements in struct initializer

There are two different changes to Object that make this work. Either adding a data member to it, or giving it an empty copy constructor body

class Object {
    private:
        int i; // this fixes it
    public:
        Object() = default;
        Object(const Object&) { } // and/or this fixes it as well
};

I don't see why these should make a difference.

Hymie answered 24/1, 2014 at 19:23 Comment(7)
Adding the data member allows aggregate initialization of it. Adding the empty body makes the constructor non-trivial.Node
I.e. if the class is an aggregate, aggregate-initialization will be performed regardless of the number and types of the initializers in the braced-init-list. Adding the empty body makes the class a non-aggregate (because now the ctor is user-provided).Cognoscenti
Sorry, adding the member as public allows aggregate initialization of it (giving an appropriate conversion error). Adding it as private might affect a property of the class, but I'm not sure.Node
@Node Indeed, if a class has a private or protected member => it's not an aggregate.Cognoscenti
"but I'm still understanding uniform initialization so it's likely I'm missing a key concept." It has the same form for every type, but not the same semantics.Cognoscenti
@dyp, Yup, just looked it up myself, thanks. So then either of these changes causes the class to not be an aggregate and thus not a candidate for aggregate initialization, and that combined with aggregate initialization being preferred over calling a constructor forms a good picture.Node
@dyp, Yes, just making sure I can find all the standardese to support it :pNode
N
3

Johannes's answer is useful, but let me elaborate on why it currently happens.

Both of the changes you describe affect your class by making it go from an aggregate to not an aggregate. See C++11 (N3485) § 8.5.1/1:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal-initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

A constructor with a definition of = default is considered not user-defined.

Then, going down to list-initialization in § 8.5.4, we see:

List-initialization of an object or reference of type T is defined as follows:

  • If T is an aggregate, aggregate initialization is performed

And then a bunch of "Otherwise..." sections. Thus, changing either of these allows a constructor to be called instead of performing aggregate initialization.

The new proposed standardese for the list-initialization definition (as viewable in Johannes's link) provides a prioritized case of a single element in the list, and it having a type of (or really close to) the type of the object being initialized. Aggregate initialization would then be top priority after that.

Node answered 24/1, 2014 at 19:44 Comment(0)
J
6

This is a known bug and will hopefully be fixed in C++17 (not for C++14, see http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1467). Your struct is an aggregate, so to initialize it with {someElement} there needs to be at least one data member, as you have discovered. Try providing an operator int(); and you will see it compiles.

Jessicajessie answered 24/1, 2014 at 19:34 Comment(1)
both clang (3.8) and g++ (6.1) seem to change the above behavior, even if compiling with -std=c++11, the OP code now compiles fine with these newer versions. Does it mean that clang and g++ went forward to C++17 (for C++11) in a non-standard way? This may change the behavior of some code, e.g. stackoverflow.com/questions/38254106 shouldn't that worry the compilers?Nessi
N
3

Johannes's answer is useful, but let me elaborate on why it currently happens.

Both of the changes you describe affect your class by making it go from an aggregate to not an aggregate. See C++11 (N3485) § 8.5.1/1:

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal-initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3).

A constructor with a definition of = default is considered not user-defined.

Then, going down to list-initialization in § 8.5.4, we see:

List-initialization of an object or reference of type T is defined as follows:

  • If T is an aggregate, aggregate initialization is performed

And then a bunch of "Otherwise..." sections. Thus, changing either of these allows a constructor to be called instead of performing aggregate initialization.

The new proposed standardese for the list-initialization definition (as viewable in Johannes's link) provides a prioritized case of a single element in the list, and it having a type of (or really close to) the type of the object being initialized. Aggregate initialization would then be top priority after that.

Node answered 24/1, 2014 at 19:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.