投掷可移动物体

时间:2011-06-15 02:40:18

标签: c++ exception c++11

我注意到当抛出类型可移动时,MSVC和g ++如何处理临时异常对象的创建有轻微的差异。狩猎这些下来提出了额外的问题。

在进一步讨论之前,这是我的问题的核心:在没有复制/移动省略的情况下,谁做了标准说明应该如何创建临时异常对象?目前,我能做的最好的是以下引用,从15.1 / 3开始:

  

throw-expression初始化一个临时对象,称为异常对象,其类型是通过从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;
  }
}

这是在ideone上重新创建的。正如您所希望的那样,gcc通过ideone在第一种情况下创建Blob对象,并在后两种情况下移动。结果总结如下,指针值替换为标识符。

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}

MSVC2010中的代码相同,无论优化设置如何,结果都是相同的,除了两个动作都是副本。这是最初引起我注意的差异。

我认为第一次测试很好;它的经典复制品。

在第二次测试中,gcc的行为与我预期的方式相同。临时Blob被视为xvalue,异常对象是从它构造的移动。但我不确定编译器是否需要识别原始Blob即将到期;如果不是它,则MSVC在复制时正常运行。因此,我的原始问题是:标准是否规定了这里发生的事情,或者它只是实施定义的行为的一部分而不是异常处理?

第三个测试恰恰相反:MSVC的行为与我的直觉要求相似。 gcc选择从b移动,但b仍然有效,这可以通过在处理抛出的异常后继续使用它来证明。显然,在这个简单的例子中,移动或复制对b本身没有任何影响,但在考虑重载解析时,当然不允许编译器查看它。

显然,复制/移动省略的存在使得这个简单的测试难以概括,但更大的问题是,任何一个编译器可能还不适合[特别是在第三次测试的gcc情况下,以及一般的MSVC] 。

请注意,这完全是出于学术目的;我几乎从不抛出任何东西,除了一个临时的,两个编译器无论如何构建到位,我很确定这种行为是允许的。

2 个答案:

答案 0 :(得分:8)

移动行为符合案例2,但不符合案例3.参见12.8 [class.copy] / p31:

  

当符合某些标准时,a   允许实现省略   复制/移动班级的建设   对象,即使复制/移动   构造函数和/或析构函数   对象有副作用。 ...

     

...

     
      
  • 在throw-expression中,当操作数是非易失性的名称时   自动对象(除了   function或catch-clause参数)   其范围不超出   最里面的封闭的结束   try-block(如果有的话),   从操作数复制/移动操作   到异常对象(15.1)即可   通过构造自动化省略   对象直接进入异常   对象
  •   

上面没有定义何时可以隐式移动对象。但确实定义何时复制/移动省略合法。要获得隐式移动是合法的,您必须转到同一部分的第32段:

  

32当一个省略的标准   复制操作已满足或将被满足   除了源的事实   object是一个函数参数,和   指定要复制的对象   通过左值,超载分辨率......

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

  1. 首先假设左值是决定构造函数将被调用或省略的右值。

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

  3. 这具有从最佳到最差生成移动语义层次结构的效果:

    1. 如果您可以忽视施工,请执行此操作。
    2. 否则,如果您可以移动对象,请执行此操作。
    3. 否则,如果您可以复制该对象,请执行此操作。
    4. 否则会发出诊断信息。
    5. 请注意,这些与本地堆栈对象的普通返回基本相同。

答案 1 :(得分:3)

投掷是一种非常实现的行为。在C ++ 03中,异常被复制了一个实现定义的次数,放在一个依赖于实现的位置,在catch块中引用然后被破坏。在C ++ 0x中,我希望实现有权同时复制和移动它多次,或者根据需要移动它多次(即,你可以抛出不可复制的类)。 / p>

然而,当然不允许您访问在catch期间移动过的对象,因为那将是真的很糟糕。如果你这样做了,那就是编译错误。您应该打印对象的地址以确定。

你还应该记住的是,MSVC的实施是多年前存在的规则,而GCC的rvalues实施是更近期的。自MSVC实施以来,规则可能已经改变。但是,编译器将在尝试抛出不可复制的类时出错,向我建议编译器可以自由复制和移动异常对象。