Default move constructor/assignment


J

Juha Nieminen

I can't find any info about this with google.

This is based on the behavior of clang++ (version 3.1), and my question
would be if this behavior is indeed how the C++11 standard defines it.

1) Classes do not have default move constructors and assignment
operators. They must always be explicitly declared. Is this correct?

2) Seemingly, if you declare a move constructor for a class, that
makes the copy assignment operator explicitly deleted. Is this correct?
(Does it also make the move assignment operator explicitly deleted?)

3) If the members of the class have their own move constructors and
assignment operators, it is enough to declare a default move constructor
and assignment for the class, like:

class MyClass
{
public:
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;

private:
AnotherClass aMember;
};

If done like this, and if 'AnotherClass' has a move constructor and
assignment operator, those will be called by the defaulted ones. Is this
also correct?

4) In the implementation of a move assignment operator, is it necessary
to check for an assignment to itself? I can't think of a situation where
a temporary could be assigned to itself.
 
Ad

Advertisements

J

Juha Nieminen

Juha Nieminen said:
I can't find any info about this with google.

Seemingly I have encountered such cutting-edge C++11 technology that
nobody even here seems to know the answers... :)

Well, I'll just have to trust that clang++ is correct and
standard-conforming.
 
M

Miles Bader

Juha Nieminen said:
Seemingly I have encountered such cutting-edge C++11 technology
that nobody even here seems to know the answers... :)

FWIW, my memory is that the compiler should make default move
constructors _but_ will avoid doing so if you define _any_ explicit
move _or_ copy constructors (and similarly, an explicit move
constructor will suppress default copy constructors).

[I believe the "suppresses everything" behavior was a lateish change
in the standard, and caused some problems compiling libstdc++ with
clang at some point when clang had been updated to reflect the change,
but libstdc++ had not... but my impression is that both gcc and clang
are both up-to-date now in their latest releases.]

But that's just my vague memory, and I have no links or tests at hand
to back it up, so take it with a large grain of salt ...

-miles
 
W

woodbrian77

Seemingly I have encountered such cutting-edge C++11 technology that
nobody even here seems to know the answers... :)

Well, I'll just have to trust that clang++ is correct and
standard-conforming.


Shalom

The following is from Howard Hinnant in May 2011.

The rules for an implicitly generated move constructor are:

If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if

•X does not have a user-declared copy constructor,
•X does not have a user-declared copy assignment operator,
•X does not have a user-declared move assignment operator,
•X does not have a user-declared destructor, and
•the move constructor would not be implicitly defined as deleted.
The rules for implicitly generated move assignment operators follows the pattern above.

The rules for when a copy constructor is implicitly generated have changed slightly!

If the class definition does not explicitly declare a copy constructor, oneis declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The lattercase is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

And similarly for the copy assignment operator:

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). The latter case is deprecated if the class has a user-declared copy constructor or a user-declared destructor.

Bottom line: The rule of 3 is now the rule of 5. You can either ignore all 5 (if the default behavior works for you), or you need to think about (and likely define) all 5:

1.copy constructor
2.copy assignment
3.move constructor
4.move assignment
5.destructor

-----------------------------------------------------------
I'm not sure how up to date this is. I recall Dave Abrahams
commenting on this subject on cppnext also.

Brian Wood
Ebenezer Enterprises
http://webEbenezer.net
 
J

Juha Nieminen

???X does not have a user-declared copy constructor,
???X does not have a user-declared copy assignment operator,
???X does not have a user-declared move assignment operator,
???X does not have a user-declared destructor, and
???the move constructor would not be implicitly defined as deleted.

So it's not necessary to explicitly declare a move constructor or
assignment operator eg. for a struct having movable objects as members?

Now that I explicitly test this with clang++, it indeed seems to be so.
I'm not sure now why in the application I was making I was not getting
this behavior (iow. the move constructors of the members were not being
called, and instead the copy constructors were, unless I explicitly
declared the default move constructor of the containing class.)
 
W

woodbrian77

So it's not necessary to explicitly declare a move constructor or
assignment operator eg. for a struct having movable objects as members?

