为什么移动构造函数和移动标准库的赋值运算符会使对象从未指定的状态移开?

问题描述 投票:7回答:2

在C ++标准库中有一个移动构造函数和移动赋值运算符的特殊描述,它表示数据移出的对象在调用后保持有效但未指定的状态。为什么?我坦白地说不明白。这是我直觉上没有想到的。真的,如果我在现实世界中将某个地方从一个地方移动到另一个地方,我移动的地方是空的(是的,有效的),直到我搬到那里一些新东西。为什么在C ++世界中应该有所不同?

例如,根据实现,以下代码:

std::vector<int> a {1, 2, 3};
std::vector<int> b {4, 5, 6};
a = std::move(b);

可能等同于下一个代码:

std::vector<int> a {1, 2, 3};
std::vector<int> b {4, 5, 6};
a.swap(b);

这真的是我没想到的。如果我将数据从一个向量移动到另一个向量,我期望向量移动数据为空(零大小)。

据我所知,标准C ++库的GCC实现在移动后将向量保持为空状态。为什么不将此行为作为标准的一部分?

将对象保留为未指定状态的原因是什么。如果是为了优化,那也有点奇怪。我可以对未指定状态的对象做的唯一合理的事情是清除它(好吧,我可以得到向量的大小,我可以打印它的内容,但由于内容未指定,我不需要它)。因此,对象将以任何方式由我手动或通过调用赋值运算符或析构函数清除。我更喜欢自己清除它,因为我希望它能被清除。但这是对clear的双重调用。优化在哪里?

c++ move move-semantics c++-standard-library
2个回答
8
投票

在C ++标准库中有一个移动构造函数和移动赋值运算符的特殊描述,它表示数据移出的对象在调用后保持有效但未指定的状态。为什么?我坦白地说不明白。这是我直觉上没有想到的。真的,如果我在现实世界中将某个地方从一个地方移动到另一个地方,我移动的地方是空的(是的,有效的),直到我搬到那里一些新东西。为什么在C ++世界中应该有所不同?

事实并非如此。

但是你没有考虑到“移动”并不总是一个举动。例如,从std::array移动数据时会发生什么?不多。由于数组就地存储了数据,因此没有交换指针,移动就成了副本。因此,虽然图书馆可能会破坏原始数据,但这样做并没有任何意义,所以标准不会比说“我们不保证你得到的”更进一步。

一个真实的例子是std::string,它当前不将其内容存储在动态分配的内存块中,而是存储在一个小的自动分配的内存块中(这通常称为小字符串优化)。像数组一样,没有办法真正“移动”这些信息;它必须被复制。该字符串可能会在之后将其归零,并且可以将其length计数器减少为零,但为什么要强制运行时对其用户造成损失?

因此,可以根据具体情况对移动后的容器的状态做出更有力的保证,但只能通过人为限制实施(并减少优化机会),坦率地说没有充分的理由。

真实世界的类比可以作为一个思想实验很有趣,但使用它们来实际理解编程语言的行为是愚蠢的。


3
投票

将对象保留为未指定状态的原因是什么。

任何一个类都可以有一个合理的不同状态。 “未指明”意味着“在时间上确定”。此状态可以只是旧值(因此编译器只能执行廉价交换),如果这没有副作用,但在vectorsshared_ptr's的情况下,此状态必须为空(请参阅移动构造函数的定义)。

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c64-a-move-operation-should-move-and-leave-its-source-in-a-valid-state

当您在案例中应用它时,会出现内存损坏。这将在下面解释。

OP在以下链接的评论中报告了他的代码:“错误的例子:coliru.stacked-crooked.com/a/8698a44f63084d68,固定示例:coliru.stacked-crooked.com/a/b6e680c8f24b8123,在gcc上运行良好的UB版本:coliru.stacked-crooked.com/a/44f9ab54257e25ec

你面临的真正问题是你必须永远不要混合shared_ptr和裸ptr。事实上你是在宣布

auto p_processor = std::make_shared<BackGroundProcessor>();

然后复制存储在线程中的函数对象中的共享指针的一个引用:

Event ev_done;
p_processor->Run([p_processor, &ev_done]() { ev_done.Set(); });

而不是通过捕获this启动线程 - 也就是说,您通过指针使用它:

void Run(std::function<void()> on_done)
{
    m_on_done.swap(on_done);
    std::thread([this]()
    {
        // Doing some processing
        ...
        m_on_done = nullptr;

由于线程将比main()花费更长的时间,因此当您在shared_ptr中重置main()时,其引用计数变为“1”。比在线程中,只要m_on_done被重置,线程中执行的对象(就是这个本身)就会在线程终止之前被删除。我相信这是您遇到的所有不可重复行为的起源。

一个common approach面对这是使用shared_from_this()声明:

class BackGroundProcessor : public std::enable_shared_from_this<BackGroundProcessor>
{
   ...

(在这里找到完整的修复http://coliru.stacked-crooked.com/a/1f5c425696c29011

然后创建一个shared_ptr并将其复制到thread-lambda中 - 这样它将保持活动状态直到运行:

void Run(std::function<void()> on_done)
{
    auto self = shared_from_this();
    m_on_done.swap(on_done);
    std::thread([this,self]()
    {
        // Doing some processing
        ...

在捕获参数中指定它应该就足够了。

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