1

TLDR - Feel free to mark this as a duplicate if it is so; I will delete the question. However, I couldn't find anything after looking around for a bit.

Consider the following classes:

class Base
{
public:
    Base(std::string var1);

    Base& operator=(Base&& other) noexcept
    {
        if (this != &other)
            var1_ = std::move(other.var1_);
        return *this;
    }

protected:
    std::string var1_;
};

class Child final : public Base
{
public:
    Child(std::string var1, std::string var2);

    Child& operator=(Child&& other) noexcept
    {
        if (this != &other)
        {
            // Take note of HERE for explanation below
            Base::operator=(std::move(other));
            var2_ = std::move(other.var2_);
        }
    }

private:
    std::string var2_;
};

Here there are two classes, Child deriving from Base. Child has more members than Base. In the move assignment operator of Child, we have a situation where we have extra members that also need to be assigned to the values of other's members. However, we're first moving the memory of other to the parent move assignment operator, so wouldn't that data then be unusable?

I suppose I'm simply confused on (1) if there's actually anything wrong with what I have above, (2) if so, why does it work since other is supposed to be moved before it is used, and (3) if not, what is the best way to accomplish the task I have?

Is the better way to do this simply to initialize the members of Child first and then hand it over to the Base move assignment operator? Does it make a difference?

Drake Johnson
  • 640
  • 3
  • 19
  • I think you're mentally confusing values with variables. You've moved the value of `var1_` from one object to the other, but the `var1_` variable still exists, and is still usable. It merely contains a "moved from" value, of some sort. What value it holds is irrelevant, because your `operator=` never reads from it after that. – Mooing Duck May 29 '20 at 23:05

1 Answers1

6

Yes, this is safe.

First, note that the rule about not using a moved-from object is a default guideline for designing and using objects. It is not a rule of the language itself. It's the common practice for what class objects generally guarantee after they've been moved-from, including Standard Library class types that don't specify otherwise (like std::unique_ptr<T> guarantees a moved-from pointer holds a null pointer).

So since you know exactly what Base::operator= does to other, and that only moves from other.var1_, it's still safe to use other.var2_.

There's another reason this pattern is safe, even if you don't actually know precisely what Base::operator= does, or you want to write the Child code to keep working even if the details of Base change. A Child object contains two subobjects: the Base base class subobject and the var2_ member subobject. These objects don't inherently know anything about the other. (They could have manually set-up interactions with each other via pointers, virtual functions, etc., to complicate things, but that sort of thing should be documented for Base or known in Child) The Base::operator=(std::move(other)); statement moves from just the base class subobject, not from the member subobject, so the member subobject is not affected, and the definition of Child::operator=(Child&&) is safe against changes to Base.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • I appreciate your thoroughness. I had a small question, though. You say "The `Base::operator=(other);` statement moves from just the base class subobject, not from the member subobject, so the member subobject is not affected ..." but my own code uses a move statement inside the function call (e.g. `Base::operator=(std::move(other))`). Would this affect anything you've already said? – Drake Johnson May 29 '20 at 21:57
  • @DrakeJohnson You might want to read the "Moving from lvalues" section of [this (rather long) answer](https://stackoverflow.com/a/11540204/9837301) to "What is move semantics?" In particular, the expression `std::move(other)` does not "move memory". – JaMiT May 29 '20 at 22:10
  • 1
    No, nothing changes. The `Base` move assignment operator calls the `std::string` move assignment operator on the members of the two objects, yes. Which makes sure `this->var1_` has the string which was in `other.var1_`, and does whatever unpredictable things it wants to `other.var1_`, and that's it. `other.var1_` is even still a valid `std::string` object in absolutely every sense - you just shouldn't count on what its contents happen to be. – aschepler May 29 '20 at 22:31