避免空指针并保持多态性

时间:2010-12-11 11:26:23

标签: c++ polymorphism

在我的代码中,我注意到我经常需要检查nullptr,即使nullptr不可能(根据指定的要求)。

然而,nullptr可能仍然会发生,因为其他人可能会发送nullptr认为这是可以的(不幸的是并非所有人都读/写规范),并且除非在测试期间在运行时触发问题(并且高),否则无法捕获此缺陷测试覆盖率很高)。因此,它可能会导致客户报告许多发布后的错误。

e.g。

class data
{
     virtual void foo() = 0;
};

class data_a : public data
{
public:
     virtual  void foo(){}
};

class data_b : public data
{
public:
     virtual void foo(){}
};

void foo(const std::shared_ptr<data>& data)
{
    if(data == nullptr) // good idea to check before use, performance and forgetting check might be a problem?
        return;
    data->foo();
}

通常我会简单地使用值类型并通过引用传递和复制。但是,在某些情况下,我需要需要指针或引用的多态性。

所以我开始使用以下“编译时多态”。

class data_a
{
public:
     void foo(){}
private:
     struct implementation;
     std::shared_ptr<implementation> impl_; // pimpl-idiom, cheap shallow copy
};

class data_b
{
public:
     void foo(){}
private:
     struct implementation;
     std::shared_ptr<implementation> impl_; // pimpl-idiom, cheap shallow copy
};

class data
{
public:
     data(const data_a& x) : data_(x){} // implicit conversion
     data(const data_b& x) : data_(x){} // implicit conversion
     void foo()
     {
          boost::apply(foo_visitor(), data_);
     }
private:
     struct foo_visitor : public boost::static_visitor<void>
     {
          template<typename T>
          void operator()(T& x){ x.foo(); }       
     };

     boost::variant<data_a, data_b> data_;
}

void foo(const data& data)
{
   data.foo();
}

在实际可行时,还有其他人认为这是个好主意吗?或者我错过了什么?这种做法有任何潜在的问题吗?

编辑:

使用引用的“问题”是您无法移动引用的所有权(例如,返回对象)。

data& create_data() { data_a temp; return temp; } // ouch... cannot return temp;

rvalue引用的问题(多态与rvalue引用有效吗?)然后就变成了你不能共享所有权。

data&& create_data() { return std::move(my_data_); } // bye bye data        

基于shared_ptr的“安全”指针确实听起来不错,但我仍然想要一个解决方案,在编译时强制执行非null,可能无法实现。

4 个答案:

答案 0 :(得分:4)

您始终可以使用null object pattern和参考。你不能传递(好吧你可以,但这是用户的错误)一个空引用。

答案 1 :(得分:3)

我个人更喜欢在类型中编码null可能性,因此使用boost::optional

  • 创建一个data_holder类,它始终拥有data(但允许多态)
  • 根据data_holder(非空)或boost::optional<data_holdder>
  • 定义界面

这样很清楚它是否可以为空。

现在,困难的部分是让data_holder永远不要保留空指针。如果使用data_holder(data*)形式的构造函数定义它,那么构造函数可能会抛出。

另一方面,它可以简单地采用一些参数,并将实际构造推迟到Factory(使用Virtual Constructor Idiom)。你仍然检查工厂的结果(并在必要时抛出),但你只有一个地方可以检查(工厂),而不是每一个施工点。

您也可以检查boost::make_shared,以查看参与转发的操作。如果你有C ++ 0x,那么你可以有效地参数转发并得到:

template <typename Derived>
data_holder(): impl(new Derived()) {}

// Other constructors for forwarding

不要忘记将默认构造函数(非模板)声明为私有(而不是定义它)以避免意外调用。

答案 2 :(得分:2)

non-null pointer是一个众所周知的概念,例如用于C的安全子集。是的,这可能是有利的。

而且,您应该使用智能指针。根据您的使用情况,您可能希望从类似于boost::shared_ptrtr1::unique_ptr的内容开始。但是,assertoperator*operator->上只有get()非空的内容,而是抛出异常。

ETA:忘了这一点。虽然我认为这种通用方法很有用(没有未定义的行为等等),但它并没有为您提供非空的编译时检查,这可能是您想要的。为此,您将在整个代码中普遍使用此类非空指针,如果没有语言支持,这仍然会泄漏。

答案 3 :(得分:1)

通常,当您使用上面概述的虚函数时,这是因为您不想知道实现所需接口的所有类。在您的示例代码中,您将枚举实现(概念)接口的所有类。当您必须随着时间的推移添加或删除实现时,这会变得令人沮丧。

您的方法也会干扰管理依赖项,因为您的类数据依赖于所有类,而使用多态性可以更容易地限制对特定类的依赖性。使用pimpl习语减轻了这一点,但我一直认为pimpl习语很烦人(因为你有两个类必须保持同步才能代表一个概念)。

使用引用或检查智能指针似乎是一种更简单,更清晰的解决方案。其他人已经在评论这些,所以我现在不会详细说明。