如何防止移动切片?

How to prevent move slicing?

当派生的 class 实例作为右值 parent 引用传递给毫无戒心的方法时,后者可以合法地更改父类的内容,导致与存储在实际对象中的任何额外数据。因此,为扩展而设计的 class 不能依赖默认的移动语义。考虑一个简单的例子:

#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!
}

为了调度资源移交到最派生class,我想到了在move构造函数中使用虚函数来获取moved-from资源:

... 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(); /**/
  }

这确实有效但有两个问题,

是否有更好/规范的解决方案?也许我遗漏了一些非常明显的东西。

您几乎不想复制移动多态对象。它们通常位于堆上,并通过(智能)指针访问。对于复制,使用 virtual clone 惯用语;而且几乎没有理由移动它们。因此,如果你的 class 有一个虚拟析构函数,那么大 5 的其他四个成员应该是 deleted(或者被保护,如果你需要它们来实现你的虚拟 clone)。

但是在(主要是假设的)情况下,当你确实需要移动一个多态对象,而你只有一个基指针或引用时,你需要意识到移动源也是对象的一部分 public 界面。所以它需要让整个对象保持一致的状态,而不仅仅是基础部分。所以你需要确保派生部分知道。不惜一切代价。通常你会想写一个专用的移出虚函数,并在你的移动中调用它 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      
}; 

现在任何派生都可以对从脚下拉出的基础部分做出正确反应。