【问题标题】:Weird behavior with exceptions thrown from std::async, in MSVC 2015在 MSVC 2015 中从 std::async 抛出异常的奇怪行为
【发布时间】:2015-08-01 22:47:23
【问题描述】:

我刚刚从 Visual Studio 2013 升级到 2015,我遇到了一堆问题,这些问题过去在 2013 年可以工作,但在 2015 年就不行了。

例如,这是一个让我难过的问题。我用原始代码创建了一个测试用例。

基本上,代码通过 std::async() 在线程中运行一些操作。在线程内,可能会抛出异常 (A),该异常应放置在 std::async() 返回的未来对象中。奇怪的是,在(B)中,调用了 Ex 的析构函数,但对象仍然被抛出。在 try 块中,当 ex (D) 变量离开分数时,如果 'mInts' 向量 (X) 是成员,则程序将崩溃。如果我将“mInts”注释掉,如下所示,我仍然会出现奇怪的行为。例如,下面的代码是这样打印的:注意构造函数被调用了 1 次,而析构函数被调用了 4 次:

输出:

constructor    
destructor   
before exception   
after exception  
destructor   
has exception   
destructor   
destructor

代码:

using FutureList = std::vector<std::future<void>>;

struct Ex {
  Ex() {
    std::cout << "constructor\n";
  }

  Ex(const Ex&) = delete;
  Ex(Ex&&) {
    std::cout << "move constructor";
  }

 ~Ex() {
    std::cout << "destructor\n";
  }

  void operator=(const Ex&) {
    std::cout << "assign\n";
  }

// std::vector<int> mInts; (X)
};


TEST(Explore, Test1) {
  FutureList futures;

  futures.push_back(
    std::async(std::launch::async, []() {           
        throw Ex();     // (A)
    }));


  std::exception_ptr ex;
  for (auto& i : futures) {
    try {
        i.get(); // (B)
        std::cout << "Doesn't get here.\n";
    }
    catch (...) { // (C)
        std::cout << "before exception\n";
        ex = std::current_exception();    // (D)
        std::cout << "after exception\n";
    }
  }

  if (ex) {
    std::cout << "has exception\n";
  }
}

【问题讨论】:

  • 多么奇怪。我刚刚谈到另一个问题,关于 VC++ 6.0 错误,它两次调用异常对象的析构函数。无论如何,既然您已经允许 VC++ 对复制构造函数的错误调用,那么构造函数和析构函数调用之间是否存在相同的差异?
  • @Cheers 和 hth。 - Alf:现在没有差异。打印输出的问题是因为复制构造函数中没有打印。我添加了一些,构造函数/析构函数计数匹配。
  • 如果您能写下您的发现作为您问题的答案并接受它,那就太好了。如果您想知道,那完全可以。它可以帮助其他人,并为您提供一些急需的声望点。 ;-)
  • 请抽象出这个TEST宏并添加一个main函数,这样我们就可以自己重现您的问题。 stackoverflow.com/help/mcve
  • @bogdan - 你继续,报告它。

标签: c++ visual-studio-2015


【解决方案1】:

实际上,MSVC 在您的示例中没有调用复制构造函数。没有代码可以调用;该功能被删除。它做了更糟糕的事情:它将类视为可简单复制并在对象上执行memcpy。这就是当您拥有std::vector&lt;int&gt; 类型的成员时崩溃的原因。

该问题与std::async 没有直接关系。这是由std::exception_ptr 的实现引起的。 std::future 的共享状态使用exception_ptr 来存储异常,因此在异常上下文中使用future 会触发问题。仅使用 std::current_exception() 时可能会重现此问题,这涉及 VC 14 标准库实现中的副本(标准允许这样做)。

问题在于crt/src/stl/excptptr.cpp__ExceptionPtr::_CallCopyCtor 的实现。它基本上做了一个测试,就像“有一个复制构造函数吗?没有?太好了,我们可以做一个memcpy 然后!”。

另一个问题是测试忽略了访问说明符。如果您提供了一个复制构造函数,但将其设为private,它将被调用。

测试是在运行时完成的,所以不会出现编译时错误,不幸的是,这是唯一的方法:没有通用的编译时测试可以告诉 @ 是什么类型的异常对象987654334@ 在所有情况下都会指向。

Core issue 1863 的提议解决方案旨在通过要求避免此问题

[...] 将抛出的对象视为左值而选择用于复制初始化的构造函数应不可删除且可访问。 [...]

问题在于Ready 状态,距离采用仅一步之遥。

因此,虽然这绝对是 MSVC 的问题,但它也与标准中的一个未解决问题有关。目前,为异常对象提供一个复制构造函数看起来是个好主意,即使标准还不需要它。


更新:问题 1863 的解决方案已被纳入 N4567 工作草案,因此如果没有合适的构造函数可用,实现该更改的编译器必须拒绝代码。

【讨论】:

    【解决方案2】:

    似乎 MSVC 2015 仍然调用复制构造函数,即使它被标记为已删除。为了解决这个问题,我定义了复制构造函数。

    打印输出的问题是因为复制构造函数中没有打印。我添加了一些,构造函数/析构函数计数匹配。

    不过,如果将复制构造函数标记为已删除,则 MSVC 2015 不应调用它。如果必须调用它,那么它应该发出错误。

    【讨论】:

      猜你喜欢
      • 2010-09-16
      • 1970-01-01
      • 2020-11-06
      • 1970-01-01
      • 2017-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多