我知道如果构造函数中抛出异常,析构函数将不会被调用(简单类,没有继承)。因此,如果构造函数中抛出异常并且有可能某些堆内存没有清除。那么这里的最佳实践是什么?假设我必须在构造函数中调用某个函数,并且它可能会抛出异常。在这种情况下,我是否应该始终使用共享指针?还有什么替代方案?谢谢!
我知道如果构造函数中抛出异常,析构函数将不会被调用(简单类,没有继承)。因此,如果构造函数中抛出异常并且有可能某些堆内存没有清除。那么这里的最佳实践是什么?假设我必须在构造函数中调用某个函数,并且它可能会抛出异常。在这种情况下,我是否应该始终使用共享指针?还有什么替代方案?谢谢!
我会坚持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
std::fstream
会自动关闭,[std::lock_guard][2]
将执行我在示例中尝试执行的操作,而std::unique_ptr
或std::shared_ptr
将负责销毁。
最好的建议是:阅读关于RAII的文章(并根据其进行设计),使用标准库,不要创建裸资源,并熟悉Herb Sutter关于“异常安全性”的观点(可以访问他的website,或者搜索“Herb Sutter Exception Safety”)。
new
和new[]
)。如果这不可能, 始终使用智能指针,例如std::unique_ptr<>
来管理在堆上分配的内存。然后你就不需要编写释放内存的代码了,即使在构造函数中抛出异常也会自动清理内存(实际上,构造函数通常是发生异常的地方,但析构函数真的不应该抛出异常)。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
处理。但正如我所说,这是针对您可能遇到的标准库未覆盖的情况。#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& org) : obj{std::make_shared<Object>(org)} {}
- Ben Voigt
noexcept(true)
,则std::vector<T>
将使用std::move
进行push_back
,否则它将进行复制)。此外,抛出异常会展开堆栈,这可能太昂贵了。一些人更喜欢使用“两阶段构造函数”,其中构造函数是noexcept(true)
,并且通过可能或可能不会抛出异常(或以其他方式发出失败信号)的方法获取各个资源以获得好处。 - Escualo