如何防止将空指针转换为引用

时间:2016-10-28 12:58:33

标签: c++ pointers reference singleton

在我的应用程序中,我有一个代表“系统”的Singleton。此对象首先创建并在最后销毁,所有其他对象应由它管理,这意味着,每个对象将由此系统对象直接或间接创建(在其run中方法)并且(希望)在系统不可用之前删除所有对象。

问题是对系统对象的可选访问(通过指针)引入了大量非空检查代码。我知道解除引用空指针会导致未定义的行为。使用好老C - assert我能做的最好吗?

// this is mostly pseudo-code, but should compile if 
// there is an appropriate MySys header
#include "MySys.h"

class MySysImplementation: public MySys 
{
public:
    MySysImplementation();
    void run();
    // ...
};

/// pointing to system object during runtime
namespace {
MySys* g_sys = NULL;
}

MySys& MySys::Instance()
{
    assert(g_sys);
    return *g_sys;
}

int main()
{
    {
        MySysImplementation sys;
        g_sys = &sys;
        sys.run();
    } // <---------- HERE all application-specific objects should be gone
    g_sys = NULL; // in real code this RAII-ensured
}

4 个答案:

答案 0 :(得分:2)

  

问题是可选的系统对象引入了许多非空检查代码。

考虑用空对象替换它:

class MySys {
    virtual void some_op() = 0;
    // other operations here
};

class NullSystem: public MySys {
    void some_op() {} // no-op implementations for all APIs
};

class YourSystem: public MySys {
    void some_op() { /* your non-optional implementation here */ }
};
  

我知道解除引用空指针会导致未定义的行为。使用好的旧C-assert我能做的最好?

首先,不要对实例使用全局,也不要使用单例访问点(您的MySys::Instance())。单例模式是一种反模式(它主要表示不做什么)。

考虑在main中实例化一个对象并通过引用传递它(也就是说,使用依赖注入而不是单例):

class SomeClientClass
{
    SomeClientClass(MySys const & system);
};

int main(int, char**)
{
    auto system = std::make_unique<YourSystem>();

    auto someClientCode = SomeClientClass(*system);
    // ... 

    // example where you want a SomeClientClass instance without system:
    auto client2 = SomeClientClass{ NullSystem{} };
}

答案 1 :(得分:1)

您可能只需执行以下操作:

MySys& MySys::Instance()
{
    static MySysImplementation instance;
    return instance;
}

答案 2 :(得分:1)

虽然可以在发布模式中删除断言,但最好不要抛出异常。

您的程序可能会因为空指针无异常而崩溃,但这是未定义的行为,可能会产生所有可能的副作用。

异常还具有以下优点:您创建的对象得到清理(堆栈展开),如果您有一部分代码可以正确处理,您还有机会捕获异常并处理它有这个。

你可以扔任何你喜欢的东西,但建议抛出任何来自std::exception的东西。 std::runtime_error在这里似乎很好。或者自己得出一些东西。

有一点需要注意:

因为您正在处理全局对象(单例也是全局对象),所以应该避免在析构函数中使用单例。当抛出异常而你是“堆栈展开”并且其中一个对象析构函数会抛出第二个异常时,程序将立即调用std::terminate。如果你终止这可能不是问题,但是如果你想在某个地方捕获异常,或者你依赖堆栈展开,这可能会导致问题。

答案 3 :(得分:1)

  

使用好的旧C-assert我能做的最好?

这是一个选择。它是否最好取决于使用空指针时程序需要做什么。

断言有两个方面:

  1. 有条件地禁用它,具体取决于NDEBUG宏 - 通常在发布版本中启用。如果您想无条件地检测空指针,if语句是更好的选择。

  2. 它调用std::abort来终止进程。如果能够以更优雅的方式解决问题,那么断言不是您的最佳选择。相反,您可以抛出异常,并在可以处理它的调用堆栈中将其捕获更高。

  3. 如果无论如何都无法有意义地处理异常,并且如果您希望避免发布版本中的检查的运行时成本,那么断言确实是最好的。

    但是,如果可能,您可以通过使用静态实例来保证对象确实存在。请考虑以下事项:

    MySys& MySys::Instance()
    {
        static MySysImplementation sys;
        return sys;
    }
    

    sys保证在任何对象之前和之后存在,在其构造函数中调用MySys::Instance - 或者其结构保证在MySysImplementation构造之后。< / p>