Right. Here's the link to the cpp-next article I mentioned --
http://cpp-next.com/archive/2011/02/w00t-w00t-nix-nix/#more-3419
Now that I explicitly test this with clang++, it indeed seems to be so.
I'm not sure now why in the application I was making I was not getting
this behavior (iow. the move constructors of the members were not being
called, and instead the copy constructors were, unless I explicitly
declared the default move constructor of the containing class.)

Perhaps adding a call to ::std::move would be needed? Post some
code.
 
Ad

Advertisements

J

Juha Nieminen

Right. Here's the link to the cpp-next article I mentioned --
http://cpp-next.com/archive/2011/02/w00t-w00t-nix-nix/#more-3419

I'm glad that move constructors are implicitly generated by the
compiler at least on simple cases because if they weren't, that would
*severely* limit the usefulness of having them in the first place.

For example, you would benefit from move constructors when using eg.
a std::vector directly, but immediately when you put that std::vector
inside a simple struct, you would lose those benefits (unless you went
into the trouble of explicitly declaring the move constructor).

I didn't really get what kind of code do these implicit move constructors
break, but I'm pretty certain that the benefits greatly outweight the
disadvantages.
 
W

woodbrian77

I'm glad that move constructors are implicitly generated by the
compiler at least on simple cases because if they weren't, that would
*severely* limit the usefulness of having them in the first place.

For example, you would benefit from move constructors when using eg.
a std::vector directly, but immediately when you put that std::vector
inside a simple struct, you would lose those benefits (unless you went
into the trouble of explicitly declaring the move constructor).

I didn't really get what kind of code do these implicit move constructors
break, but I'm pretty certain that the benefits greatly outweight the
disadvantages.

Yeah, I pretty much agree.

http://www.velocityreviews.com/foru...ants-and-implicit-move-constructors-c-0x.html
 
H

Howard Hinnant


I agree too. However it is good to be aware of the cases that do
break, so you can learn to recognize them, and learn how to fix them.

The most convincing case I've seen to date is this one:

http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/6b158b62e7debf05#

The code shown doesn't break. And the original code shown has
incorrect move members. But nevertheless it is a reasonable example
of code that might break when moving from C++03 to C++11. To break it
you have to dress it up a little more:

#include <cassert>
#include <vector>
#include <iostream>

typedef unsigned char Byte;
typedef std::vector<Byte> ImageVec;

class Image
{
ImageVec m_data;
size_t m_width;
size_t m_height;
public:
Image(): m_width(0), m_height(0) {}
Image(Byte* img_data, size_t width, size_t height):
m_data(img_data, img_data + width * height * 3),
m_width(width),
m_height(height) {
}

Byte& operator()(size_t i, size_t j, size_t k)
{
return m_data[3*m_width*i + 3*j + k];
}

friend
std::eek:stream& operator<<(std::eek:stream& os, const Image& img)
{
os << "{\n";
if (img.m_height > 0)
{
os << " {";
if (img.m_width > 0)
{
os << '{';
os << static_cast<unsigned>(img.m_data[0]) << ','
<< static_cast<unsigned>(img.m_data[1]) << ','
<< static_cast<unsigned>(img.m_data[2]);
os << '}';
for (size_t j = 1; j < img.m_width; ++j)
{
os << ",{";
os << static_cast<unsigned>(img.m_data[3*j]) <<
','
<< static_cast<unsigned>(img.m_data[3*j+1]) <<
','
<< static_cast<unsigned>(img.m_data[3*j+2]);
os << '}';
}
}
os << "}";
for (size_t i = 1; i < img.m_height; ++i)
{
os << ",\n {";
if (img.m_width > 0)
{
os << '{';
os <<
static_cast<unsigned>(img.m_data[3*img.m_width*i]) << ','
<<
static_cast<unsigned>(img.m_data[3*img.m_width*i+1]) << ','
<<
static_cast<unsigned>(img.m_data[3*img.m_width*i+2]);
os << '}';
for (size_t j = 1; j < img.m_width; ++j)
{
os << ",{";
os <<
static_cast<unsigned>(img.m_data[3*img.m_width*i+3*j]) << ','
<<
static_cast<unsigned>(img.m_data[3*img.m_width*i+3*j+1]) << ','
<<
static_cast<unsigned>(img.m_data[3*img.m_width*i+3*j+2]);
os << '}';
}
}
os << "}";
}
}
return os << "\n}\n";
}

friend
bool
operator==(const Image& x, const Image& y)
{
return x.m_width == y.m_width && x.m_height == y.m_height &&
x.m_data == y.m_data;
}

friend
bool
operator!=(const Image& x, const Image& y)
{
return !(x == y);
}
};

