C++:如何捕获从构造函数抛出的异常?

3

我有一个类,叫做A,它的构造函数需要一些输入参数,并且如果这些参数不适合构造该对象,则可能会抛出异常。在我的main代码中,我按照以下方式构造了一个A类型的对象:

A my_obj(arg1,arg2,arg3);

然后使用它。显然,如果构造函数失败并抛出异常,程序的执行将在打印“未处理的异常”消息后终止。

但是,在这种情况下,我希望向用户提供更多信息,告诉他/她为什么抛出了exception。所以,我需要一种方法来catch这个异常。

为此,一种可能性是将从声明my_obj开始到程序结束的整个代码包含在try块中,并在之后catch异常:

try {
    A my_obj(arg1, arg2, arg3);
    // ... 
    // about 100 other lines of code being executed if my_obj is created properly 
}
catch (std::exception& e) {
    // print a user-friendly error message and exit
}

但是在我看来,这似乎有点“过度杀伤力”。特别是因为在剩下的100行中没有抛出任何其他异常。有没有其他更好的方法来完成这个任务呢?


1
尽管以下的“100行代码”可能会访问/引用my_obj,但如果在其构造过程中抛出异常,则不能这样做。外观漂亮是次要的 - 定义良好的行为是首要的。 - Scheff's Cat
1
那么,你的问题实际上是如何在没有try-catch块的情况下捕获异常? - DimChtz
@DimChtz,不,我知道如果没有try/catch块是无法捕获它的,但我只是想知道是否可能仅在这样的块中包含对象创建。例如,我可以在块外定义一个指针'A* p_obj',在try/catch块内初始化它(确保程序在异常情况下停止),然后再次在块外使用它。 - MikeL
请不要忘记 A my_obj 的作用域。(它在 catch 之前结束。您的指针方法仅在使用 new 构造 A 时有效。...(正如 @AndreKampling 的答案中所建议的那样)。 - Scheff's Cat
3
不,如果这能直接表达你的逻辑,那么这不会过度。如果你担心使用try/catch覆盖了100行代码,你可以将这些代码放在一个函数中或者考虑更进一步的分解。在单个函数中有100行代码和有100行try/catch几乎是同样让人怀疑的。 - Andriy Tylychko
显示剩余3条评论
5个回答

2

一种可能的方法是使用指针(最好使用智能指针,如下面代码中的 unique_ptr)。您将保留 unique_ptr,在try块中调用构造函数,并将指针移动到unique_ptr中。之后,您的其他代码将执行。当然,您需要使用 unique_ptroperator bool 做一个简单的if语句来检查有效指针。
为了简化对 my_obj 的使用,引用被提取出来:A& my_obj_ref = *my_obj;

std::unique_ptr<A> my_obj;
try {
    my_obj = std::move(std::unique_ptr<A>(new A(arg1, arg2, arg3));
}
catch (std::exception& e) {
    // print a user-friendly error message and exit
}

if (my_obj) { // needed if your exception handling doesn't break out of the function
    A& my_obj_ref = *my_obj;

    // ... 
    // about 100 other lines of code being executed if my_obj is created properly
}

记住,这种方式会将你的对象分配在堆上而不是栈上。

1
我曾考虑过这样的解决方案,但它会让代码的其余部分变得丑陋;'A'有一些像operator[]这样的运算符,我需要在代码的其余部分中使用。使用指针的话,代码会变成'(*my_obj)[index]'而不是'my_obj[index]'。 - MikeL
1
在 catch 块之后定义一个引用。 - M.M
由于他正在使用对象,因此最好在该类中使用operator bool方法,以便他可以在if条件中使用该对象来确定其有效性。 - Asesh
@AndreKampling 我会提到堆栈存储和堆存储之间的区别。(每当适用时,我更喜欢使用堆栈存储。) - Scheff's Cat
@Asesh,这通常被认为是不良风格,因为这意味着使用该对象的所有其余代码都必须具有丑陋的测试。 - M.M

2
如果构造函数抛出异常,你就不会得到一个对象。 std::optional<> 是一种类型,意思是“这里可能没有对象”。
template <typename T, typename ... Args>
std::optional<T> try_make(Args&& ... args)
{ try {
    return make_optional(std::forward(args...));
} catch (...) {
    return {};
} }

然后
auto my_obj = try_make<A>(arg1,arg2,arg3);
if (my_obj) {
    // about 100 other lines of code being executed if my_obj is created properly
}

std::forward(args...)编译不通过,你是不是想要std::forward(args)...?另外,make_optional(<...>)应该改为std::make_optional(T(<...>))或者只用T(<...>) - user4815162342

1
你可以将对象构造抽象成一个函数,该函数捕获异常:
template<typename... Args>
A make_a(Args&&... args) {
    try {
        return A(std::forward(args)...);
    }
    catch (std::exception& e) {
        // print a user-friendly error message and exit
        ...
        std::exit(EXIT_FAILURE);
    }
}

// ... in the actual code:
A my_obj = make_a(arg1, arg2, arg3);

上述代码利用了程序在构造失败时会退出的事实。如果要继续运行,该函数可以返回std::optional<A>(如果您无法使用C++17,则可以返回其boost等效项)。

2
如果他们不想退出程序怎么办? - M.M
不错的解决方案!谢谢! - MikeL
我认为你可以在函数中添加std :: function参数来处理异常,然后用户可以传递lambda表达式来处理异常发生时想要执行的任何操作。 - user2807083
@M.M 这个解决方案反映了问题陈述中所指定的情况,即在出现异常时退出程序。正如答案中所提到的,如果不是这种情况,那么我建议工厂函数返回 std::optional<A> 或等效物。 - user4815162342

0

这里有几个选择,取决于您希望在构建失败时如何继续控制。


如果您想通过抛出异常来退出函数,那么您不需要做任何事情,可以让A构造异常向上传播。

如果您想通过抛出不同的异常或在让A构造异常传播之前执行一些操作来退出,则使用一个工厂函数(可能是lambda)来执行这些操作,例如:

auto a_factory(T x, U y) -> A     // or use perfect forwarding
{
    try { return A(x, y);  }
    catch(...) {
         log("constructing A failed...");
         throw other_exception();
    } 
}

// ... 
     A my_obj = a_factory(x, y);        

如果您想通过返回一个值来退出,那么仍然可以使用上述方法,但是将调用函数包装在另一个函数中,该函数捕获预期的异常并返回一个值。
或者,您可以使用下面的`optional` 或 `unique_ptr` 技术(如其他答案所述),但要在 `catch` 块中执行`return` 语句。

如果你想在没有有效的A的情况下继续执行,则可以执行以下操作:

std::optional<A> opt_my_obj;
try
{
    A temp(...args...);
    opt_my_obj.swap(temp);
} catch(...)
{
    // handling, you could return from the function here
}

// At this point you can test `if ( opt_my_obj )` to branch the flow.
// When you're at a point where you have verified the object exists, you 
// can enable normal object syntax by writing:

A& my_obj = *opt_my_obj;

如果您的函数中有多个需要考虑的对象,我倾向于建议使用将整个函数包装在一个 try...catch 中的版本,以处理所有不同的异常情况。

0

我倾向于简单处理:抛出可读的人类信息。当没有选择时,这种策略效果很好,通常情况下也是如此。不过有一个问题,你希望异常处理足够健壮,因此我将消息封装在一个std::array<char,4096>中,必要时截断并记住零终止符(我知道这可能会导致堆栈溢出,但如果我们不在递归函数中,应该没问题),然后抛出它。

示例:

try
    {
    Options opts(argv);
    SomeResource resource(opts.someParameter());
    //...More actions that could throw
    }
catch(const std::array<char,4096>& errmessage) //Or rather some other type that contains the message.
    {
    fprintf(stderr,"Error: %s\n",errmessage.data());
    return -1; //Or any non-zero value
    }
return 0;

优点:

  • 由于只有一个异常类,因此快速实现新类的新构造函数,该类将适用于所有情况
  • 您将直接从源代码中获取任何系统消息

缺点:

  • 缺乏上下文:消息必须说出类似于“无法打开文件foo:没有这样的文件或目录。”而不告诉用户异常的根本原因。这个问题是从异常模型继承而来的,不能在不将异常视为光荣错误代码的情况下解决

  • 如果要根据异常内容进行分支处理,则必须解析消息,但我发现这很少需要。可能在编译器的上下文中,但那也会打印该消息foo:54:1: Error: bar is not a baz


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