C++ 投掷可移动物体

C++ 投掷可移动物体,c++,exception,c++11,C++,Exception,C++11,我注意到,当抛出的类型是可移动的时,MSVC和g++在处理临时异常对象的创建时有一点不同。追查这些数据引发了更多的问题 在进一步讨论之前,我的问题的核心是:在没有复制/移动省略的情况下,标准中谁能很好地说明如何创建临时异常对象?目前,我能做的最好的事情就是引用15.1/3中的以下内容: throw表达式初始化临时对象,称为异常对象,其类型是通过从throw操作数的静态类型中删除任何顶级cv限定符,并将类型从“T数组”或“函数返回T”调整为“指向T的指针”或“指向函数返回T的指针”来确定的,分别

我注意到,当抛出的类型是可移动的时,MSVC和g++在处理临时异常对象的创建时有一点不同。追查这些数据引发了更多的问题

在进一步讨论之前,我的问题的核心是:在没有复制/移动省略的情况下,标准中谁能很好地说明如何创建临时异常对象?目前,我能做的最好的事情就是引用15.1/3中的以下内容:

throw表达式初始化临时对象,称为异常对象,其类型是通过从throw操作数的静态类型中删除任何顶级cv限定符,并将类型从“T数组”或“函数返回T”调整为“指向T的指针”或“指向函数返回T的指针”来确定的,分别

我猜答案隐藏在定义表达式类型和对象初始化方式的其他语言中,但我没有运气将其拼凑在一起。当抛出一个对象时,异常对象是否得到(a)复制构造,(b)移动构造(如果合适),复制构造(否则),或者(c)以实现定义的方式初始化

考虑以下代码:

#include <iostream>
using std::cout;
using std::cin;
using std::endl;

struct Blob {
  Blob() { cout << "C" << endl; }
  Blob(const Blob&) { cout << "c" << endl; }
  Blob(Blob&&) { cout << "m" << endl; }
  Blob& operator =(const Blob&) { cout << "=" << endl; return *this; }
  Blob& operator =(Blob&&) { cout << "=m" << endl; return *this; }
  ~Blob() { cout << "~" << endl; }

  int i;
};

int main() {
  try {
     cout << "Throw directly: " << endl;
     throw Blob();
  } catch(const Blob& e) { cout << "caught: " << &e << endl; }
  try {
     cout << "Throw with object about to die anyhow" << endl;
     Blob b;
     throw b;
  } catch(const Blob& e) { cout << "caught: " << &e << endl;  }
  {
    cout << "Throw with object not about to die anyhow (enter non-zero integer)" << endl;
    Blob b;
    int tmp;
    cin >> tmp; //Just trying to keep optimizers from removing dead code
    try {
      if(tmp) throw b;
      cout << "Test is worthless if you enter '0' silly" << endl;
    } catch(const Blob& e) { cout << "caught: " << &e << endl;  }
    b.i = tmp;
    cout << b.i << endl;
  }
}
MSVC2010中的相同代码,无论优化设置如何,结果都是相同的,只是两个移动是副本。这就是最初引起我注意的差异

我假设的第一个测试很好;这是经典的复制省略

在第二个测试中,gcc的行为与我预期的一样。临时的
Blob
被视为一个xvalue,并从中构造异常对象move。但是我不确定编译器是否需要识别原始的
Blob
即将过期;如果不是,那么MSVC在复制时的行为是正确的。因此,我的原始问题是:标准是否要求这里发生什么,或者它只是实现定义的行为的一部分,而不是异常处理的固有部分

第三个测试正好相反:MSVC的行为符合我的直觉要求。gcc选择从
b
移动,但是
b
仍然是活动的,我在处理抛出的异常后继续使用它就证明了这一点。显然,在这个简单的示例中,移动或复制对
b
本身没有影响,但在考虑重载解析时,编译器肯定不允许查看这一点

显然,复制/移动省略的存在使得这个简单的测试很难从中概括出来,但更大的问题是,两个编译器都可能还不是complient[特别是在gcc的第三个测试中,以及一般的MSVC中]


注:这完全是出于学术目的;我几乎从不抛出任何东西,除了一个临时的,不管怎样,这两个编译器都在适当的地方构造了它,而且我非常确定这种行为是允许的

抛出是一种由实现定义的行为。在C++03中,异常被复制了实现定义的次数,放置在依赖于实现的位置,在catch块期间被引用,然后被销毁。在C++0x中,我希望实现要么有权复制和移动它任意多次,要么有权移动它任意多次(即,可以抛出不可复制的类)

但是,您当然不允许访问在捕获过程中从中移动的对象,因为这将非常糟糕™. 如果您已经这样做了,那么这是一个编译器错误。您应该打印对象的地址以确定


您还应该记住的是,MSVC的实现是根据多年前存在的规则进行的,而GCC的rvalues实现则要晚得多。自MSVC实施这些规则以来,这些规则可能已经发生了变化。但是,编译器在尝试抛出不可复制的类时会出错,向我建议编译器可以自由复制和移动异常对象。

移动行为符合情况2,但不符合情况3。见第12.8节[类别副本]/p31:

当满足某些标准时,一个 允许实现省略 复制/移动类的构造 对象,即使复制/移动 函数的构造函数和/或析构函数 对象有副作用

  • 在抛出表达式中,当操作数是非易失性 自动对象(非自动对象) 函数或catch子句参数) 其范围不超出 最内层封闭结构的末端 try block(如果有),则 从操作数复制/移动操作 异常对象(15.1)的 通过构造自动 对象直接导入异常 反对
上面没有定义何时可以隐式移动对象。但它确实定义了何时复制/移动省略是合法的。若要了解隐式移动是否合法,您必须转到同一节中的第32段:

32当删除一个 满足或将满足复制操作 除了源代码 对象是函数参数,并且 要复制的对象已指定 通过左值,重载解析

本段解释了当复制/移动省略合法时,过载解析会发生两次:

  • 首先假设左值是右值,以决定调用或省略哪个构造函数

  • 如果1)失败,则使用参数作为左值重复重载解析

  • 这会产生从最佳到最差的移动语义层次结构:

  • 如果你可以省略构造,那么就这样做
  • 否则,如果可以将对象移出,请执行此操作
  • 否则,如果可以复制对象,请执行此操作
  • 否则会发出诊断信号
  • 请注意,对于本地堆栈对象的普通返回,这些规则基本相同。

    K
    Throw directly: 
    C {A}
    caught: {A}
    ~ {A}
    Throw with object about to die anyhow
    C {A}
    m {B} <- {A}
    ~ {A}
    caught: {B}
    ~ {B}
    Throw with object not about to die anyhow (enter non-zero integer)
    C {A}
    m {B} <- {A}
    caught: {B}
    ~ {B}
    2
    ~ {A}