4

When a derived class instance is passed as a r-value parent reference to an unsuspecting method, the latter can legally change the parent's contents, causing incoherence with any extra data stored in the actual object. Therefore a class designed for extension can not rely on default move semantics. Consider for a trivial example:

#include <memory>
#include <utility>
#include <iostream>

struct Resource {
  int x;
  Resource(int x_) : x(x_*x_) { }
};

struct A {
  std::unique_ptr<Resource> ptr;

  A(int x) : ptr{std::make_unique<Resource>(x)} { }
  A(A&& other) = default; // i.e. : ptr(std::move(other.ptr)) { }
  virtual ~A() = default;
  // other elements of the rule of 5 left out for brevity

  virtual int value() {
    return ptr ? ptr->x : 0;
  }
};

struct B : A {
  int cached;

  B(int x) : A(x), cached(A::value()) { }

  int value() override {
    return cached;
  }
  int value_parent() {
    return A::value();
  }
};

int main() {
  B b{5};
  std::cout << "Before: b.value() = " << b.value()
        << " (parent: " << b.value_parent() << ")\n";
  A a = std::move(b);
  std::cout << "After: b.value() = " << b.value()
        << " (parent: " << b.value_parent() << ")\n"; // INCONSISTENT!
}

In order to dispatch the resource hand-over to the most derived class, I thought of using a virtual function to get the moved-from resource in the move constructor:

