为什么将容器的元素分配给容器(而不是)定义良好的 C++?

问题描述 投票:0回答:1

在 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>

c++ language-lawyer assignment-operator copy-assignment memory-aliasing
1个回答
0
投票

我不相信(1)是明确定义的,因为为了将新值复制到

f[0]
,驻留在该位置的旧对象必须首先被销毁,或者至少在const合同。

来自 std::vector::operator=强调我的):

如果分配后

*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

© www.soinside.com 2019 - 2024. All rights reserved.