ChatGPT解决这个技术问题 Extra ChatGPT

从构造函数中抛出异常

我正在与一位同事就从构造函数中抛出异常进行辩论,并认为我想要一些反馈。

从设计的角度来看,可以从构造函数中抛出异常吗?

假设我在一个类中包装了一个 POSIX 互斥体,它看起来像这样:

class Mutex {
public:
  Mutex() {
    if (pthread_mutex_init(&mutex_, 0) != 0) {
      throw MutexInitException();
    }
  }

  ~Mutex() {
    pthread_mutex_destroy(&mutex_);
  }

  void lock() {
    if (pthread_mutex_lock(&mutex_) != 0) {
      throw MutexLockException();
    }
  }

  void unlock() {
    if (pthread_mutex_unlock(&mutex_) != 0) {
      throw MutexUnlockException();
    }
  }

private:
  pthread_mutex_t mutex_;
};

我的问题是,这是标准的做法吗?因为如果 pthread mutex_init 调用失败,互斥对象将不可用,因此抛出异常可确保不会创建互斥对象。

我是否应该为 Mutex 类创建一个成员函数 init 并调用 pthread mutex_init,在其中将根据 pthread mutex_init 的返回返回一个布尔值?这样我就不必为这样一个低级别的对象使用异常。

相关主题的另一个链接:writeulearn.com/exception-constructor
好吧,可以从 ctors 中像从任何其他函数中抛出一样多,也就是说,您应该小心地从任何函数中抛出。
不相关的事情:为什么不删除您的锁定/解锁方法,并直接在构造函数中锁定互斥体并在析构函数中解锁?这样,只需在作用域中声明一个自动变量即可自动锁定/解锁,无需处理异常、返回等...有关类似实现,请参见 std::lock_guard
如果您的构造失败并引发异常,则不会调用 ~Mutex() 并且不会清理 mutex_。不要在构造函数中抛出异常。
@LaurentGrégoire:在构造函数中创建和锁定互斥锁是没有意义的,因为没有其他人会引用所述互斥锁,所以它不会保护任何东西。您想要 lockunlock 以便您的互斥锁类型与 std::lock_guard 一起使用;他在这里重新实现 std::mutex,而不是 std::lock_guard,这两个类在 C++ 标准库中是分开的是有原因的。

D
Darren Cook

是的,从失败的构造函数中抛出异常是执行此操作的标准方法。阅读有关 Handling a constructor that fails 的常见问题解答以了解更多信息。有一个 init() 方法也可以,但是每个创建 mutex 对象的人都必须记住必须调用 init()。我觉得这违反了RAII原则。


在大多数情况下。不要忘记像 std::fstream 这样的东西。失败时它仍然会创建一个对象,但是因为我们总是在正常测试对象的状态,所以它运行良好。因此,在正常使用情况下测试的具有自然状态的对象可能不需要抛出。
@Widor:感谢您查看我建议的编辑号。 278978. 我再问一个与编辑相关的问题?此评论所附的答案有一个过时的超链接。要修复它,只需要更改一个字符,将 URL 中的“#faq-17.2”替换为“#faq-17.8”。但是,Stackoverflow 的软件要求像我这样的低声誉用户提交的编辑至少要更改六个字符。很明显,断开的链接想要修复,它不是一个六个字符的修复。请问你知道我该如何解决吗?
并非如此,在这种特定情况下,请注意他的 Mutex 析构函数永远不会被调用,可能会泄漏 pthread 互斥体。解决方案是为 pthread 互斥体使用智能指针,最好使用 boost 互斥体或 std::mutex,当有更好的替代方案时,没有理由继续使用旧的函数式操作系统构造。
@Martin York:我不确定 std::fstream 是一个很好的例子。是的。它确实依赖于构造函数后的错误检查。但应该吗?这是一个糟糕的设计,可以追溯到禁止构造函数抛出异常的 C++ 版本。
F
Ferruccio

如果确实从构造函数中抛出异常,请记住,如果需要在构造函数初始化程序列表中捕获该异常,则需要使用函数 try/catch 语法。

例如

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

对比

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }

