【问题标题】:How to prevent converting a null pointer into a reference如何防止将空指针转换为引用
【发布时间】:2017-03-11 09:07:49
【问题描述】:

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

问题是对 system 对象的可选访问(通过指针)引入了大量非空检查代码。我知道取消引用空指针会导致未定义的行为。使用旧的 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
}

【问题讨论】:

  • 你也可以抛出异常。在发布模式下可以删除断言。在发布模式下会抛出异常,你可以在某个地方捕获它并在有意义的情况下处理问题。如果不是,您的程序只是终止而没有在无效内存上运行,并且可能格式化您的 PC
  • 您是否考虑过将其保留为静态变量?它会将清理工作移出main,但也许你可以接受。
  • @Hayt 好点,是否有一个异常类可以用于此目的?
  • @krzaq 我更喜欢在 main 中完全控制以逃避 静态初始化噩梦
  • 我不明白它如何既是可选的又是“先创建后销毁”。

标签: c++ pointers reference singleton


【解决方案1】:

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

考虑用空对象替换它:

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 断言是我能做的最好的吗?

首先,不要对实例使用全局变量,也不要使用单例访问点(您的 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{} };
}

【讨论】:

  • 传递是我的实际问题,所以我决定接受“全局”对象的事实上的可用性。
  • 为了可用性,只需将其注入到使用它的代码中,作为参数:对于对象,在对象的构造函数中传递一个引用;对于函数,只需将其添加为参数。传递一个额外的参数比声明一个全局变量更有优势。
【解决方案2】:

虽然可以在发布模式下删除断言,但最好还是抛出异常。

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

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

你可以扔任何你喜欢的东西,但建议扔任何源自std::exception的东西。 std::runtime_error 在这里看起来不错。或者自己推导出一些东西。

有一点需要注意:

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

【讨论】:

  • avoid using the singleton in a destructor -- 很好,我确实计划提供可选访问(即可以检查的指针)。
  • 目前已接受,因为专注于我的问题的核心。
【解决方案3】:

您可以简单地执行以下操作:

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

【讨论】:

  • 谢谢。但这对于控制非平凡的清理是有问题的。
  • clarified the question 表明我的意思。
【解决方案4】:

我能做到最好吗?

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

assert有两个方面:

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

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

如果您无论如何都无法有意义地处理异常,并且如果您希望避免在发布构建中检查的运行时成本,那么您可以做的最好的事情就是断言。


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

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

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

【讨论】:

  • 首先,断言可能真的是个坏主意。其次,由于清理工作很重要,因此在我的情况下,静态变量方法不是一种选择。
  • @Wolf 好的,我在建议中添加了“如果可能”资格。非平凡的清理是什么意思?
猜你喜欢
  • 1970-01-01
  • 2012-10-24
  • 1970-01-01
  • 1970-01-01
  • 2012-04-27
  • 1970-01-01
  • 2021-06-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多