It depends on the inheritance hierarchy. But chances are very good that this code is fine. Here is a complete demo showing that it is safe (for this specific demo):
#include <iostream>
struct base1
{
base1() = default;
base1(base1&&) {std::cout << "base1(base1&&)\n";}
};
struct base2
{
base2() = default;
base2(base2&&) {std::cout << "base2(base2&&)\n";}
};
struct br1
{
br1() = default;
br1(br1&&) {std::cout << "br1(br1&&)\n";}
};
struct br2
{
br2() = default;
br2(br2&&) {std::cout << "br2(br2&&)\n";}
};
struct X
: public base1
, public base2
{
br1 mbr1;
br2 mbr2;
public:
X() = default;
X(X&& rhs)
: base1(std::move(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
};
int
main()
{
X x1;
X x2 = std::move(x1);
}
which should output:
base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)
Here you see that each base and each member is moved exactly once.
Remember: std::move
doesn't really move. It is just a cast to rvalue, nothing more.
So the code casts to rvalue X
and then passes that down to the base classes. Assuming the base classes look like I have outlined above, then there is an implicit cast to rvalue base1
and base2
, which will move construct those two separate bases.
Also,
Remember: A moved-from object is in a valid but unspecified state.
As long as the move constructor of base1
and base2
don't reach up into the derived class and alter mbr1
or mbr2
, then those members are still in a known state and ready to be moved from. No problems.
Now I did mention that problems could occur. This is how:
#include <iostream>
struct base1
{
base1() = default;
base1(base1&& b)
{std::cout << "base1(base1&&)\n";}
template <class T>
base1(T&& t)
{std::cout << "move from X\n";}
};
struct base2
{
base2() = default;
base2(base2&& b)
{std::cout << "base2(base2&&)\n";}
};
struct br1
{
br1() = default;
br1(br1&&) {std::cout << "br1(br1&&)\n";}
};
struct br2
{
br2() = default;
br2(br2&&) {std::cout << "br2(br2&&)\n";}
};
struct X
: public base1
, public base2
{
br1 mbr1;
br2 mbr2;
public:
X() = default;
X(X&& rhs)
: base1(std::move(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
};
int
main()
{
X x1;
X x2 = std::move(x1);
}
In this example, base1
has a templated constructor that takes an rvalue-something. If this constructor can bind to an rvalue X
, and if this constructor will move from the rvalue X
, then you have problems:
move from X
base2(base2&&)
br1(br1&&)
br2(br2&&)
The way to fix this problem (which is relatively rare, but not vanishingly rare), is to forward<base1>(rhs)
instead of move(rhs)
:
X(X&& rhs)
: base1(std::forward<base1>(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
Now base1
sees an rvalue base1
instead of an rvalue X
, and that will bind to the base1
move constructor (assuming it exists), and so you again get:
base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)
And all again is good with the world.