需要注意的是,子对象的构造引发的异常不能被抑制:gotw.ca/gotw/066.htm
佚名

抛出异常是处理构造函数失败的最好方法。您应该特别避免半构建对象,然后依靠您的类的用户通过测试某种标志变量来检测构造失败。

在相关的一点上,你有几种不同的异常类型来处理互斥错误,这让我有点担心。继承是一个很好的工具,但它可能被过度使用。在这种情况下,我可能更喜欢单个 MutexError 异常,可能包含信息性错误消息。


我第二次 Neil 关于异常层次结构的观点 - 单个 MutexError 可能是更好的选择,除非您特别想以不同的方式处理锁定错误。如果您有太多异常类型,那么将它们全部捕获会变得令人厌烦且容易出错。
我同意一种类型的互斥异常就足够了。这也将使错误处理更加直观。
X
Xiaofeng
#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}

输出:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

不调用析构函数,因此如果需要在构造函数中抛出异常,则需要做很多事情(例如清理?)。


很好的一点。我很惊讶没有其他答案可以解决这种类型的泄漏。
您应该使用 std::unique_ptr 或类似的。如果在构造过程中抛出异常,则调用成员的析构函数,但普通指针没有任何异常。将 bar* b 替换为 std::unique_ptr<bar> b(您必须删除 delete b; 并添加 <memory> 标头),然后再次运行。
这种行为是非常明智的。如果构造函数失败(没有成功完成)为什么要调用析构函数?它没有什么要清理的,如果确实尝试清理甚至没有正确实例化的对象(想想一些指针),它会导致更多的问题,这是不必要的。
@zar 是的,问题不在于是否应该调用析构函数。在此示例中,应在引发异常之前进行清理。我并不是说我们不能在构造函数中抛出异常,我只是说开发人员应该知道他是什么东东。没有好,也没有坏,但在做之前要三思而后行。
根据@Naveen's answer,似乎内存确实释放了。但 valgrind --leak-check=full ./a.out 抱怨块丢失:错误摘要:来自 2 个上下文的 2 个错误
R
Richard Corden

从构造函数中抛出是可以的,但是您应该确保在 main 启动之后和完成之前构造您的对象:

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}

M
Michael Kohne

唯一不会从构造函数抛出异常的情况是您的项目有禁止使用异常的规则(例如,Google 不喜欢异常)。在这种情况下,您不会比其他任何地方都更希望在构造函数中使用异常,而您必须使用某种 init 方法。


您可能会对 groups.google.com/group/comp.lang.c++.moderated/browse_thread/… 上关于 Google 指南的冗长讨论感兴趣
有趣的讨论。我个人的意见是,只有在实际设计程序的错误处理结构以利用它们时才应该使用异常。如果您在编写代码后尝试进行错误处理,或者尝试将异常硬塞到不是为它们编写的程序中,那只会导致 try/catch EVERYWHERE(消除异常的优势)或程序崩溃最小的错误。我每天都处理这两个问题,我不喜欢它。
G
Guy Avraham

在这里添加所有答案,我想提一下,一个非常具体的原因/场景,您可能希望从类的 Init 方法而不是从 Ctor 抛出异常(这当然是首选和更常见的方法)。

我将提前提到,这个示例(场景)假设您不为类的指针数据成员使用“智能指针”(即 std::unique_ptr)。

直截了当:如果您希望您的类的 Dtor 在您调用它时(在这种情况下)捕获 Init() 方法抛出的异常后“采取行动” -您不得从 Ctor 抛出异常,因为不会在“半生不熟”的对象上调用 Ctor 的 Dtor 调用。

请参阅下面的示例来证明我的观点:

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}

我会再次提到,这不是推荐的方法,只是想分享一个额外的观点。

此外,正如您可能从代码中的一些打印内容中看到的那样 - 它基于 Scott Meyers(第 1 版)出色的“更有效的 C++”中的第 10 项。


Z
Zoran Horvat

如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数中抛出异常是比不抛出更好的解决方案。如果没有抛出异常,则将对象初始化为僵尸状态。这样的对象需要暴露一个标志,表明该对象是否正确。像这样的东西:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

