Move constructor forces copy assignment to be implicitly defaulted?

Hello,

The following piece of code:

copytest.cpp

My reading of the relevant section of the standard (I only have N3126
handy, so I may be mistaken): §12.8/20:

     If the class definition does not explicitly declare a
     copy assignment operator and there is no user-declared
     move assignment operator, a copy assignment operator
     is implicitly declared as defaulted (8.4).

tells me that an implicitly *defaulted* copy assignment operator
should've been generated and used instead.

This paragraph now reads:

If the class definition does not explicitly declare a
copy assignment operator, one is declared implicitly.
If the class definition declares a move constructor or
move assignment operator, the implicitly declared copy
assignment operator is defined as deleted; otherwise, it
is defined as defaulted (8.4).

So the declaration of movable(movable &&) deletes the copy assignment operator.

This paragraph goes on to say:

The latter case is deprecated if the class has a user-declared
copy constructor or a user-declared destructor.

I.e. even without the move constructor, the declared copy constructor makes the defaulted copy assignment deprecated. It may become deleted in a future standard.

In summary, if you declare either copy member or the destructor, implicit move members are inhibited. If you declare either move member, the copy members are implicitly deleted unless you declare them otherwise.

Howard

Howard,

Thanks a ton for taking the time to explain this. I need to update my
references asap!

In summary, if you declare either copy member or the destructor,
implicit move members are inhibited.

I am assuming that by inhibited you mean something other than deleted.

If you declare either move
member, the copy members are implicitly deleted unless you
declare them otherwise.

This makes sense. It looks like this is as a direct result of core
issue #667 and is tracked by most compiler vendors as N3053: Defining
move special member functions. However, I wasn't able to trace a
particular document that could provide me with a rationale on this
shunning of the copy assignment operator. This is perhaps asking for
too much and not relevant to this group, but if you could point me in
the right direction I'd be much obliged.

