2009-05-01 145 views
222

我正在与同事讨论抛出构造函数的异常,并且认为我想要一些反馈。从构造函数中抛出异常

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

可以说,我在一个类中封装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的返回值返回一个bool?这样我不必为这样的低级对象使用异常。

+0

关于相关主题的另一个链接:http://www.writeulearn.com/exception-constructor/ – 2014-11-04 17:38:41

+0

可以从ctors扔掉尽可能多的东西,就像从其他函数中扔出来一样,被说你应该小心地扔掉来自任何功能。 – g24l 2015-12-26 23:04:52

+3

不相干的东西:为什么不移除你的锁/解锁方法,直接在构造函数中锁定互斥锁,并在析构函数中解锁?这种方式只需在作用域中声明自动变量即可自动锁定/解锁,无需处理异常,返回等等。有关类似的实现,请参阅std :: lock_guard。 – 2016-07-07 08:25:44

回答

207

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

0

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

30

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

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

85

如果您确实从构造函数中抛出异常,请记住如果您需要在构造函数初始值设定项列表中捕获该异常,则需要使用函数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 
    { ... } 
10

这是确定从你的构造函数抛出,但你应该确保 主要开始后,你的对象构造和之前 结束:

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) 
    { 
    } 
} 
2

您不会从构造函数中抛出异常的唯一时间是如果您的项目有一个反对使用例外规则(例如,Google不喜欢例外)。在这种情况下,你不希望在你的构造函数中使用异常,而必须使用某种类型的init方法。

3

如果您的项目通常依赖于异常来区分不良数据和良好数据,那么从构造函数中抛出异常比不抛出更好。如果不抛出异常,则对象将以僵尸状态初始化。这样的对象需要暴露一个标志,说明对象是否正确。像这样的:

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时它是否正确运行。这很好地解释了初始化僵尸状态的对象是一个坏主意。

3

除了事实,即在std::mutex做了你并不需要在特定情况下,从构造函数抛出因为pthread_mutex_lock actually returns an EINVAL if your mutex has not been initialized,您可以在通话后扔至lock

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

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

然后在一般从施工人员投掷OK收购在施工过程中的错误,并符合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) 
} 

关注这些语句:

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("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函数来做到这一点。另一方面,您可以创建缓冲区或仅设置其状态。

因此,您的问题归结为定义您的初始状态。如果你的情况你的初始状态是互斥量必须初始化那么你应该从构造函数中抛出。相反,它只是没有初始化然后(如在std::mutex中所做的那样),并将您的不变状态定义为互斥体创建为。在任何速率不变不受其成员对象的状态不一定compromized,由于通过Mutex公共方法Mutex::lock()Mutex::unlock()lockedunlocked之间的mutex_对象发生突变。

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 
}; 
8
#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 

析构函数不叫,所以如果一个例外,需要在构造函数中被抛出,很多东西(如清理?)做。

相关问题