这种方法的问题在于调用方。该类的每个用户在实际使用该对象之前都必须执行一个 if。这是对错误的呼吁——没有什么比在继续之前忘记测试条件更简单的了。

如果构造函数抛出异常,构造对象的实体应该立即处理问题。下游的对象消费者可以自由地假设对象是 100% 可操作的,因为他们获得了它。

这种讨论可以在许多方向上继续进行。

例如,使用异常作为验证问题是一种不好的做法。一种方法是结合工厂类的 Try 模式。如果您已经在使用工厂,请编写两种方法:

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

使用此解决方案,您可以就地获取状态标志,作为工厂方法的返回值,而无需输入带有错误数据的构造函数。

第二件事是如果你用自动化测试覆盖代码。在这种情况下,每段使用不抛出异常的对象的代码都必须通过一个额外的测试来覆盖——当 IsValid() 方法返回 false 时它是否正确运行。这很好地解释了在僵尸状态下初始化对象是一个坏主意。


是否可以将 CreateScaler 和 TryCreateScaler 设为静态?
g
g24l

除了 您不需要在特定情况下从构造函数中抛出之外,因为 pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized 并且您可以像在 std::mutex 中所做的那样在调用 lock 之后抛出:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

那么通常从构造函数中抛出对于构造过程中的获取错误是可以的,并且符合 RAII(资源获取是初始化)编程范式。

检查此example on RAII

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

关注这些陈述:

静态 std::mutex 互斥 std::lock_guard lock(mutex); std::ofstream 文件(“example.txt”);

第一个语句是 RAII 和 noexcept。在 (2) 中,很明显 RAII 应用于 lock_guard 并且它实际上可以 throw ,而在 (3) 中 ofstream 似乎不是 RAII ,因为必须通过调用 is_open() 来检查对象状态检查 failbit 标志。

乍一看,它似乎还没有决定它是什么标准方式,并且在第一种情况下 std::mutex 不会引发初始化,*与 OP 实现相反*。在第二种情况下,它会抛出从 std::mutex::lock 抛出的任何东西,而在第三种情况下,根本就没有抛出。

注意差异:

(1) 可以声明为静态,实际会声明为成员变量 (2) 永远不会实际声明为成员变量 (3) 预期声明为成员变量,底层资源可能并不总是可用。

所有这些形式都是 RAII;要解决这个问题,必须分析 RAII。

资源:你的对象

获取(分配):您正在创建的对象

初始化:您的对象处于其不变状态

这不需要您在构造时初始化和连接所有内容。例如,当您创建一个网络客户端对象时,您实际上不会在创建时将其连接到服务器,因为它是一个缓慢的操作,并且会失败。您将改为编写一个 connect 函数来执行此操作。另一方面,您可以创建缓冲区或仅设置其状态。

因此,您的问题归结为定义您的初始状态。如果在你的情况下你的初始状态是 mutex must be initialized 那么你应该从构造函数中抛出。相比之下,不进行初始化就好了(就像在 std::mutex 中所做的那样),并将您的不变状态定义为 mutex is created 。无论如何,不变量不一定会受到其成员对象的状态的影响,因为 mutex_ 对象通过 Mutex 公共方法 Mutex::lock()Mutex::unlock()lockedunlocked 之间变化。

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};

S
SridharKritha

请注意the destructor never gets called after the exception is thrown from the constructor

struct B
{
    char* p;
    B() { 
        cout << "Constructor - B" << endl; 
        p = new char[1024];
        throw std::exception("some exception");
    }
    ~B() { // NEVER GETS CALLED AFTER EXCEPTION !!!! - memory leak 
        cout << "Destructor - B" << endl; 
        delete[] p;
    } 
};

int main()
{
    try {
        B b;
    }
    catch (...) {
        cout << "Catch called " << endl;
    }
}

输出:

Constructor - B
Catch called       (Note: B's Destructor is NEVER called)

d
danish

虽然我没有在专业水平上工作过 C++,但在我看来,从构造函数中抛出异常是可以的。我在.Net中这样做(如果需要)。查看 thisthis 链接。这可能是你的兴趣。


.NET 不是 C++,JAVA 也不是。投掷的机制不一样,成本也不一样。