关于c ++:从构造函数中抛出异常

Throwing exceptions from constructors

我正在和一个同事讨论如何抛出构造函数的异常,我想我需要一些反馈。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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对象将不可用,因此引发异常可确保不会创建mutex。

我应该为mutex类创建一个成员函数init并调用pthread mutex_init,在该函数中,将根据pthread mutex_init的返回返回一个bool吗?这样我就不必为这样一个低级对象使用异常。


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


如果确实从构造函数引发异常,请记住,如果需要在构造函数初始值设定项列表中捕获该异常,则需要使用函数try/catch语法。

例如

1
2
3
4
5
6
func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

VS

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


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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#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;
}

输出:

1
2
3
4
5
6
7
8
heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
  {
  }
}

除了在特定情况下不需要抛出构造函数之外,因为如果您的互斥体尚未初始化,并且您可以像在std::mutex中那样在调用lock之后抛出,那么pthread_mutex_lock实际上会返回一个einval:

1
2
3
4
5
6
7
8
9
void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

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

然后,一般来说,对于构造过程中的获取错误,从构造函数抛出是可以的,并且符合RAII(资源获取是初始化)编程范式。

在raii上检查这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
}

关注这些陈述:

  • static std::mutex mutex
  • std::lock_guard lock(mutex);
  • std::ofstream file("example.txt");
  • 第一个声明是raii和noexcept。在(2)中,很明显RAII应用于lock_guard,实际上它可以用于throw,而在(3)中,ofstream似乎不是RAII,因为必须通过调用检查failbit标志的is_open()来检查对象状态。

    乍一看,它似乎还没有决定它的标准方式是什么,在第一种情况下,与OP实现相比,std::mutex不引入初始化*。在第二种情况下,它会扔任何从以东丢来的东西(13),在第三种情况下,它一点也不扔。

    注意区别:

    (1)可以声明为静态,并将实际声明为成员变量(2)永远不会实际期望声明为成员变量(3)应该声明为成员变量,并且基础资源可能并不总是可用的。

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

    • 资源:您的对象
    • 获取(分配):正在创建的对象
    • 初始化:对象处于其不变状态

    这不需要初始化和连接构造上的所有内容。例如,当您要创建一个网络客户机对象时,您不会在创建时将其实际连接到服务器,因为这是一个缓慢的操作,但会出现故障。您可以编写一个connect函数来实现这一点。另一方面,您可以创建缓冲区或只是设置其状态。

    因此,您的问题归根结底就是定义初始状态。如果您的初始状态是mutex,则必须初始化它,然后从构造函数抛出。相反,如果不初始化then(就像在std::mutex中所做的那样),并将固定状态定义为创建互斥体,那就很好了。无论如何,由于mutex_对象通过Mutex公共方法Mutex::lock()Mutex::unlock()lockedunlocked之间发生突变,因此该不变量不必由其成员对象的状态来完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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
    };


    如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数中抛出异常比不抛出更好的解决方案。如果未引发异常,则对象将在僵尸状态下初始化。此类对象需要公开一个标志,该标志指示对象是否正确。像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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。这是对bug的调用——没有什么比在继续之前忘记测试一个条件更简单的了。

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

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

    例如,将异常作为验证问题使用是一种糟糕的做法。一种方法是结合工厂类使用一个try模式。如果你已经在使用工厂,那么写两种方法:

    1
    2
    3
    4
    5
    6
    class ScalerFactory
    {
        public:
            Scaler CreateScaler(double factor) { ... }
            int TryCreateScaler(double factor, Scaler **scaler) { ... };
    }

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

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


    我想说的是,除了这里的所有答案之外,还有一个非常具体的原因/场景,您可能希望从类的Init方法中抛出异常,而不是从ctor中抛出异常(当然,这是首选和更常见的方法)。

    我将提前提到这个示例(场景)假设您没有为您的类使用"智能指针"(即-std::unique_ptr)。s指针数据成员。

    因此,关键是:如果您希望类的dtor在(在本例中)捕获Init()方法抛出的异常之后调用它时"采取行动"——您不能从ctor抛出异常,因为ctor的dtor调用不会在"半烘焙"对象上调用。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    #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 <<"
     
     item10Usage - end"
    << endl;
    }

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

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

    此外,正如你可能从代码中的一些打印中看到的那样,它是基于Scott Meyers(第一版)中的"更有效的C++"中的第10项。

    希望它有帮助。

    干杯,

    家伙。


    唯一一次您不会从构造函数中抛出异常的是,如果您的项目有一个规则禁止使用异常(例如,Google不喜欢异常)。在这种情况下,您不希望在构造函数中使用异常,而应该使用某种类型的init方法。


    虽然我还没有在专业层面上使用C++,但在我看来,从构造函数中抛出异常是可以的。我在.NET中这样做(如果需要)。看看这个和这个链接。这可能是你感兴趣的。