在C++构造函数中抛出异常

3

我创建了一个类,如果其中一个成员为空,我不想创建对象。以下是代码行:

#include "verification/CVerifObj.hpp"

VerifObj::VerifObj(const fs::path& imgNameIn)
{
    m_image = cv::imread(imgNameIn.string());
    AnalyseCSV csv;
    m_plateContour = csv.getPlateRegion(imgNameIn); // search for the csv file and load the values
    if (m_plateContour.empty()) // throw exception if empty csv
    {
        throw EmptyContourException(imgNameIn.string());
    }
    m_imageName = imgNameIn.string();
}

VerifObj::~VerifObj()
{
    // are these enough for destructor?
    m_image.release();
    m_plateContour.clear();
}

这样可以吗?还是我需要做更多的事情?如果抛出异常,我如何确保对象不会被创建?

我有以下代码行来确保它:

for(fs::directory_iterator iter(folderIn); iter != end; ++iter)
{
    if(!fs::is_regular_file(iter->status()))
    {
        continue;
    }
    std::string extension = iter->path().extension().string();
    if(targetExtensions.find(extension) == targetExtensions.end()) 
    { 
        continue;
    }
    try
    {
        VerifObj verifObj(iter->path());
        verifObjVecRet.push_back(verifObj);
    }
    catch (EmptyContourException& ece)
    {
        continue;
    }
    catch (CSVException &csve)
    {
        continue;
    }
}
2个回答

8
希望你的类中的m_imagem_plateContour(以及其他任何复杂成员)都是正确设计的RAII类型,具有清理它们可能拥有的任何资源的析构函数。
在这种情况下,你的类将不需要析构函数,如果你的构造函数抛出异常,所有成员都将自动正确销毁 - 在那里不需要采取任何行动。
然而,析构函数的存在意味着它们可能是需要手动清理的邪恶类型。在这种情况下,请修复它们。
如果由于某些原因无法修复它们,则需要在抛出异常之前使用m_image.release();。你还需要大量的咖啡因补给,因为这样的类将导致长时间的调试会话,试图修复内存泄漏问题。

2

简短的回答是,将东西放在构造函数中是危险的。

首先,让我们定义设计问题:您有一个可能无法初始化的类。如果类无法初始化,则无法使用它(如果使用会导致其他错误),因此在涉及该类的情况下,类失败被认为是“关键故障”。

简而言之,我们要避免的是让用户使用未能成功初始化的类。

想象以下情况:

class A
{
  public:
  A()
  {
    stuff1 = malloc(100);//some dynamic memory allocation
    throw "This throw is crazy";
    stuff2 = malloc(100);//some dynamic memory allocation
  }
  ~A() {free(stuff1); free(stuff2);}
  private: void* stuff2;void* stuff2;
};

int main(int argc, char** argv)
{
  A a;
}

一旦您在构造函数中出现问题,会发生什么情况?那么,它将成为一个即时的内存泄漏。析构函数永远不会被调用。这是非常糟糕的。如果您处理异常:

int main(int argc, char** argv)
{
  try
  {
  A a;
  }
  catch(...)
  {
     //can't do much here
  }
}

你丢失了对A的引用,这是一场噩梦。因此有些人尝试使用以下方法来解决(但事实上仍然不好)

int main(int argc, char** argv)
{
   A* a;

   try { a= new A();}
   catch(...){delete a;}
}

但这同样不好。你可能仍然有一个对a的引用(指向a的内存不会泄漏),但是a现在处于未知状态...你仍然需要在某天删除它,而且stuff2的free操作失败了。

解决方法之一是让你的构造函数和析构函数更加智能。捕获构造函数中可能抛出的任何异常并清理对象,返回一个“僵尸”对象。并且使析构函数能够轻松地检查僵尸对象。我发现这种方法更加复杂。

对一些人来说,更好的方法是使用初始化器:

 class A
 {
 public:
    A() {stuff1=null; stuff2=null;}
    void init()
    {
      stuff1 = malloc(100);//some dynamic memory allocation
      throw "This throw is crazy";
      stuff2 = malloc(100);//some dynamic memory allocation
    }
    void destroy() {if (stuff1) {delete stuff1; stuff1=NULL;} if (stuff2) {delete stuff2; stuff2=NULL;}}
    ~A() {destroy();}
 };

我不喜欢这种方法,因为你仍然会得到“僵尸”对象,并且需要调用init和destroy来维护它们。回到最初的问题,既然无论是一个更聪明的构造函数还是提供初始化器都不能解决核心问题:最终用户仍然可以(事实上相当容易)使用处于未知状态的实例。

在这里我喜欢遵循伪RAII(资源获取即初始化)原则:如果你有一个对象的引用,那么它应该是有效的。否则就不应该有内存泄漏。实现这一点的最好方式(在我看来)是使用工厂方法,并将所有初始化设为私有。

 class A
 {
 public:
    ~A() {destroy();}
     static A* MakeA()
     {
         A* ret = new A();
         try { ret->init();}
         catch(...)
         {
            delete ret;
            return NULL;
         }
         return ret;
     }
  private: void* stuff1,*stuff2;
          A() {stuff1=null; stuff2=null;}
    void init()
    {
      stuff1 = malloc(100);//some dynamic memory allocation
      throw "This throw is crazy";
      stuff2 = malloc(100);//some dynamic memory allocation
    }
    void destroy() {if (stuff1) {delete stuff1; stuff1=NULL;} if (stuff2) {delete stuff2; stuff2=NULL;}}
 };

现在用户无法对失败的对象进行有效引用,这很好。
专业提示:
1. 异常处理速度较慢。如果您预计此情况经常发生或认为它不会是致命错误,请在init语句中使用错误代码。工厂方法中的错误代码也可以为用户提供更多信息。
2. 我在这个答案中使用了原始指针,因为我太懒了,不想在答案中写那些字符。始终使用智能指针,这将变得更加容易。摆弄原始指针只适用于犯罪精神异常者。 (澄清使用智能指针)
编辑:澄清我懒惰的内容。

3
"我用原始指针是因为懒。" 我总是使用智能指针,正是因为我很懒。 - MarekR
那么,您会建议我做一些类似构造对象的事情,如果该成员为空,则不将其添加到向量中(请参见“try”块)?这仅适用于我的情况(我以前没有看到过此选项)。 - sop
1
我太懒了,不想调试由于操作原始指针而导致的潜在内存泄漏问题。只需使用智能指针即可解决问题。这样你就永远不需要说出“在构造函数中抛出东西是危险的”这样的无意义话语了。它只有你自己制造的那么危险。 - Mike Seymour
1
@MadScienceDreams:那是无稽之谈。如果你抛出了部分构建的对象和未解开的堆栈帧上的任何内容,它们都会被销毁,不会留下任何实例处于任何状态。唯一的问题是如果你在处理非RAII资源,这种情况下解开不会捕获它们。(我猜这就是你所说的“僵尸”,但只有当你像这个答案中写的那样编写疯狂的代码时才会出现这个问题。) - Mike Seymour
1
@MadScienceDreams:当然,b2是被初始化的。和所有成员一样,在构造函数体开始之前就已经被初始化了。当构造函数抛出异常时,部分构造的对象的成员将会被销毁(删除由b1拥有的对象,并对空的b2不做任何操作),并且一旦控制流程到达异常处理程序,该对象本身将无法访问。 - Mike Seymour
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接