5

With a simple struct such as

struct Foo { int i; };

I can create a new instance using an initializer list; no need to write a constructor:

Foo foo { 314 };

If I now add a move constructor

struct Bar
{
    int i;
    Bar(Bar&& other) { i = other.i; }
};

The initializer no longer works and I have to add a constructor too:

Bar(int i) : i(i) {}

I'm guessing this behavior is somewhat related to this answer (for user-defined move-constructor disables the implicit copy-constructor?), but more details would be nice.

Edit: as indicated by the answers, this has to do with adding a constructor. Which in turn would seem to create an inconsistency of sorts, if I add just a move operator:

struct Baz
{
    int i;

    Baz& operator=(Baz&& other)
    {
        this->i = other.i;
        return *this;
    }
};

The initializer works again, although with a slightly different syntax for "move" (yes, this is actually default construct and move assignment; but the end result seems about the same):

Baz baz{ 3141 };
Baz b;
b = std::move(baz);
Community
  • 1
  • 1
Ðаn
  • 10,934
  • 11
  • 59
  • 95
  • in your edit, `b` will be default constructed and move assigned, not just different syntax – apple apple Nov 25 '16 at 15:38
  • 3
    I assume your real case is different but please note that nothing about the move semantics requires you to *clear* the source object (especially not its trivial members like `int`s). Just to leave it in a state where it's safe to destruct, reusing all resources you can. The point of swapping a vector for an empty vector, for example, is that the *ownership* of the underlying array is transmitted (instead of the contents cloned) and yet the source has something in it which makes sense, not that it would be necessary that the moved-from object has empty contents. – The Vee Nov 25 '16 at 15:38
  • 1
    @Dan may I ask you why you want this kind of initialization? – apple apple Nov 25 '16 at 15:45
  • @Dan maybe you can use default move constructor. – apple apple Nov 25 '16 at 15:49

3 Answers3

6

When there are no constructors this syntax is aggregate initialization because this structure is an aggregate.

When a constructor is added this structure is no longer an aggregate, aggregate initialization cannot be used. The exact rules are listed in list initialization, the relevant ones are:

The effects of list initialization of an object of type T are:

  • Otherwise, if T is an aggregate type, aggregate initialization is performed.
  • Otherwise, the constructors of T are considered, in two phases:...
Community
  • 1
  • 1
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
4

It is not initializer list construction that is disabled by the move constructor (that was not present there to start with), but aggregate construction. And for a good reason: by adding a custom constructor, we indicate to the compiler exactly that the class in not an aggregate, that something different is necessary than just operate on each of its members one by one.

For an aggregate, the default default, copy, and move constructor will work well even if the member variables have nontrivial types. A copy construction will automatically be deleted if it can't be delegated to them, leaving move construction usable:

struct A { // non-copyable
  int a;
  int b;
  A(int a_, int b_): a(a_), b(b_) { std::cout << "A(int,int)\n"; }
  A() { std::cout << "A()\n"; }
  A(const A&) = delete;
  A(A&&) { std::cout << "A(A&&)\n"; }
};

struct B {
  A a;
};

int main() {
  B b1{{1,2}}; // OK: aggregate
  B b2{std::move(b1)}; // OK: calls A::A(A&&)
  //B b3{b1}; // error: B::B(const B&) auto-deleted
}

However, if you want to delete copy construction for some other reason and keep the others at default, just be explicit about it:

struct A { // copyable
  int a;
  int b;
  A(int a_, int b_): a(a_), b(b_) { std::cout << "A(int,int)\n"; }
  A() { std::cout << "A()\n"; }
  A(const A&) { std::cout << "A(const A&)\n"; }
  A(A&&) { std::cout << "A(A&&)\n"; }
};

struct B { // non-copyable
  A a;
  B() = default;
  B(const B&) = delete;
  B(B&&) = default;
};

int main() {
  B b1{{1,2}}; // OK: still an aggregate
  B b2{std::move(b1)}; // delegates to A::A(A&&)
  //B b3{b1}; // error
}
The Vee
  • 11,420
  • 5
  • 27
  • 60
3

Because you're using aggregate initialization, which says:

Aggregate initialization is a form of list-initialization, which initializes aggregates An aggregate is one of the following types: array type class type (typically, struct or union), that has

  • no private or protected non-static data members
  • no user-provided, inherited, or explicit (since C++17) constructors (explicitly defaulted or deleted constructors are allowed) (since C++11)
  • no virtual, private, or protected (since C++17) base classes
  • no virtual member functions

Point 2 makes your case fail.

Hatted Rooster
  • 35,759
  • 6
  • 62
  • 122
  • @Dan When you start implementing a user-provided constructor then sadly not, you'll have to strick to writing a constructor (for now). – Hatted Rooster Nov 25 '16 at 15:32
  • 1
    @Dan There's no advantage in having a move constructor like yours for POD (plain old data) types: actually, with the zeroing of the memory your computer will in fact have more work to do. The default copy constructor is optimal here so the suggested alternative is to not implement anything extra. – The Vee Nov 25 '16 at 15:32
  • @TheVee I'm assuming however that in the real case the type is more complex, good point though. – Hatted Rooster Nov 25 '16 at 15:34