在 C++ 中,存在臭名昭著的自分配问题:在实现
operator=(const T &other)
时,必须小心 this == &other
的情况,以免在从 this
复制之前破坏 other
的数据。
然而,
*this
和other
可能会以比作为同一个对象更有趣的方式相互作用。即,一个可以包含另一个。考虑以下代码:
#include <iostream>
#include <string>
#include <utility>
#include <vector>
struct Foo {
std::string s = "hello world very long string";
std::vector<Foo> children;
};
int main() {
std::vector<Foo> f(4);
f[0].children.resize(2);
f = f[0].children; // (1)
// auto tmp = f[0].children; f = std::move(tmp); // (2)
std::cout << f.size() << "\n";
}
我希望
(1)
和 (2)
行是相同的:程序已明确定义可以打印 2
。然而,我还没有找到一个可以与 (1)
行配合使用并且启用了 Address Sanitizer 的编译器+标准库组合:GCC+stdlibc++、Clang+libc++ 和 Visual Studio+Microsoft STL 都崩溃了。
奇怪的是,禁用 Address Sanitizer 可以消除崩溃并且程序开始打印
2
。
为什么在标准 C++ 中禁止或允许此操作?
额外问题:相同,但带有
f[0].children = f
。额外问题:使用 std::any
代替 std::vector<Foo>
。
我不相信(1)是明确定义的,因为为了将新值复制到
f[0]
,驻留在该位置的旧对象必须首先被销毁,或者至少在const合同。
来自 std::vector
因此,在上述所有场景中,对象可能会在复制之前被销毁,因此您会陷入未定义或特定于实现的行为领域。如果分配后
的分配器与旧值不相等,则使用旧分配器来释放内存,然后在复制元素之前使用新分配器来分配内存。否则,在可能的情况下,*this
拥有的内存可以被重用。在任何情况下,最初属于*this
的元素都可以通过逐元素复制赋值被销毁或替换。*this
实际上,要使向量重用此内存,通常需要先执行
placement-delete,然后执行 placement-new,因此在这些情况下,正在复制的引用对象在此过程中被销毁也就不足为奇了。
即使在最宽松的场景中(即 “被逐元素复制赋值替换”),您也可以从在 Foo::operator=(const Foo&)
上调用
f[0]
开始,将其替换为
f[0].children[0]
的副本。向量
f[0].children[0].children
是空的,因此复制将导致
f[0].children
的两个元素都被销毁,但目标向量的容量(即 2)保持不变。在到达下一个元素之前,最初被复制的
const Foo&
实际上已经被修改了,打破了它的契约,所有的赌注都被取消了。我认为如果不使用某种自定义垃圾收集分配器,就没有任何自动方法可以防止这种情况。你只需要认识到自我参照问题并避免它。您通过引入副本解决了 (2) 中的问题,并且
that 至少是明确定义的。可以更进一步,先将数据移出容器:
auto tmp = std::move(f[0].children);
f = std::move(tmp);
也许可以通过仔细应用 std::shared_ptr
来更普遍地解决该问题,因为您的主要问题是您预期仍引用的数据被破坏。我认为整个破坏常量对象契约的东西确实是回答你关于
f[0].children = f
的“额外”问题的关键,而不需要太深入细节。在这种情况下,由于需要增加容量,
children
可能会被重新分配,并且这样做会修改原本应该是常量的
f
。