... A {
  A(A&& other) : ptr{std::move(other).yield()} { } /**/
  virtual std::unique_ptr<Resource>&& yield() && {
    return std::move(ptr);
  }

... B {
  virtual std::unique_ptr<Resource>&& yield() && override {
    cached = 0;
    return std::move(*this).A::yield(); /**/
  }

This does the trick but has two issues,

  • gets unnecessarily verbose rather quickly due to C++ "forgetting" that a r-value function parameter was a && (see the need for std::move in lines marked /**/),
  • can't be generalized easily when more than one object needs to be yield'ed.

Is there a better / a canonical solution? Maybe I'm missing something really obvious.

The Vee
  • 11,420
  • 5
  • 27
  • 60
  • Make the base non-public. – eerorika Feb 20 '19 at 16:38
  • 1
    Copying and polymorphism don't mix, and this is true wrt moving too. If your value your sanity, any class that has anything virtual should be uncopyable and unmovable. If you need to make copies, use the virtual clone idiom. – n. m. could be an AI Feb 20 '19 at 16:39
  • @n.m. Is it so? As long as one respect that `B` *is-an* `A` which happens to have some further capabilities, copying (passing the instance of `B` as a `const A&`) should pose no problem. It just ignores whatever `B` offers in addition to `A`, but does not affect the object. Or not? – The Vee Feb 20 '19 at 16:43
  • 2
    Passing the instance of `B` as a `const A&` is always OK, and it has nothing to do with copying. Copying involves copy constructors or copy assignment. These should be `delete`d. – n. m. could be an AI Feb 20 '19 at 16:46
  • 1
    Your premise is faulty, the public interface of a parent should _never_ cause any inconsistency in derived classes. Doing so violates a _is-a_ relation or whatever you want to call it. – Passer By Feb 20 '19 at 16:50
  • A moved from instance should be assumed to be in a valid, destroyable and (usually) assignable state. But anything beyond that is up to your type to enforce and document. It's not inconsistent for a moved-from instance to still contain values and data unless you, as the class' designer, decide that it is. It's up to the user to assume that they aren't there or aren't usable, unless you document otherwise. So you have to decide whether or not this is *actually* a problem. If it is, what problem does that decision fix? – François Andrieux Feb 20 '19 at 16:50
  • @FrançoisAndrieux @PasserBy: The moved-from **base** is in a valid and expected state, it's just that the "porcelain" added by `B` was not notified. As an author of both the classes I'm surely trying to foresee this and amend the `A` accordingly given it's a class designed for extension. `yield` seems to achieve this, having the whole of the object in a consistent moved-from state, but the question is whether that's indeed the simplest way. – The Vee Feb 20 '19 at 16:54
  • @n.m. I'm confused. How's, say, copy assignment (which is wrong) different from passing a constant reference (which is "always OK") to a member function that happens to be called the `operator =`? – The Vee Feb 20 '19 at 16:57
  • I don't understand what you are saying. Copy assignment is wrong. Have it deleted. You cannot pass anything to a member function named `operator =` because it's deleted. The question how it's different or similar to anything else becomes meaningless because it's now simply invalid. – n. m. could be an AI Feb 20 '19 at 17:06
  • @n.m. I see, that was simple. Thanks for specifying. @ everyone: Answer...? – The Vee Feb 20 '19 at 17:08
  • Moving-from, as a part of the public interface of your object, must leave the *entire* object in a consistent state, not just the base part. It's up to you, the class author, to have the derived part notified when the base part is moved-from. Mo ing is almost never what you *want* to do with polymorphic objects, so just delete the 4 out of 5. But for the (mostly imaginary) case when you do meed moving, you have to do what it takes to notify the derived parts. – n. m. could be an AI Feb 20 '19 at 17:09
  • Normally you would not read from a moved object. What is the motivation here? – super Feb 20 '19 at 17:19
  • @super I admit I don't have a particular motivation. It just occurred to me like this, like something that shouldn't be so hard to resolve cleanly. But move assignment leads to the same considerations and reading following that is conceivable. – The Vee Feb 20 '19 at 17:23
  • After you std::move from an object, that object is no longer valid -- any access other than just destroying it or assigning a new value to it with an assignment operator is undefined behavior. So it should be no suprise that after `A a = std::move(b);`, `b` is in an inconsistent state and can't be used for anything. – Chris Dodd Feb 20 '19 at 17:37
  • @ChrisDodd I am pretty sure that's *almost* true. Can't find the relevant source right now, but I am certain that you can do anything with the moved-from object as long as it does not require any preconditions (which would be perfect description for "*valid, but unspecified state*", as quoted from the standard) and there are standard library objects that provide more functionality than simply assigning-to that require no preconditions. – Fureeish Feb 20 '19 at 17:44
  • @ChrisDodd I think it's an [open suggestion](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3181.html#1374). I couldn't find the changes proposed in [here](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3241.html) adopted in the snapshots of the standard that I keep (not saying they are the most recent ones though). – The Vee Feb 20 '19 at 17:48
  • @ChrisDodd No it's not. You wrote the move constructor, you know perfectly well what happened to the object. There is nothing special about the parameters passed to a constructor. There is also nothing special about assignments, it is a function like any other. – Passer By Feb 20 '19 at 18:15
  • @PasserBy Well, one could be delegating some work to the `std::`'s move implementations. Like I leave the internals of what it means to move-assign a unique pointer in the above. So the UB (*if* it was UB) would not be a direct result of code I wrote myself only. – The Vee Feb 20 '19 at 18:23
  • @PasserBy: its undefined behavior in all libraries and classes defined by anyone else -- its the "exepected semantics" of std::move. Yes, you can overload your move constructor to do something unlike moving, but that's not generally a good idea. – Chris Dodd Feb 20 '19 at 18:55

1 Answers1

3

You almost never want to copy or move polymorphic objects. They generally live on the heap, and are accessed via (smart) pointers. For copying, use the virtual clone idiom; and there's almost never a reason to move them. So if your class has a virtual destructor, the other four members of the big 5 should be deleted (or made protected, if you need them to implement your virtual clone).

But in a (mostly hypothetical) situation when you do need to move a polymorphic object, and you only have a base pointer or reference, you need to realise that moving-from is also a part of the object's public interface. So it needs to leave the entire object in a consistent state, not just the base part. So you need to make sure the derived parts know. Do whatever it takes. Normally you would want to write a dedicated move-from virtual function, and call it in your move constructor/assignment:

class Base {      
  virtual void moved_fom() {} // do nothing for base
  // some stuff
  // members of the big 5
  virtual ~Base() = default; 
  Base (Base&& other) {
      // do the move
      other->moved_from();
  }
  // etc      
}; 

Now any derived can properly react to the base part being pulled from under its feet.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243