问题描述
|
我已经注意到,当抛出类型是可移动的时,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;
}
}
这些都是在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的行为符合我的预期。临时ѭ1被视为x值,并从其构造异常对象。但是我不确定是否需要编译器来识别原始的ѭ1即将到期;如果不是,则MSVC在复制时行为正确。因此,我的第一个问题是:标准要求此处发生了什么,还是仅仅是异常处理固有的实现定义的行为的一部分?
第三个测试恰恰相反:MSVC的行为符合我的直觉要求。 gcc选择了从ѭ5move移出,但是b
仍然存在,这一事实证明了我在处理抛出的异常后继续使用它。显然,在这个简单的示例中,移动或复制对ѭ5本身没有任何影响,但是在考虑重载分辨率时,肯定不允许编译器考虑该问题。
显然,复制/移动省略的存在使此简单测试难以一概而论,但更大的问题是,任一编译器可能尚不兼容(特别是在gcc的情况下,与第三种测试相对应,而MSVC通常) 。
注意这完全是出于学术目的;除了临时文件以外,我几乎从不抛出任何东西,无论哪种方式,两个编译器都可以在适当的位置进行构造,而且我很确定可以允许这种行为。
解决方法
情况2的移动行为符合情况,但情况3则不行。请参见12.8 [class.copy] / p31:
当满足某些条件时,
实施可以省略
复制/移动类的构造
对象,即使复制/移动
的构造函数和/或析构函数
对象有副作用。 ...
...
在throw表达式中,当操作数是非易失性的名称时
自动对象(除
功能或子句参数)
其范围不超出
最内层封闭的末端
尝试块(如果有),则
从操作数复制/移动操作
可以将异常对象(15.1)设置为
通过构造自动
对象直接进入异常
宾语
上面没有定义何时可以隐式移动对象。但是它确实定义了复制/移动省略何时合法。要获得隐式移动合法的时间,您必须转到同一部分的第32段:
32当标准为
复制操作已达到或将要达到
除了事实,
object是一个函数参数,并且
指定要复制的对象
通过左值,重载分辨率...
本段说明,当复制/移动省略是合法的时,则通过两次通过来进行重载解析:
首先,假设左值是一个右值,以确定将要调用或消除什么构造函数。
如果1)失败,则使用参数作为左值重复重载解析。
这具有产生从最佳到最坏的移动语义层次的效果:
如果您可以忽略构造,则可以这样做。
否则,如果可以将对象移出,则可以这样做。
否则,如果您可以复制对象,则可以这样做。
否则发出诊断。
请注意,这些对于普通返回本地堆栈对象本质上是相同的规则。
, 抛出是非常实现定义的行为。在C ++ 03中,该异常被复制了实现定义的时间,放置在与实现相关的位置,在catch块中被引用,然后被破坏。我希望在C ++ 0x中实现可以同时复制和移动它喜欢的次数,或者可以将它移动很多次(即,您可以抛出不可复制的类)。
但是,当然不允许您访问在
catch
期间已移动过的对象,因为那将是Really Bad™。如果这样做,则是编译器错误。您应该确定打印对象的地址。
您还应该记住,MSVC的实现是对许多年前存在的规则的,而GCC的右值实现是最近的。自MSVC实施以来,这些规则可能已更改。但是,编译器在尝试抛出不可复制的类时会出错,这提示我编译器可以自由复制和移动异常对象。