问题描述
我编写了一个向量类来学习移动语义。 我使用移动构造函数来移动 T(注释行)。
我的问题是为什么不像在 C 中一样复制临时对象的所有字节并将临时对象的所有字节设置为零?
我知道会为临时对象调用析构函数,它可能需要一些初始化的成员才能正确析构。这就是为什么我不能改变对象的内部表示的原因。
但如果我 100% 确定我的 ~T() 没有这样的要求,这是一个很好的优化吗?
20 void push_back(T&& val)
21 {
22 check_cap();
23 //new (m_data + m_size) T(std::move(val));
24 for(int i = 0; i < sizeof(T); ++i)
25 {
26 reinterpret_cast<char*> (m_data + m_size)[i] = reinterpret_cast<char*> (&val)[i];
27 reinterpret_cast<char*> (&val)[i] = 0;
28 }
29 m_size++;
30 }
(如果与实际问题无关,请不要谈论演员表及其安全性)
(我知道这不是一个好方法,最好不要在实际项目中使用它。但我只关心从效率的角度来看它有多好。)
解决方法
为什么不直接复制临时对象的所有字节并将临时对象的所有字节设置为零,就像在 C 中一样?
这是一个很好的优化吗?
我认为不会。以下是使用 clang++
的手写版本与普通版本之间的 quick-bench 比较:
当使用 g++
时,结果是 a tie,所以也没有增益。
你的计划很糟糕。
编译器通常可以使用 the as-if rule 计算出 memcpy 和零 - 甚至只是跳过析构函数 - 是合法的。
如果这样的编译器遇到您手工制作的未定义行为,它要么被未定义行为混淆,要么因此无法进一步优化。
有时有充分的理由诉诸未定义的行为。但是他们首先证明您的解决方案可以让事情变得更好,然后用尽符合标准的解决方案。最后,他们坦率地讨论了真正的短期、中期和长期风险,只换取短期保证收益。
您的案例与此无关。
将 move-destroy 优化为 memcpy 是编译器已经对简单代码流中易于理解的类型所做的事情。手动操作是 99/100 没有意义和 90/100 倍有害。
如果您的类型不简单且代码流易于理解,那么您的优化可能也难以证明是安全的。而且,如果您简化类型和代码流,当您可以可靠地证明 memcpy 零是最佳的时,您的编译器可能也可以。
与 Godbolt 一起坐下来,体验优化后的编译器输出。很有教育意义。
,您的优化在少数情况下可能有效。当您的移动构造函数执行特定操作时,它可能会导致问题。
class MyCustomClass : public IObserver
{
Registry ®istry;
// ...
public:
MyCustomClass(MyCustomClass &&rhs)
: registry{rhs.registry}
{
registry.register(*this);
}
// ...
};
如果您对此类进行按位复制,则移动的实例不会在注册表中注册。
通过清零,引用被破坏,原始实例的析构函数很可能会在从注册表中注销时崩溃。