Image
get_Image()
{
Byte img_data[][6][3] =
{
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
};
return Image(reinterpret_cast<Byte*>(img_data), 6, 4);
};

int main()
{
std::vector<Image> v;
v.push_back(get_Image());
v.push_back(get_Image());
v.push_back(get_Image());
std::cout << v.back() << '\n';
v.back()(0, 0, 0) = 0;
std::vector<Image>::iterator j = std::unique(v.begin(), v.end());
assert(j == v.begin() + 2);
std::cout << v.back() << '\n';
}

The key to why this code breaks is:

1. It's ostream<<() friend assumes that m_data.size() == 3 * m_width
* m_height, and indeed that is a good assumption in C++03.

2. Client code actually inspects (prints out) a value that will be
moved from in C++11. It does this by calling std::unique on a
sequence of Images, which ends up moving from Images at the end of the
sequence (in order to "remove" duplicates).

In C++03 this program prints out:

{
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

{
{{0,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

But when recompiled for C++11, it prints out (for me):

{
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

{
Segmentation fault: 11

Now if main() had used unique in the idiomatic way:

v.erase(std::unique(v.begin(), v.end()), v.end());

there would have been no crash. It is only because main used the
moved-from value at v.back() in a way that required Image's invariant
to hold that causes the crash.

The easiest way to fix this is to default the copy members:

Image(const Image&) = default;
Image& operator=(const Image&) = default;

Just doing this inhibits auto-generation of the move members, and then
the code again prints out:

{
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

{
{{0,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

If you want to properly add the move members, you can't default them,
as the defaults break the invariant as discussed above. The proper
move members must set the source to the default constructed
(resourceless) state:

Image(Image&& img)
: m_data(std::move(img.m_data)),
m_width(img.m_width),
m_height(img.m_height)
{
img.m_data.clear();
img.m_width = 0;
img.m_height = 0;
}

Image& operator=(Image&& img)
{
m_data = std::move(img.m_data);
m_width = img.m_width;
m_height = img.m_height;
img.m_data.clear();
img.m_width = 0;
img.m_height = 0;
return *this;
}

And now the code prints out:

{
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}},
{{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3},{1,2,3}}
}

{

}

Howard
 
W

woodbrian77

I agree too. However it is good to be aware of the cases that do
break, so you can learn to recognize them, and learn how to fix them.

Right.

The most convincing case I've seen to date is this one:

Thanks for the detailed explanation.

Image(Image&& img)
: m_data(std::move(img.m_data)),

I recall something Anthony Williams wrote about the
members of an rvalue reference being rvalue references
and not needing to use std::move there. Perhaps you
use it as a reminder of what is happening and not
because it's really needed?
 
W

woodbrian77

Thanks for the detailed explanation.



I recall something Anthony Williams wrote about the
members of an rvalue reference being rvalue references
and not needing to use std::move there. Perhaps you
use it as a reminder of what is happening and not
because it's really needed?

I read the thread you linked to in the moderated group
now and the std::move is needed. It's been a while since
I read what Anthony wrote so don't know if I'm remembering
things wrong or he was mistaken.
 
Ad

Advertisements

S

SG

Thanks for the detailed explanation.


I recall something Anthony Williams wrote about the
members of an rvalue reference being rvalue references
and not needing to use std::move there.  Perhaps you
use it as a reminder of what is happening and not
because it's really needed?

References have no members. And `img` is an lvalue expression. So an
explicit std::move is required to select the move constructor. But
you can also write

std::move(img).m_data

because member access on an rvalue expression yields an rvalue
expression.

Cheers!
SG
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top