【问题标题】:What's the best practice to prevent memory leak if an exception thrown in constructor?如果构造函数中抛出异常,防止内存泄漏的最佳做法是什么?
【发布时间】:2013-08-21 20:37:30
【问题描述】:

我知道如果构造函数抛出异常,析构函数将不会被调用(简单类,没有继承)。因此,如果在构造函数中抛出异常并且有可能没有清理一些堆内存。那么这里的最佳做法是什么?假设我必须在构造函数中调用某个函数,它可能会抛出异常。在这种情况下我应该总是使用共享指针吗?有什么替代方案?谢谢!

【问题讨论】:

  • @OtávioDécio 那是垃圾。构造函数是抛出异常的地方——如果初始化参数没有意义,还有什么地方可以抱怨?
  • @ycshao 你是指你分配的内存还是你调用的函数分配和抛出它的内存?
  • @Walter 我不同意。在构造函数中不抛出异常可以获得很多好处(例如,如果T 的构造函数声明为noexcept(true),则std::vector<T> 将使用push_back 使用push_back,否则它将复制)。此外,抛出异常会展开堆栈,这可能过于昂贵。有些人更喜欢使用“两阶段构造函数”,其中构造函数是noexcept(true),并且各个资源通过可能会或可能不会抛出(或以其他方式发出失败信号)的方法获取以获得收益,
  • @Arrieta 好的,我同意复制,特别是移动构造函数应该避免抛出。但是,当检查参数的敏感性不是问题时,它们从现有对象构造。关键是带有可能具有无意义值的参数的构造函数。
  • @Walter 好的设计可以帮助避免“无意义”的值(通过类型检查或类似的构造)。当然,你总是可以有一个不存在的文件名,或者类似的类型正确但仍然是无意义的构造函数参数。在这种情况下,有些人更喜欢返回错误代码或类似的“成功标志”以避免抛出异常和展开堆栈。例如,dynamic_cast 可以返回空指针,而不是抛出 bad_cast。我是“一切为了例外”,但我真的认为构造函数不一定是抛出它们的最佳位置(它们可以是

标签: c++ memory exception-handling constructor destructor


【解决方案1】:

我会坚持RAII 成语。

如果您避免使用“裸”资源(例如 operator new、裸指针、裸互斥体等),而是将所有内容包装到具有适当 RAII 行为的容器或类中,您将不会遇到您描述的问题,即使存在例外情况。

也就是说,不要在你的构造函数中获取裸资源。相反,创建一个本身遵循 RAII 的对象的实例。这样,即使您的构造函数失败(即创建实例的构造函数),也会调用已初始化对象的析构函数。

所以,这是不好的做法:

#include<iostream>
#include<stdexcept>

struct Bad {
  Bad() {
    double *x = new double;
    throw(std::runtime_error("the exception was thrown"));
  }

  ~Bad() {
    delete x;
    std::cout<<"My destructor was called"<<std::endl;
  }

  double *x;  
};

int main() {
  try {
    Bad bad;
  } catch (const std::exception &e) {
    std::cout<<"We have a leak! Let's keep going!"<<std::endl;
  }
  std::cout<<"Here I am... with a leak..."<<std::endl;
  return 0;
}

输出:

We have a leak! Let's keep going!
Here I am... with a leak...

与这种人为且愚蠢的好实现进行比较:

#include<iostream>
#include<stdexcept>

struct Resource {

  Resource() {
    std::cout<<"Resource acquired"<<std::endl;    
  }

  ~Resource() {
    std::cout<<"Resource cleaned up"<<std::endl;        
  }

};

struct Good {
  Good() {
    std::cout<<"Acquiring resource"<<std::endl;
    Resource r;
    throw(std::runtime_error("the exception was thrown"));
  }

  ~Good() {
    std::cout<<"My destructor was called"<<std::endl;
  }  
};


int main() {
  try {
    Good good;
  } catch (const std::exception &e) {
    std::cout<<"We DO NOT have a leak! Let's keep going!"<<std::endl;
  }
  std::cout<<"Here I am... without a leak..."<<std::endl;
  return 0;
}

输出:

Acquiring resource
Resource acquired
Resource cleaned up
We DO NOT have a leak! Let's keep going!
Here I am... without a leak...

我的观点是:尝试将所有需要释放的资源封装到自己的类中,构造函数不会抛出,析构函数正确释放资源。然后,在析构函数可能抛出的其他类上,只需创建被包装资源的实例,获取的资源包装器的析构函数将保证被清理。

以下可能是一个更好的例子:

#include<mutex>
#include<iostream>
#include<stdexcept>

// a program-wide mutex
std::mutex TheMutex;

struct Bad {
  Bad() {
    std::cout<<"Attempting to get the mutex"<<std::endl;
    TheMutex.lock();
    std::cout<<"Got it! I'll give it to you in a second..."<<std::endl;
    throw(std::runtime_error("Ooops, I threw!"));
    // will never get here...
    TheMutex.unlock();
    std::cout<<"There you go! I released the mutex!"<<std::endl;    
  }  
};

struct ScopedLock {
  ScopedLock(std::mutex& mutex)
      :m_mutex(&mutex) {
    std::cout<<"Attempting to get the mutex"<<std::endl;
    m_mutex->lock();
    std::cout<<"Got it! I'll give it to you in a second..."<<std::endl;    
  }

  ~ScopedLock() {
    m_mutex->unlock();
    std::cout<<"There you go! I released the mutex!"<<std::endl;        
  }
  std::mutex* m_mutex;      
};

struct Good {
  Good() {
    ScopedLock autorelease(TheMutex);
    throw(std::runtime_error("Ooops, I threw!"));
    // will never get here
  }  
};


int main() {
  std::cout<<"Create a Good instance"<<std::endl;
  try {
    Good g;
  } catch (const std::exception& e) {
    std::cout<<e.what()<<std::endl;
  }

  std::cout<<"Now, let's create a Bad instance"<<std::endl;
  try {
    Bad b;
  } catch (const std::exception& e) {
    std::cout<<e.what()<<std::endl;
  }

  std::cout<<"Now, let's create a whatever instance"<<std::endl;
  try {
    Good g;
  } catch (const std::exception& e) {
    std::cout<<e.what()<<std::endl;
  }

  std::cout<<"I am here despite the deadlock..."<<std::endl;  
  return 0;
}

输出(用gcc 4.8.1 编译,使用-std=c++11):

Create a Good instance
Attempting to get the mutex
Got it! I'll give it to you in a second...
There you go! I released the mutex!
Ooops, I threw!
Now, let's create a Bad instance
Attempting to get the mutex
Got it! I'll give it to you in a second...
Ooops, I threw!
Now, let's create a whatever instance
Attempting to get the mutex

现在,请不要按照我的示例创建自己的范围保护。 C++(特别是 C++11)在设计时考虑了 RAII,并提供了丰富的生命周期管理器。例如,std::fstream 将自动关闭,[std::lock_guard][2] 将执行我在示例中尝试执行的操作,std::unique_ptrstd::shared_ptr 将负责销毁。

最好的建议?阅读 RAII(并根据它进行设计),使用标准库,不要创建裸资源,并熟悉 Herb Sutter 关于“异常安全”的说法(继续阅读他的website,或谷歌“Herb Sutter Exception Safety”)

【讨论】:

  • RAII 是正确的方法,但资源作为本地变量是一种罕见的情况。通常你会担心最终会被正在建设的对象所拥有的资源。
  • @BenVoigt 同意。在这种情况下你会推荐什么?两期建设?
  • 您可以使用执行分配并返回智能指针的辅助函数来初始化初始化器列表中的智能指针。或者您可以拥有一个本地智能指针并将其交换/移动到成员变量。但是您想出了一个很好的本地资源示例,具有互斥体所有权范围。
  • 关于 RAII 的注意事项:该技术考虑到在 C++ 中,唯一可以保证在抛出异常后执行的代码是驻留在堆栈上的对象的析构函数。通过包装代码并避免使用“裸”资源,一切都基于堆栈以及如果发生异常,这些资源将被释放的事实。
  • @Ben Voigt:我说“通过包装代码并避免使用“裸”资源......”。正如答案中所说,意思是“避免手动动态内存分配”。
【解决方案2】:

避免使用标准库容器在堆上分配内存(通过newnew[])。如果这是不可能的,总是使用智能指针,如std::unique_ptr&lt;&gt; 来管理在堆上分配的内存。然后你将永远不需要编写删除内存的代码,即使在你的构造函数中抛出异常,它也会被自动清理(实际上构造函数通常是异常的可能位置,但析构函数真的不应该抛出) .

【讨论】:

  • 另一种说法是用默认析构函数足够的方式实现你的类。
【解决方案3】:

如果您必须处理资源,而您的用例没有由标准库中的任何实用程序处理,那么规则很简单。处理一个,并且只处理一个资源。任何需要处理两个资源的类都应该存储两个能够自行处理的对象(即遵循 RAII 的对象)。作为一个不该做什么的简单示例,假设您想编写一个需要一个动态整数数组和一个动态双精度数组的类(暂时忘记标准库)。你不会做的是:

class Dingbat
{
public:
    Dingbat(int s1, int s2)
    {
        size1 = s1;
        size2 = s2;
        a1 = new int[s1];
        a2 = new int[s2];
    }
    ...
private:
    int * a1;
    double * a2;
    int size1, size2;
};

上面构造函数的问题是,如果a2的分配失败,会抛出异常,a1的内存没有释放。您当然可以使用 try catch 块来处理这个问题,但是当您拥有多个资源时,它会变得更加复杂(不必要地)。

相反,您应该编写能够正确处理单个动态数组的类(或在这种情况下为单个类模板),负责初始化自身、复制自身和处理自身。如果只有一次调用new,那么您无需担心分配失败。将抛出异常并且不需要释放内存。 (你可能想要处理它并抛出你自己的自定义异常以提供更多信息)

一旦你完成了那个/那些类,那么你的Dingbat 类将包含这些对象中的每一个。 Dingbat 类则要简单得多,并且可能不需要任何特殊的例程来处理初始化、复制或销毁。

这个例子当然是假设的,因为上述情况已经由std::vector 处理。但就像我说的,这是因为如果你碰巧遇到了标准库没有涵盖的情况。

【讨论】:

    【解决方案4】:

    你经常可以做的就是在构造函数之前调用可能失败的函数,然后用可能失败的函数返回的值调用导师。

    #include <string>
    #include <iostream>
    #include <memory>
    
    class Object {};
    

    这只是我们班级需要的一些Object。它可以是连接的套接字,也可以是绑定的套接字。在构造函数中尝试连接或绑定时可能会失败。

    Object only_odd( int value ) {
        if ( value % 2 == 0 )
            throw "Please use a std::exception derived exception here";
        else
            return Object();
    }
    

    此函数返回一个对象并在失败时抛出(对于每个偶数)。所以这可能是我们首先想要在析构函数中做的事情。

    class ugly {
        public:
            ugly ( int i ) {
                obj = new Object;
                try{
                    *obj = only_odd( i );
                }
                catch ( ...) {
                    delete obj;
                    throw ( "this is why this is ugly" );
                }
            }
    
            ~ugly(){ delete obj; }
    
        private:
    
            Object* obj;
    };
    

    better 采用可能失败并因此抛出的预构造值。因此,我们也可以从已经初始化的对象构造better 类。然后我们甚至可以在类被构造之前进行错误处理,然后我们不必从构造函数中抛出。更好的是,它使用智能指针来处理内存,这样我们就可以非常确定内存被删除了。

    class better {
    
        public:
    
            better ( const Object& org ) : obj { std::make_shared<Object>(org) }
            {
            }
    
        private:
            /*Shared pointer will take care of destruction.*/
            std::shared_ptr<Object>  obj;
    };
    

    这可能就是我们使用它的方式。

    int main ( ) {
        ugly (1);
    
        /*if only odd where to fail it would fail allready here*/
        Object obj = only_odd(3); 
        better b(obj);
    
        try { /*will fail since 4 is even.*/
            ugly ( 4  );
        }
        catch ( const char* error ) {
            std::cout << error << std::endl;
        }
    }
    

    【讨论】:

    • 首选初始化而不是赋值。 better( const Object&amp; org ) : obj{std::make_shared&lt;Object&gt;(org)} {}
    • @BenVoigt 感谢您的评论,我同意并更新了答案。
    猜你喜欢
    • 2015-08-29
    • 2020-11-02
    • 2019-08-19
    • 2014-11-15
    • 2018-11-18
    • 2014-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多