提问人:yeputons 提问时间:8/9/2023 最后编辑:yeputons 更新时间:8/9/2023 访问量:125
为什么将容器的元素分配给容器(不是)定义良好的 C++?
Why is assigning a container's element to the container (not) a well-defined C++?
问:
在 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";
}
我希望行和是相同的:程序是明确定义的打印。但是,我还没有找到启用行和地址清理器的编译器+标准库组合:GCC + stdlibc ++,Clang + libc ++和Visual Studio + Microsoft STL都崩溃了。(1)
(2)
2
(1)
奇怪的是,禁用 Address Sanitizer 会消除崩溃,程序开始打印。2
为什么在标准 C++ 中禁止或允许此操作?
额外的问题:相同,但有.额外的问题:使用代替 .f[0].children = f
std::any
std::vector<Foo>
答:
我不相信 (1) 是明确定义的,因为为了将新值复制到 中,必须首先销毁驻留在该位置的旧对象,或者至少在常量契约下进行修改。f[0]
来自 std::vector<T,Allocator>::operator=(强调我的):
如果赋值后的分配器与其旧值不相等,则使用旧分配器解除分配内存,然后使用新分配器在复制元素之前分配内存。否则,在可能的情况下,可以重复使用所拥有的内存。在任何情况下,原来属于的元素都可以被销毁或替换为按元素复制分配。
*this
*this
*this
因此,在上述所有情况下,对象可能在被复制之前就被销毁,并且您属于未定义或特定于实现的行为领域。
实际上,为了让向量重用此内存,它通常需要放置删除,然后是放置新建,在这些情况下,被复制的引用对象再次在此过程中被销毁。
即使在最宽松的情况下(即“替换为元素复制赋值”),您也要从 invoked on 开始将其替换为 的副本。向量是空的,因此复制将导致两个元素都被销毁,但目标向量的容量(即 2)保持不变。甚至在进入下一个元素之前,最初被复制的元素已被修改,违反了其合同,所有赌注都已关闭。Foo::operator=(const Foo&)
f[0]
f[0].children[0]
f[0].children[0].children
f[0].children
const Foo&
我不认为有任何自动方法可以防止这种情况,除非使用某种自定义垃圾收集分配器。你只需要认识到自我参照问题并避免它。您通过引入副本解决了 (2) 中的问题,并且至少定义明确。可以通过先将数据移出容器来更进一步:
auto tmp = std::move(f[0].children);
f = std::move(tmp);
也许这个问题可以通过仔细应用来解决,因为您的主要问题是您期望仍被引用的数据的破坏。std::shared_ptr
我认为整个 const-object 的契约破坏内容确实是回答您的“额外”问题的关键,而无需太深入的细节。在这种情况下,由于所需的容量增加,可以重新分配,并在这样做时修改本应是常量的。f[0].children = f
children
f
评论
const
const
评论
std::vector<Foo>*
f
vector<Foo>
*this
push_back
operator=
v = v[1];