【问题标题】:Can I throw a unique_ptr?我可以抛出一个 unique_ptr 吗?
【发布时间】:2014-03-10 01:01:35
【问题描述】:

我已经开始使用 C++ 11,特别是大量使用 unique_ptr 来使代码异常安全和所有权更具可读性。在我想抛出一个 unique_ptr 之前,这通常效果很好。我有创建复杂状态的错误代码(抛出很多地方,被捕获在一个地方)。由于动态分配内存的所有权在逻辑上正在从 thrower 转移到 catcher,因此 unique_ptr 似乎是表明这一点的合适类型,并清楚地表明 catcher 已获取堆对象。至少在免费的 Visual Studio 2013 中不起作用。这是一个简化的代码示例,它不再类似于任何有用的东西,但会引发行为:

// cl /nologo /EHsc /W4 test1.cpp
#include <memory>
using std::unique_ptr;
class IError
    {
public:
    virtual ~IError() {};
    virtual void DoStuff();
    };

unique_ptr<IError> Error();

int Foo() { throw Error(); }

int main(void)
    {
    try {
        Foo();
        }
    catch(unique_ptr<IError> Report)
        {
        Report->DoStuff();
        }
    return 0;
    }

编译器就这样喷了:

test1.cpp
test1.cpp(13) : warning C4673: throwing 'std::unique_ptr<IError,std::default_delete<_Ty>>' the following types will n
ot be considered at the catch site
        with
        [
            _Ty=IError
        ]
test1.cpp(13) : warning C4670: '_Unique_ptr_base<class IError,struct std::default_delete<class IError>,1>' : this bas
e class is inaccessible
test1.cpp(13) : error C2280: 'std::unique_ptr<IError,std::default_delete<_Ty>>::unique_ptr(const std::unique_ptr<_Ty,
std::default_delete<_Ty>> &)' : attempting to reference a deleted function
        with
        [
            _Ty=IError
        ]
        C:\bin\Visual Studio Express 2013\VC\INCLUDE\memory(1486) : see declaration of 'std::unique_ptr<IError,std::d
efault_delete<_Ty>>::unique_ptr'
        with
        [
            _Ty=IError
        ]

我哪里做错了?

【问题讨论】:

  • 例外是符合 RAII 的,为什么你有恐惧?正如Syl 所说,按值抛出,按引用捕获。

标签: c++ exception-handling stl unique-ptr


【解决方案1】:

作为代理,我将使用Rextester,它的 MSVC 版本为 18.00.21005.1。对于 GCC 4.8.1 和 Clang 3.5,我将使用 Coliru。现在,最初在给出一个仓促的答案时,我说unique_ptrs 不能被复制,所以你应该通过引用来捕捉它们。但是,当您 throw MSVC 中的对象时,似乎会发生错误。所以上面的建议只适用于 GCC 和 Clang。

catch(unique_ptr<IError>& Report)

似乎它们在 MSVC 处理复制/移动省略和/或移动语义的方式上有所不同,我在 C++ 方面还不够好,无法更具体,但让我们展示一些可编译的示例。首先是一个带有已删除复制构造函数的基本结构:

#include <iostream>
struct D {
    D() {};
    D(const D& other) = delete;
    D(D&& other) { std::cout << "call D move constructor... \n"; }
};

int main()
{
    try {
        throw D();
    } catch(D const& d)
    {   
    }
}

不管优化级别如何,对于 GCC 和 Clang,除非您在调用中添加 -fno-elide-constructors 并且我们看到它们都调用了移动构造函数,否则没有输出。对于 MSVC,我们收到此错误:

source_file.cpp(22) : error C2280: 'D::D(const D &)' : attempting to reference a deleted function
        source_file.cpp(7) : see declaration of 'D::D'

有关更复杂的示例,请参阅Throwing movable objects。问题是两年前的问题,但我们观察到相同的behavior,因为 GCC 和 Clang 在某些情况下都调用移动构造函数,但 MSVC 在所有情况下调用复制构造函数(GCC 和 Clang 在Throw with object not about to die anyhow (enter non-zero integer 部分不同。)

Throw directly: 
C
caught: 007FFA7C
~
Throw with object about to die anyhow
C
c
~
caught: 007FFA74
~
Throw with object not about to die anyhow (enter non-zero integer)
C
c
caught: 007FFA70
~
1
~

TL;DR GCC 和 Clang 会编译它,但 MSVC 不会。一个糟糕的解决方法是抛出一个指针:

throw new unique_ptr<IError>;

catch(unique_ptr<IError>* Report);

【讨论】:

  • 请注意,错误是在抛出,而不是捕获。除非我输入错误,否则您的建议不会对编译器错误产生任何影响。
  • @Ron 是的,我正在 Visual Studio 中对其进行测试,它仍然会出错,但在 gcc 和 clang 中不会。不过,我不会删除我的答案。
  • @RonBurk,异常对象在抛出时被复制。可能有编译器特定的优化会忽略副本,这可以解释为什么它在 gcc 和 clang 中有效。
  • @Ron 也许这是相关的:Throwing movable objects。尝试自己测试。删除 Blob 的复制构造函数。 gcc 和 clang 都调用了移动构造函数,Visual Studio 会出错,因为它调用了复制构造函数。请注意,我使用的是 Rextester,其版本为 18.00.21005.1,因此可能是 VS2013。
  • @remyabel “重载解决需要两次通过”(来自 Howard Hinnant 接受的对链接问题的回答)。我想原因是 VS 因缺乏两次重载解决方案而臭名昭著。这似乎是另一个例子。
【解决方案2】:

常见的例外规则是“按值抛出,按常量引用捕获”。你不能复制一个 unique_ptr,我猜这是问题的一部分。

从 std::exception 导出你的类错误,然后抛出它。

【讨论】:

    【解决方案3】:

    您的错误是由于 std::unique_ptr (故意)没有编译器在捕获异常时尝试调用的复制构造函数引起的。

    注意,在您的 catch 子句中,任何编译器都不得尝试移动 std::unique 对象(不能从异常对象移动)。

    一方面,

    您可以通过引用捕获您的异常 - 在 catch 子句中可以避免复制。所以特别是这个错误会消失。

    注意, 如果按值捕获,(自 C++11 起)如果复制省略不会因任何原因而不是跳过复制构造函数和 catch 的析构函数而改变程序的可观察行为,编译器可能会在 catch 子句中执行复制省略子句参数(例如,如果修改了 catch 子句参数,并且使用 throw 重新抛出异常对象)。 情况确实如此,但由于它是非强制性省略,因此您的编译器不执行它的事实并不违反语言标准。

    另一方面,

    用于抛出异常的类型必须有一个复制构造函数。 除了在投掷和接球时复制省略只允许但没有义务的事实之外,在某些情况下复制省略是不明智的。 即使您实际上避免在 catch 子句中复制与复制省略或在抛出异常时移动,但用于异常的类型(类)没有复制构造函数也是不正确的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-03-20
      • 2022-11-27
      • 2013-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-30
      相关资源
      最近更新 更多