我注意到当抛出类型可移动时,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] 。
请注意,这完全是出于学术目的;我几乎从不抛出任何东西,除了一个临时的,两个编译器无论如何构建到位,我很确定这种行为是允许的。
答案 0 :(得分:8)
移动行为符合案例2,但不符合案例3.参见12.8 [class.copy] / p31:
当符合某些标准时,a 允许实现省略 复制/移动班级的建设 对象,即使复制/移动 构造函数和/或析构函数 对象有副作用。 ...
...
- 在throw-expression中,当操作数是非易失性的名称时 自动对象(除了 function或catch-clause参数) 其范围不超出 最里面的封闭的结束 try-block(如果有的话), 从操作数复制/移动操作 到异常对象(15.1)即可 通过构造自动化省略 对象直接进入异常 对象
上面没有定义何时可以隐式移动对象。但确实定义何时复制/移动省略合法。要获得隐式移动是合法的,您必须转到同一部分的第32段:
32当一个省略的标准 复制操作已满足或将被满足 除了源的事实 object是一个函数参数,和 指定要复制的对象 通过左值,超载分辨率......
本段解释了当复制/移动省略合法时,过载解决会发生两次:
首先假设左值是决定构造函数将被调用或省略的右值。
如果1)失败,则使用参数作为左值重复重载解析。
这具有从最佳到最差生成移动语义层次结构的效果:
请注意,这些与本地堆栈对象的普通返回基本相同。
答案 1 :(得分:3)
投掷是一种非常实现的行为。在C ++ 03中,异常被复制了一个实现定义的次数,放在一个依赖于实现的位置,在catch块中引用然后被破坏。在C ++ 0x中,我希望实现有权同时复制和移动它多次,或者根据需要移动它多次(即,你可以抛出不可复制的类)。 / p>
然而,当然不允许您访问在catch
期间移动过的对象,因为那将是真的很糟糕。如果你这样做了,那就是编译错误。您应该打印对象的地址以确定。
你还应该记住的是,MSVC的实施是多年前存在的规则,而GCC的rvalues实施是更近期的。自MSVC实施以来,规则可能已经改变。但是,编译器将在尝试抛出不可复制的类时出错,向我建议编译器可以自由复制和移动异常对象。