18

This is the move constructor of class X:

X::X(X&& rhs)
    : base1(std::move(rhs))
    , base2(std::move(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{ }

These are the things I'm wary about:

  1. We're moving from rhs twice and rhs isn't guaranteed to be in a valid state. Isn't that undefined behavior for the initialization of base2?
  2. We're moving the corresponding members of rhs to mbr1 and mbr2 but since rhs was already moved from (and again, it's not guaranteed to be in a valid state) why should this work?

This isn't my code. I found it on a site. Is this move constructor safe? And if so, how?

0

3 Answers 3

16

This is approximately how an implicit move constructor typically works: each base and member subobject is move-constructed from the corresponding subobject of rhs.

Assuming that base1 and base2 are bases of X that do not have constructors taking X / X& / X&& / const X&, it's safe as written. std::move(rhs) will implicitly convert to base1&& (respectively base2&&) when passed to the base class initializers.

EDIT: The assumption has actually bitten me a couple of times when I had a template constructor in a base class that exactly matched X&&. It would be safer (albeit incredibly pedantic) to perform the conversions explicitly:

X::X(X&& rhs)
    : base1(std::move(static_cast<base1&>(rhs)))
    , base2(std::move(static_cast<base2&>(rhs)))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{}

or even just:

X::X(X&& rhs)
    : base1(static_cast<base1&&>(rhs))
    , base2(static_cast<base2&&>(rhs))
    , mbr1(std::move(rhs.mbr1))
    , mbr2(std::move(rhs.mbr2))
{}

which I believe should exactly replicate what the compiler would generate implicitly for X(X&&) = default; if there are no other base classes or members than base1/base2/mbr1/mbr2.

EDIT AGAIN: C++11 §12.8/15 describes the exact structure of the implicit member-wise copy/move constructors.

Sign up to request clarification or add additional context in comments.

1 Comment

Public service announcement: Casey's answer is equally correct as mine and he beat me by a few minutes. :-)
16

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.

17 Comments

Demos prove it works, demos do not prove it's safe. (presumably his code already worked, he's asking if it was safe)
@MooingDuck: In this example, static_cast<base1&&>(rhs) works just as well. Circumstances can arise where the use of forward<T> is safer than the use of static_cast<T&&>. Examples have been documented in open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html , use cases C and F. In these (admittedly rarer) use cases, the use of forward will fail with a compile time error, whereas the use of static_cast will fail with a run time error.
@0x499602D2: It is equivalent. I don't recommend it. std::move is typically used with deduced templated arguments. std::forward can never be used with deduced template arguments. Read std::move(x) as: cast x to rvalue. Read std::forward<T>(u) as: cast u to T&&, recalling that if T is an lvalue reference, reference collapsing will occur. Specifically, std::forward<base1>(rhs) says: forward rhs as an rvalue base1. And static_cast<base1&&>(rhs) also says that exact same thing.
I still don't understand why the language feature "cast x to rvalue" was named std::move.
@LightnessRacesinOrbit: I was trying to be polite. Now I will put it more bluntly: everyone, including yourself, had plenty of time to object, and failed to do so in a timely manner. Maybe 2002 was before your time, I don't know. But the design went through an extraordinarily long design review time. The naming of move was never picked up as controversial until it was too late to do anything about. This isn't about basic physics being wrong. This is simply a design detail that no one considered important enough, or objectionable enough to complain about for a decade.
|
2

No, that would be an example of possible undefined behavior. It might work a lot of the time, but it's not guaranteed to. C++ allows it, but you are the one who has to make sure you don't try to use rhs again in that way. You are trying to use rhs three times after its guts had been ripped out its rvalue reference was passed to a function that might potentially move from it (leaving it in an undefined state).

2 Comments

@MooingDuck Thank you for pointing out my mistake. I knew that at some point but I forgot. Is my answer more correct now?
Oh wait, I hadn't considered that base1 or base2 might be constructable from a X&&. In that case your answer is right, that it might possibly be undefined depending on the constructors of base1 and base2. I just hadn't made the connection that the base1 constructor might not implicitly convert X&& to a base1&&. Sorry for the unnecessary confusion.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.