<off-topic>
Should I consider this a bug in gcc 4.6? (Status of N3053 is marked as
complete in http://gcc.gnu.org/gcc-4.6/cxx0x_status.html. Also, I
wasn't able to find a relevant bug file against gcc 4.6 on bugzilla.)
</off-topic>

Regards,
Suman

Howard,

Thanks a ton for taking the time to explain this. I need to update my
references asap!

In summary, if you declare either copy member or the destructor,
implicit move members are inhibited.

I am assuming that by inhibited you mean something other than deleted.

Right. I mean that they don't exist, just as in C++98/03. Deleted move members are generally problematic as they inhibit "copying" from rvalues. If you have valid copy members and deleted move members, you can't return such an object from a function.

If you declare either move
member, the copy members are implicitly deleted unless you
declare them otherwise.

This makes sense. It looks like this is as a direct result of core
issue #667 and is tracked by most compiler vendors as N3053: Defining
move special member functions. However, I wasn't able to trace a
particular document that could provide me with a rationale on this
shunning of the copy assignment operator. This is perhaps asking for
too much and not relevant to this group, but if you could point me in
the right direction I'd be much obliged.

My eyes were glazing over during this phase of standardization. But you might take a look at N3201.

<off-topic>
Should I consider this a bug in gcc 4.6? (Status of N3053 is marked as
complete in http://gcc.gnu.org/gcc-4.6/cxx0x_status.html. Also, I
wasn't able to find a relevant bug file against gcc 4.6 on bugzilla.)
</off-topic>

I'm purposefully ignorant of all gcc past 4.2.

Howard

Right. I mean that they don't exist, just as in C++98/03. Deleted move members are generally problematic as they inhibit "copying" from rvalues. If you have valid copy members and deleted move members, you can't return such an object from a function.

Okay, I still can't wrap my head around the last sentence. This looks ominous.

My eyes were glazing over during this phase of standardization. But you might take a look at N3201.

This. And N3174 and Dave Abraham's posts are illuminating.

A big thank you again!

Regards,
Suman

struct A
{
    A() = default;
    A(const A&) = default;
    A& operator=(const A&) = default;
    A(A&&) = delete;
    A& operator=(A&&) = delete;
};

A
make()
{
    A a;
    return a;
}

int main()
{
    A a = make();
}

test.cpp:67:7: error: call to deleted constructor of 'A'
    A a = make();
      ^ ~~~~~~
test.cpp:54:5: note: function has been explicitly marked deleted here
    A(A&&) = delete;
    ^
1 error generated.

But:

struct A
{
    A() = default;
    A(const A&) = default;
    A& operator=(const A&) = default;
};

A
make()
{
    A a;
    return a;
}

int main()
{
    A a = make();
}

Compiles and runs fine.

Howard

Right. I mean that they don't exist, just as in C++98/03. Deleted move members are generally problematic as they inhibit "copying" from rvalues. If you have valid copy members and deleted move members, you can't return such an object from a function.

Okay, I still can't wrap my head around the last sentence. This looks ominous.

[...]

But:

struct A
{
A() = default;
A(const A&) = default;
A& operator=(const A&) = default;
};

A
make()
{
A a;
return a;
}

int main()
{
A a = make();
}

Compiles and runs fine.

This looks like (to my ignorant eye) a fine way of not breaking
C++98/03 code. But I am probably oversimplifying.

A quick question:

struct A {};

struct B : A { ~B() {} };

A makeA() {
A a;
return a;
}

B makeB() {
B b;
return b;
}

int main() {
A a = makeA();
B b = makeB();
}

Is it correct to assume that A will be moved and B will not? Should a
diagnostic be required for the call to makeB?

I am still going through your and Steve's emails -- I'll try to get
back as soon as I am sure I understand.

Regards,
Suman

A quick question:

struct A {};

struct B : A { ~B() {} };

A makeA() {
A a;
return a;
}

B makeB() {
B b;
return b;
}

int main() {
A a = makeA();
B b = makeB();
}

Is it correct to assume that A will be moved and B will not?

Yes.

Should a
diagnostic be required for the call to makeB?

No. B should be allowed to to suppress an implicit move constructor. The implicit version might do the wrong thing.

But you can create your own compile-time diagnostic.

#include <type_traits>
#include <string>

struct A
{
    std::string s;
};

static_assert(std::is_nothrow_move_constructible<A>::value,
              "A should be nothorw move constructible");

struct B
  : public A
{
    ~B();
};

static_assert(std::is_nothrow_move_constructible<B>::value,
              "B should be nothorw move constructible");

A makeA() {
  A a;
  return a;
}

B makeB() {
  B b;
  return b;
}

int main() {
  A a = makeA();
  B b = makeB();
}

test.cpp:18:1: error: static_assert failed "B should be nothorw move constructible"
static_assert(std::is_nothrow_move_constructible<B>::value,
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

Since the move constructor in B has been suppressed, it is no longer nothrow to move B. Now if you add:

    B() = default;
    B(B&&) = default;

then everything will compile.

Howard

Right. My point was that the behavior changes subtly across a
hierarchy. This can be a spot of bother when designing. I understand
the reason -- since the user *defined* a destructor somewhere down the
hierarchy, the compiler (correctly) stays away from (using a crystal
ball and) moving stuff.

There is also this tendency among beginning programmers to define an
empty destructor (for no good reason perhaps and I've seen quite a few
such cases with seasoned ones too). Suddenly, a no-op definition
starts costing more!

Let me summarize my understanding (along with what you've already
written) w.r.t move members:

[1] While both copy and move members can be implicitly
defaulted/deleted only move members can be suppressed.
[2] A move member can never be implicitly defined to be deleted
because of the presence of a copy member or a destructor.

Finally, there's this bit about 12.8/9 that I need help with:

When the move constructor is not implicitly declared or explicitly
supplied, expressions that otherwise would have invoked the move
constructor may instead invoke a copy constructor.

This, I believe, keeps the compiler happy about compiling the example
I posted? Also, is

'When the move constructor is not implicitly declared or explicitly
supplied'

standardese for suppressed/inhibited/deprecated?

Is it correct to consider the case where move members are deprecated
as C++98/03 fallback mode?

Regards,
Suman

Let me summarize my understanding (along with what you've already
written) w.r.t move members:

[1] While both copy and move members can be implicitly
defaulted/deleted only move members can be suppressed.
[2] A move member can never be implicitly defined to be deleted
because of the presence of a copy member or a destructor.

Sounds right to me.

Finally, there's this bit about 12.8/9 that I need help with:

   When the move constructor is not implicitly declared or explicitly
   supplied, expressions that otherwise would have invoked the move
   constructor may instead invoke a copy constructor.

This, I believe, keeps the compiler happy about compiling the example
I posted?

Yes.

Also, is

   'When the move constructor is not implicitly declared or explicitly
   supplied'

standardese for suppressed/inhibited/deprecated?

Yes for suppressed/inhibited. Deprecated means something different. When something in the standard is deprecated, it means that it might be removed in a future version of the standard.

Is it correct to consider the case where move members are deprecated
as C++98/03 fallback mode?

It is correct to consider the case where move members are suppressed/inhibited/absent as just like you would write in C++98/03.

Howard