"throw MyException()"和"throw (MyException())"有什么区别吗?

7
我想知道将异常写在inbox和outbox中是否会改变程序的行为,例如throw MyException();throw (MyException()); 我的代码:
#include <iostream>
#include <exception>
using namespace std;

class MyException: public exception {
public:
      virtual const char* what() const throw()
      {
         return "Something bad happened";
      }
};
class Test
{
public:
   void goWrong()
   {
      throw (MyException());
   }
};

int main()
{
   Test test;
   try
   {
      test.goWrong();
   }
   catch (MyException &err)
   {
      cout << "The Exception is Executed: " << err.what() << '\n';
   }
   cout << "Still Running" << '\n';
   return 0;
}

异常对象由运行时复制/移动(可能被省略),因此不再是const限定的。为了实现多态行为,应该通过引用来捕获异常,但只有在需要防止修改时才将其设置为const,而这并不必要。 - Deduplicator
@Bathsheba 好的,我认为 return 和 throw 是相同的,但是使用 throw 在调试时更容易发现这是一个抛出异常的操作,如果我没有错的话。 - Ali-Baba
我猜 () 可能会使它抛出一个类似于括号返回语句的引用。这将导致该对象被复制为一个引用,如果它指向的原始异常临时对象被销毁,则可能会出现问题。 - Anonymous1847
4
我认为对于第一个问题的赞数达到+5的新用户应该获得一枚勋章。 - Bathsheba
2个回答

6

异常对象是进行复制初始化的(except.throw/3),所以使用哪个并不重要;结果是相同的。

即使你从中得到一个引用限定符,复制初始化也会忽略它。

我们可以通过一些跟踪输出来证明这一点:

#include <cstdio>
using std::printf;

struct T
{
    T() { printf("T()\n"); }
    ~T() { printf("~T()\n"); }
    T(const T&) { printf("T(const T&)\n"); }
    T(T&&) { printf("T(T&&)\n"); }
    T& operator=(const T&) { printf("T& operator=(const T&)\n"); return *this; }
    T& operator=(const T&&) { printf("T& operator=(T&&)\n"); return *this; }
};

int main()
{
    try
    {
        throw T();
    }
    catch (const T&) {}
}

即使你从 throw T() 改为 throw (T()) ,语义(和输出)仍然完全相同:
T()
T(T&&)
~T()
~T()

即,先构造一个临时的T()对象,然后将其移动到真正的异常对象中(该对象存在于某个神奇的“安全空间”中),最终两者都被析构。

请注意,要看到此证明,您必须回到C++14(因为C++17在所谓的“强制省略”(mandatory elision)中直到需要真正的异常对象时才生成临时对象),并关闭pre-C++17可选省略(例如,在GCC中使用-fno-elide-constructors)。

如果像一些人声称的那样,“抛出引用”(变成悬空引用)是可能存在的,那么您只会看到一个 T 的构造。 实际上,尽管 decltype auto 尽力让您相信存在引用类型的表达式,但不存在这样的东西。

在这两种情况下,我们给throw的表达式是一个rvalue的MyException

当使用推导返回类型(通过decltype(auto))时,我们必须小心return,原因是推导考虑值类别。如果您有一个局部变量int x,则在return x中,表达式xxvalueint,因此您推导的类型将为int,一切正常。如果您改为写return(x),则该表达式为lvalue int ,这会导致推导类型为int&,突然间出现问题。请注意,两个表达式都没有引用类型实际上不存在的东西)。但在您的throw场景中,所有这些都不相关,最重要的是您的参数首先是一个临时对象。


2
@Ali-Baba,是啊,但我不知道你为什么想把异常对象放在括号里;其他人都没这样做过。 - Asteroids With Wings
1
@Anonymous1847 这不是真的。表达式b是一个左值int。所有处理都会删除引用限定符。(这就是为什么如果您想传播右值引用,就必须再次编写std::move)。在那里,ca绑定就像b一样;它是完全相同的机制。a也没有被复制!“初始化MyException&”不存在此类事情,因为引用是一种幻觉,实际上并不存在。在每种情况下,传递给throw的表达式的类型都是右值MyException。就这些。 - Asteroids With Wings
@Anonymous1847:我并没有说这种语言没有引用;我说它们是一种幻觉。我们正在迅速偏离主题,但这是关于C++中表达式如何工作的问题。没有引用类型的表达式存在。这样的东西不存在。引用类型将影响结果表达式是左值还是右值,但在这种情况下,它已经将是一个右值。因此,这两个东西是完全等价的,没有什么可担心的。 - Asteroids With Wings
@AsteroidsWithWings 引用并不是幻觉,因为 int& 存储为指针,而不是 int,它像指针一样被复制,而不是复制一个 int。它们有功能上的区别。第二个评论:但通常情况下,你不会抛出一个引用(在这种情况下,编译器确定你想要你的对象语义成为实例语义,而不是引用语义)。你抛出的是一个类实例,因为它是被抛出的对象。当你添加 () 时,表达式的 decltype 变成了引用类型,编译器使用引用语义。 - Anonymous1847
@Anonymous1847 没有人谈论存储。这是关于表达式的问题。在每种情况下,赋给throw作为“参数”的表达式都是一个rvalue MyException。就是这样。故事结束!没有所谓的“抛出引用”。不要让decltypeauto的规则愚弄你-它们不会告诉你表达式的真实类型,而是将其_调整_以反映其值类别。我不知道还有什么其他的解释方式,抱歉。干杯。 - Asteroids With Wings
显示剩余12条评论

-5

我认为它会像C++14及更高版本中的return语句一样工作,即括号表示要抛出的类型是引用,而不是异常实例本身。引用将被存储、复制和处理,就像异常一样,但引用指向的临时对象可能会在堆栈展开时被销毁,这可能会引起问题。如果您的异常对象是POD类型,则可能不会立即出现问题,因为在堆栈上创建这样的对象的销毁是微不足道的,但仍然是未定义行为。


你确定吗?异常对象通常存在一个特殊的保留位置。我认为在得出结论之前需要进行更多的分析。 - Asteroids With Wings
@AsteroidsWithWings 啊,但实际的异常对象是一个引用,实质上是一个指针。它所指向的对象是一个临时对象。 - Anonymous1847
“实际的异常对象是一个引用,实质上是一个指针。”不是这样的。 “它所指向的对象是一个临时对象。”也不是这样的。 - Asteroids With Wings
被抛出的类型是 MyException&。那就是异常。MyException& 会被复制,但 MyException 不会。 - Anonymous1847
3
那是不正确的。你根本误解了C++中表达式的工作原理。 - Asteroids With Wings

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接