如何防止移动切片?
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(); /**/
}
这确实有效但有两个问题,
- 由于 C++ "forgetting" r 值函数参数是
&&
,因此很快变得不必要地冗长(请参阅标记为 /**/
的行中对 std::move
的需求),
- 当需要对多个对象进行
yield
编辑时,无法轻易概括。
是否有更好/规范的解决方案?也许我遗漏了一些非常明显的东西。
您几乎不想复制或移动多态对象。它们通常位于堆上,并通过(智能)指针访问。对于复制,使用 virtual clone
惯用语;而且几乎没有理由移动它们。因此,如果你的 class 有一个虚拟析构函数,那么大 5 的其他四个成员应该是 delete
d(或者被保护,如果你需要它们来实现你的虚拟 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
};
现在任何派生都可以对从脚下拉出的基础部分做出正确反应。
当派生的 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(); /**/
}
这确实有效但有两个问题,
- 由于 C++ "forgetting" r 值函数参数是
&&
,因此很快变得不必要地冗长(请参阅标记为/**/
的行中对std::move
的需求), - 当需要对多个对象进行
yield
编辑时,无法轻易概括。
是否有更好/规范的解决方案?也许我遗漏了一些非常明显的东西。
您几乎不想复制或移动多态对象。它们通常位于堆上,并通过(智能)指针访问。对于复制,使用 virtual clone
惯用语;而且几乎没有理由移动它们。因此,如果你的 class 有一个虚拟析构函数,那么大 5 的其他四个成员应该是 delete
d(或者被保护,如果你需要它们来实现你的虚拟 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
};
现在任何派生都可以对从脚下拉出的基础部分做出正确反应。