12

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

4

4 回答 4

18

我会坚持使用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::fstreamwill 自动关闭,一个[std::lock_guard][2]will 执行我在我的示例中尝试执行的操作,并且要么std::unique_ptrstd::shared_ptr将负责销毁。

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

于 2013-08-19T22:48:21.040 回答
3

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

于 2013-08-19T22:33:43.493 回答
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;
    }
}
于 2013-08-19T23:29:50.733 回答
1

如果您必须处理资源,并且您的用例没有由标准库中的任何实用程序处理,那么规则很简单。处理一个,并且只处理一个资源。任何需要处理两个资源的类都应该存储两个能够自行处理的对象(即遵循 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;
};

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

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

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

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

于 2013-08-19T22:54:54.087 回答