以下三个目标会导致冲突:
- 我了解到,
std::move
什么都不做,但是在之后std::move(someDObject)
对象someDObject
可能被卡尼巴尔化,您不应访问someDObject
- CleanCode-SingleResponsibiliPattern:一个类应该负责 仅适用于一个功能
- CodingStyle-DataEncapsulation:类不应该公开实现细节,甚至不应该公开派生类
假设一个(不可复制构造(类具有两个不同的功能。SRB的原因之一在Base
年实现,第二个在Derived : public Base
年实现。这两个类都将它们的数据保存在一个可移动的Conainer中,就像std::list<std::string>
一样,称为m_dataDerived
并相互m_dataBase
。数据封装的原因m_dataBase
应private:
。
这导致了如何为派生类嵌入 move-Constructor的问题。也:
Derived::Derived(Derived &&rhs)
: Base(std::move(rhs))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}
这在语法上违反了规则一,std::move(rhs)
之后不访问rhs
然而m_dataDerived
不能被Base
的构造函数可以化,因为Base
不知道它=>因此m_dataDerived
应该仍然有效。我不喜欢应该。
反之则会导致其他问题:
Derived::Derived(Derived &&rhs)
{
m_dataBase = std::move(rhs.m_dataBase);
m_dataDerived = std::move(rhs.m_dataDerived);
}
为此,您需要将m_dataBase
视为破坏DataEncapsulationprotected:
的原因。 此外,必须在所有派生的移动构造器中完成Base
的每一次更改,这会导致维护问题。
缺少的是一些std::move(
只有Base
rhs)
的一部分.有没有办法这样做?
第一个选择的编译示例是在onlineGdb上(但是使用std::vector
而不是std::list
(。
此外,下面列出的代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
class Base;
class Base {
public:
Base() {};
Base(Base &&rhs) {
std::cout << "BaseMove_Construktor(" << rhs.m_list.size() << ") --> " ;
m_list = std::move(rhs.m_list);
std::cout << m_list.size() << std::endl;
}
Base(std::initializer_list<std::string> &&p_list) {
int i=0;
m_list.resize(p_list.size());
for(auto it = std::begin(p_list); it != std::end(p_list); ++it) {
m_list[i++] = *it;
}
};
friend std::ostream &::operator<<(std::ostream & oStream, Base const &rhs);
friend std::ostream &::operator<<(std::ostream & oStream, Base &&rhs);
int size() { return m_list.size(); }
private:
std::vector<std::string> m_list;
};
class Derived : public Base {
public:
Derived() {};
Derived(Derived &&rhs) : Base(std::move(rhs)) {
std::cout << "DerivedMove_Construktor(" << Base::size() << ',' << rhs.m_numbers.size() << ')' << std::endl;
m_numbers = std::move(rhs.m_numbers);
}
Derived(std::initializer_list<std::string> &&p_list, std::initializer_list<double> &&p_numbers)
: Base(std::move(p_list))
{
int i=0;
std::cout << "Derived-List_Construktor(" << Base::size() << ',' << p_numbers.size() << ')' << std::endl;
m_numbers.resize(p_numbers.size());
for(auto it = std::begin(p_numbers); it != std::end(p_numbers); ++it) {
m_numbers[i++] = *it;
}
};
friend std::ostream &::operator<<(std::ostream & oStream, Derived const &rhs);
private:
std::vector<double> m_numbers;
};
std::ostream &operator<<(std::ostream & oStream, Base const &rhs)
{
oStream << "{ ";
for(auto it = std::begin(rhs.m_list); it != std::end(rhs.m_list); ++it)
{
oStream << '"' << *it << "", ";
}
oStream << '}';
}
std::ostream &operator<<(std::ostream & oStream, Base &&rhs)
{
oStream << "{m: ";
for(auto it = std::begin(rhs.m_list); it != std::end(rhs.m_list); ++it)
{
oStream << '"' << *it << "", ";
}
oStream << '}';
}
std::ostream &operator<<(std::ostream & oStream, Derived const &rhs)
{
oStream << '{';
for(auto it = std::begin(rhs.m_numbers); it != std::end(rhs.m_numbers); ++it)
{
oStream << *it << ", ";
}
oStream << (Base const &)rhs << '}';
}
int main()
{
std::cout << "Hello World" << std::endl;
std::cout << Base({ "tafel", "kreide", "Schwamm" }) << std::endl;
Base base{ "tafel", "kreide", "Schwamm" };
std::cout << base << std::endl;
Derived derived({ "tafel", "kreide", "Schwamm" }, { 0.2342, 8.639 });
std::cout << derived << std::endl;
Derived derivedCopy(std::move(derived));
std::cout << "derived is empty now: " << derived << std::endl;
std::cout << "derivedCopy holds data: " << derivedCopy << std::endl;
return 0;
}
Derived::Derived(Derived &&rhs)
: Base(std::move(rhs))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}
此代码是正确的和惯用的。如果需要,可以采用阐明如何仅移动Base
部分的方式编写它:
Derived::Derived(Derived &&rhs)
: Base(std::move(static_cast<Base&&>(rhs)))
, m_dataDerived(std::move(rhs.m_dataDerived))
{}
我们只移动Derived
的Base
子对象。事实上,rhs
的Base
子对象在使用它调用Base
move 构造函数后处于有效但(按照惯例(未定义的状态,因此我们最好不要对此做出任何假设。但我们显然没有碰过m_dataDerived
,所以事后离开它很好。
不过,我建议不要编写上述代码(带有额外的static_cast
(。对于初学者来说,std::move
实际上变得毫无意义(但省略它会使代码的可读性更加降低(。在移动构造函数的上下文中,直接从std::move(rhs)
移动构造基础的意图和效果应该是完全清晰和惯用的。
你的第一条规则("我了解到,std::move
什么都不做,但std::move(someDObject)
后对象someDObject
可能会被卡尼巴尔化,你不应该访问someDObject
">(也是不准确的:
对象可以被蚕食,但只能通过可以蚕食的操作。因此,在调用
std::move
后访问对象并不一定是坏事(但大概有人把std::move
放在那里是有原因的,假设蚕食不会出错(。您可以访问移自对象。但是你不应该对它做任何对其状态做出任何假设的事情(除了它是否有效,这是最终销毁所必需的(。在标准库术语中,您只能对没有前提条件的对象使用操作。因此,您可以
reset
从中移动的std::unique_ptr
,可以在移自std::vector
上调用size()
等等。
这当然与移动施工不是很相关,但了解到底发生了什么是值得的。