我知道如果在构造函数中抛出异常,则不会调用析构函数(简单类,没有继承)。因此,如果在构造函数中抛出异常,并且有可能没有清理某些堆内存。那么这里最好的做法是什么?让我们假设我必须在构造函数中调用一些函数,它可能会抛出异常。在这种情况下,我总是使用共享指针吗?什么是替代品?谢谢!
答案 0 :(得分:16)
我会坚持使用RAII成语。
如果您避免使用“裸”资源(如操作员新的,裸指针,裸体互斥体等)而将所有内容包装到具有正确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_ptr
或std::shared_ptr
将负责销毁。< / p>
最好的建议?阅读有关RAII(并根据它设计),使用标准库,不要创建裸露的资源,并熟悉Herb Sutter在“异常安全”方面所说的话(继续阅读他的{{3} }或谷歌“website”)
答案 1 :(得分:3)
避免需要使用标准库容器在堆上分配内存(通过new
和new[]
)。如果无法做到这一点,始终使用智能指针(如std::unique_ptr<>
)来管理堆上分配的内存。然后你将永远不需要编写用于删除内存的代码,即使在构造函数中抛出异常,它也会被自动清理(实际上构造函数通常是异常的地方,但是析构函数应该不会抛出)
答案 2 :(得分:1)
如果您必须处理资源,并且标准库中的任何实用程序都不处理您的用例,那么规则很简单。处理一个,只有一个资源。需要处理两个资源的任何类都应该存储两个能够自己处理的对象(即跟随RAII的对象)。作为不该做的一个简单示例,假设您想要编写一个需要动态数组int的类,以及一个动态的双精度数组(暂时忘记标准库)。你不会做的是:
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
已经处理了上述情况。但就像我说的那样,这是如果你碰巧有一个标准库没有涵盖的情况。
答案 3 :(得分:1)
您经常可以做的是在构造函数之前调用可能失败的函数,并使用可能失败的函数返回的值调用教师。
#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;
}
}