【问题标题】:Correct way to write move constructor with unique_ptr member (crash)使用 unique_ptr 成员编写移动构造函数的正确方法(崩溃)
【发布时间】:2015-03-23 04:54:30
【问题描述】:

以下代码在Visual Studio 2013下会崩溃

我想知道为什么:在这种情况下编写移动构造函数的正确方法是什么? 删除移动构造函数解决了这个问题。 是VC++的bug还是这段代码错了?

移动构造函数的默认定义有什么不同,使这段代码不会崩溃,而我自己的定义会崩溃?

#include <memory>
#include <vector>

class A
{};

class Foo
{
public:
    Foo(std::unique_ptr<A> ref) : mRef(std::move(ref)) {}
    Foo(Foo&& other) : mRef(std::move(other.mRef)) {}

    Foo(const Foo& other) {}
    Foo& operator=(const Foo& other) { return *this; }

protected:
    std::unique_ptr<A> mRef;
};

int main(int argc, char *argv[])
{
    std::vector<Foo>({ Foo(std::make_unique<A>()), Foo(std::make_unique<A>()) });
    // Crash : Debug Assertion Failed !
    // Expression : _BLOCK_TYPE_IS_VALID(pHead->nBlockUse)
}

这可能与此有关,对吧?

Double delete in initializer_list vs 2013

这里是实际的代码,带有充实的复制构造函数和赋值,但错误是完全一样的

class A
{
public:
     std::unique_ptr<A> clone() { return std::make_unique<A>(*this); }
};

class Foo
{
public:
    Foo(std::unique_ptr<A> ref) : mRef(std::move(ref)) {}
    Foo(Foo&& other) : mRef(std::move(other.mRef)) {}

    Foo(const Foo& other) : mRef(other.mRef->clone()) {}
    Foo& operator=(const Foo& other) { mRef = other.mRef->clone(); return *this; }

protected:
    std::unique_ptr<A> mRef;
};

【问题讨论】:

  • MCVE?您需要包含&lt;vector&gt;
  • 适用于 clang++ 和 g++4.9
  • 我最好的猜测是这是一个在 VS-2015 中解决的错误,也许看到我在最后添加的链接?
  • 可能与您当前的问题无关,但无论如何:您的 copy 构造函数和赋值运算符实际上并未复制。

标签: c++ visual-c++ c++14 unique-ptr move-constructor


【解决方案1】:

这听起来像是 VS-2013 的错误。但看起来你的代码虽然格式良好,但可能也没有按照你的意愿行事(但只有你可以肯定地说)。

我在您的Foo 中添加了打印声明:

class Foo
{
public:
    Foo(std::unique_ptr<A> ref) : mRef(std::move(ref)) {}
    Foo(Foo&& other) : mRef(std::move(other.mRef)) {}

    Foo(const Foo& other) {}
    Foo& operator=(const Foo& other) { return *this; }

    friend std::ostream&
    operator<<(std::ostream& os, const Foo& f)
    {
        if (f.mRef)
            os << *f.mRef;
        else
            os << "nullptr";
        return os;
    }

protected:
    std::unique_ptr<A> mRef;
};

我还在您的main 中添加了一条打印声明:


除此之外:我还在您的 A 中添加了身份/状态,以便更容易看到正在发生的事情。


int main(int argc, char *argv[])
{
    std::vector<Foo> v({ Foo(std::make_unique<A>(1)), Foo(std::make_unique<A>(2)) });
    // Crash : Debug Assertion Failed !
    // Expression : _BLOCK_TYPE_IS_VALID(pHead->nBlockUse)
    for (const auto& p : v)
        std::cout << p << '\n';
}

对我来说这个输出:

nullptr
nullptr

我认为这是正确的输出。

一个不能initializer_list移动,因此vector构造函数调用Foo的复制构造函数,它只是默认构造unique_ptr

确实,如果删除了Foo 复制构造函数,该构造函数应该被隐式删除(或者你可以显式删除它),程序不应该编译。

要真正完成这项工作,您必须为Foo 提供一个可操作的复制构造函数。也许是这样的:

    Foo(const Foo& other)
        : mRef(other.mRef ? new A(*other.mRef) : nullptr)
    {}

总而言之,我认为编译器和当前代码都会因错误而获得奖励。虽然来自 cmets,但听起来当前的代码错误只是正确减少代码以隔离问题的工件。

VS-2013 错误。

至于你的移动构造函数,没关系。虽然如果使用= default 实现会更好。如果您这样做,它将自动继承 noexcept 规范。这样的规范不应掉以轻心。这对于有效使用vector&lt;Foo&gt; 是最重要的。

我的理解是 VS-2013 既不理解默认移动成员也不理解noexcept

我对 VS-2013 的轶事经验是,一个人遇到的编译器错误的数量与花括号的使用成正比。我希望 VS-2015 将与这种体验相矛盾。同时,我建议避免使用涉及{} 的构造表达式。


更新

您更新的副本成员在:

class Foo
{
public:
    Foo(std::unique_ptr<A> ref) : mRef(std::move(ref)) {}
    Foo(Foo&& other) : mRef(std::move(other.mRef)) {}

    Foo(const Foo& other) : mRef(other.mRef->clone()) {}
    Foo& operator=(const Foo& other) { mRef = other.mRef->clone(); return *this; }

protected:
    std::unique_ptr<A> mRef;
};

潜在 nullptr-dereference 错误。如果other 处于移出状态,则-&gt;clone() 将取消引用nullptr。从技术上讲,如果您非常非常小心,您可以摆脱这种情况。但是,您很容易不小心碰到这个错误。

【讨论】:

  • 更新的解决方案是检查 nullptr 还是更复杂?
  • @N0vember:对,只需检查nullptr,如果找到则分配nullptr,而不是调用clone()。我更喜欢为此使用条件表达式,如我的答案中的示例Foo 复制构造函数所示。但这只是一种风格选择。
猜你喜欢
  • 2013-03-27
  • 2012-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-25
  • 2017-09-24
  • 2014-11-08
相关资源
最近更新 更多