19 September 2022

C++ 核心指南目录

“If a constructor cannot construct a valid object, throw an exception”

理由

放任无效对象会导致麻烦的问题。

例子

class X2 {
    FILE* f;
    // ...
public:
    X2(const string& name)
        :f{fopen(name.c_str(), "r")}
    {
        if (!f) throw runtime_error{"could not open" + name};
        // ...
    }

    void read();      // read from f
    // ...
};

void f()
{
    X2 file {"Zeno"}; // throws if file isn't open
    file.read();      // fine
    // ...
}

错误例子

class X3 {     // bad: the constructor leaves a non-valid object behind
    FILE* f;   // call is_valid() before any other function
    bool valid;
    // ...
public:
    X3(const string& name)
        :f{fopen(name.c_str(), "r")}, valid{false}
    {
        if (f) valid = true;
        // ...
    }

    bool is_valid() { return valid; }
    void read();   // read from f
    // ...
};

void f()
{
    X3 file {"Heraclides"};
    file.read();   // crash or bad read!
    // ...
    if (file.is_valid()) {
        file.read();
        // ...
    }
    else {
        // ... handle error ...
    }
    // ...
}

注意

对于变量来说,不管是在栈上的还是某个对象的成员,都没法直接通过函数调用获取错误信息。如果有无效的对象,然后要倚靠用户使用前自己去调用 isinvalid() 这样的函数进行有效性检查,很麻烦、容易出错、效率不高。

例外

在某些领域,比如飞机控制等硬实时系统中,为了实时性,异常出错没有额外的工具支持,所以就必须使用 isvalid() 这样的技巧。这种情况下,及时的调用 isvalid() 来模拟 RAII。

其他情况

如果你有想法使用“构造后初始化”或“两级初始化”方法,请赶紧抹杀掉。如果你真的要这么做,请试试工厂函数方法。

注意

有人为了避免代码重复,用 init() 函数,而不在构造函数中进行初始化。把构造过程交给成员的默认初始化会更好。还有人觉得应该延迟到对象使用的时候再初始化,这种情况的解决办法是不要声明变量,指导要用的时候再声明并初始化。