在MSVC 2015中,std::async抛出异常的奇怪行为

Weird behavior with exceptions thrown from std::async, in MSVC 2015

本文关键字:抛出异常 async 2015 std MSVC      更新时间:2023-10-16

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

例如,这里有一个让我难住了。我从原始代码中创建了一个测试用例。

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

constructor    
destructor   
before exception   
after exception  
destructor   
has exception   
destructor   
destructor
代码:

using FutureList = std::vector<std::future<void>>;
struct Ex {
  Ex() {
    std::cout << "constructorn";
  }
  Ex(const Ex&) = delete;
  Ex(Ex&&) {
    std::cout << "move constructor";
  }
 ~Ex() {
    std::cout << "destructorn";
  }
  void operator=(const Ex&) {
    std::cout << "assignn";
  }
// 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 exceptionn";
        ex = std::current_exception();    // (D)
        std::cout << "after exceptionn";
    }
  }
  if (ex) {
    std::cout << "has exceptionn";
  }
}

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

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

问题在于crt/src/stl/excptptr.cpp__ExceptionPtr::_CallCopyCtor的实现。它本质上做了一个类似于"是否存在复制构造函数"的测试?没有?太好了,我们可以做memcpy了!"

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

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

Core issue 1863的提议决议旨在通过要求

[…考虑到被抛出的对象为左值,为复制初始化选择的构造函数必须是不可删除且可访问的。[…]

问题是在Ready状态,离采用只有一步之遥。

所以,虽然这绝对是MSVC的一个问题,但它也与标准中的一个开放问题有关。现在,为异常对象提供复制构造函数看起来是个好主意,即使标准还没有要求。


更新:问题1863的决议已被采纳到N4567工作草案中,因此如果没有合适的构造函数可用,则需要实现该更改的编译器拒绝代码。

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

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

尽管如此,MSVC 2015不应该调用复制构造函数,如果它被标记为删除。如果必须调用,则应该发出错误。