如何解决这个典型的异常不安全代码? [英] How to fix this typical exception unsafe code?

查看:174
本文介绍了如何解决这个典型的异常不安全代码?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据 GOTW#56 ,存在潜在的经典内存泄漏和异常安全

  //在某些头文件中:
void f(T1 *,T2 *) ;
//在一些实现文件中:
f(new T1,new T2);

原因是当我们 new T1 ,或 new T2 ,那么类的构造函数可能会抛出异常。



同时,根据解释:


简介: new T1被调用,简单来说,一个
new-expression。回想一下new-expression真的做了什么(为了简单起见,我会忽略
放置和数组形式,因为它们在这里不是很相关):




  • 它分配内存



  • 因此,每个new-expression本质上是一系列两个函数调用:
    一个调用操作符new()(全局一个,或者一个由创建的对象的类型
    提供),然后

    对于示例1,考虑如果编译器决定
    生成代码会发生什么,如下所示:



    1:为T1分配内存

    2:构造T1

    3:为T2分配内存

    4:构造T2

    5:调用f()



    问题是这样的:如果步骤3或步骤4失败,因为
    异常,C ++标准不要求T1对象被
    销毁并且它的内存被释放。这是一个经典的内存泄漏,
    ,显然不是一个好事。
    [...]


阅读更多:



< >

为什么标准只是防止问题,要求编译器在清理时做正确的事?



基本的答案是,它没有被注意到,即使现在已经注意到它可能不是可取的修复它。 C ++标准允许编译器按照表达式求值的顺序有一定的自由度,因为这允许编译器执行可能不可能实现的优化。为了允许这样,表达式求值规则以一种方式指定这不是异常安全的,所以如果你想写异常安全代码,你需要知道,并避免这些情况。


所以我的问题是:


  1. 如何修复此典型异常不安全代码?我们应该避免编写这样的代码吗?


  2. 这个答案让我有点困惑,为了处理
    构造函数失败,我们应该根据 C ++常见问题解答并确保已正确释放已分配的内存,因此假设类T实现了处理构造失败的代码,上述代码中是否仍然存在异常安全问题?


感谢您的时间和帮助。

解决方案

首先,写 make_unique

  template< typename T,typename。 Args> 
std :: unique_ptr< T> make_unique(Args& ... args){
return {new T(std :: forward< Args>(args)...)};
}

这不包括在标准中,基本上作为监督。 std :: make_shared 是和 make_unique 可能会显示在C ++ 14或17中。



其次,将函数签名更改为:

  //在某些头文件中: 
void f(std :: unique_ptr< T1>,std :: unique_ptr< T2>);

并调用它:

  f(make_unique< T1>(),make_unique< T2>()); 

,结果是异常安全代码。



如果 T1 有一个非常重要的构造函数,你可以使用 make_unique< T1> ,它将它们完美地转发到 T1 的构造函数。



通过() {} 构建 T1 没有什么是完美的。


According to GOTW #56, there is a potential classic memory leak and exception safety issues in the following code:

//  In some header file:
void f( T1*, T2* );
//  In some implementation file:
f( new T1, new T2 );

The reason is that when we new T1, or new T2, there might be exceptions thrown from the classes' constructors.

Meanwhile, according to the explanation:

Brief recap: An expression like "new T1" is called, simply enough, a new-expression. Recall what a new-expression really does (I'll ignore placement and array forms for simplicity, since they're not very relevant here):

  • it allocates memory

  • it constructs a new object in that memory

  • if the construction fails because of an exception the allocated memory is freed

So each new-expression is essentially a series of two function calls: one call to operator new() (either the global one, or one provided by the type of the object being created), and then a call to the constructor.

For Example 1, consider what happens if the compiler decides to generate code as follows:

1: allocate memory for T1
2: construct T1
3: allocate memory for T2
4: construct T2
5: call f()

The problem is this: If either step 3 or step 4 fails because of an exception, the C++ standard does not require that the T1 object be destroyed and its memory deallocated. This is a classic memory leak, and clearly not a Good Thing. [...]

By reading more:

Why doesn't the standard just prevent the problem by requiring compilers to Do The Right Thing when it comes to cleanup?

The basic answer is that it wasn't noticed, and even now that it has been noticed it might not be desirable to fix it. The C++ standard allows the compiler some latitude with the order of evaluation of expressions because this allows the compiler to perform optimizations that might not otherwise be possible. To permit this, the expression evaluation rules are specified in a way that is not exception-safe, and so if you want to write exception-safe code you need to know about, and avoid, these cases. (See below for how best to do this.)

So my questions are:

  1. How to fix this typical exception unsafe code? Should we simply avoid writing code like this?

  2. The answer confused me a little bit, in order to handle constructor failures, we should throw exception from constructor according to C++ FAQ and make sure memory allocated are released properly, so assuming that the class T did implement code that handles construction failure, do we still have exception safety issue in the above code?

Thank you for your time and help.

解决方案

First, write make_unique:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique( Args&&... args ) {
  return {new T(std::forward<Args>(args)...)};
}

which was not included in the standard, basically as an oversight. std::make_shared is, and make_unique will probably show up in C++14 or 17.

Second, change your function signature to:

//  In some header file:
void f( std::unique_ptr<T1>, std::unique_ptr<T2> );

and call it like:

f( make_unique<T1>(), make_unique<T2>() );

and the result is exception-safe code.

If T1 has a non-trivial constructors you want to use, you can just pass the arguments to make_unique<T1> and it perfectly forwards them to the constructor of T1.

There are issues with the multiple ways of constructing a T1 via () or {}, but nothing is perfect.

这篇关于如何解决这个典型的异常不安全代码?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