编写更通用的指针代码

时间:2016-12-20 01:19:39

标签: c++ c++11 smart-pointers

假设我想编写一个接收指针的函数。但是我想允许调用者使用裸指针或智能指针 - 无论他们喜欢什么。这应该是好的,因为我的代码应该依赖于指针语义,而不是指针实际实现的方式。这是一种方法:

template<typename MyPtr>
void doSomething(MyPtr p)
{
    //store pointer for later use
    this->var1 = p;

    //do something here
}

上面将使用duck typing,可以传递裸指针或智能指针。当传递的值是基指针时我们需要查看是否可以转换为派生类型。

template<typename BasePtr, typename DerivedPtr>
void doSomething(BasePtr b)
{
    auto d = dynamic_cast<DerivedPtr>(b);
    if (d) {
        this->var1 = d;

        //do some more things here
    }
}

上面的代码适用于原始指针但不适用于智能指针,因为我需要使用dynamic_pointer_cast代替dynamic_cast

上述问题的一个解决方案是我添加了一个新的实用工具方法,类似于universal_dynamic_cast,它可以通过使用std::enable_if选择重载版本来处理原始指针和智能指针。

我的问题是,

  1. 添加所有这些复杂性是否有价值,因此代码支持原始指针和智能指针?或者我们应该在我们的库公共API中使用shared_ptr吗?我知道这取决于库的目的,但是在整个API签名中使用shared_ptr的一般感觉是什么?假设我们只需要支持C ++ 11。
  2. 为什么STL没有内置的指针强制转换,无论你是否传递原始指针或智能指针?这是故意的STL设计师还是只是疏忽?
  3. 上述方法中的另一个问题是智能感知的丢失和可读性的缺失。这显然是所有鸭类型代码中的问题。但是,在C ++中,我们有选择。我可以很容易地在上面强调我的论点,如shared_ptr<MyBase>,这会牺牲调用者的灵活性来传递包含在任何指针中的任何内容,但是我的代码的读者会更自信,并且可以在应该进入的内容上构建更好的模型。在C ++公共库API中,是否存在这样或那样的一般偏好/优势?
  4. 我在其他SO回答中看到了另一种方法,其中作者建议您只使用template<typename T>并让调用者决定T是否为某种指针类型或引用或类。如果我必须在T中调用某些东西,这种超级通用方法显然不起作用,因为C ++需要解除引用指针类型,这意味着我可能create utility method like universal_deref使用std::enable_if将*运算符应用于指针类型但是对普通物体没有任何作用。我想知道是否有任何设计模式可以更容易地实现这种超级通用方法。最重要的是,是否值得去解决所有这些问题,或者只是保持简单并在任何地方使用shared_ptr

5 个答案:

答案 0 :(得分:4)

在类中存储shared_ptr具有语义含义。这意味着该类现在声称拥有该对象的所有权:其破坏的责任。对于shared_ptr,您可能会与其他代码共享此责任。

存放裸T* ......好吧,没有明确的含义。核心C ++指南告诉我们,裸指针不应该用于表示对象所有权,但其他人做不同的事情。

根据核心指南,您所谈论的是根据用户如何调用对象,可能会或可能不会声明对象的所有权。我会说你的界面非常混乱。所有权语义通常是代码基本结构的一部分。一个功能要么拥有所有权,要么不拥有;它根据被调用的地点而不是确定的东西。

但是,有时(通常出于优化原因)您可能需要这些。你可能有一个对象,在一个实例中给予内存的所有权,而在另一个实例中则没有。这通常会产生字符串,其中一些用户将分配一个你应该清理的字符串,而其他用户将从静态数据中获取字符串(如文字),所以你不要清理它。

在这些情况下,我会说你应该开发一个具有这种特定语义的智能指针类型。它可以由shared_ptr<T>T*构建。在内部,如果您无法访问variant<shared_ptr<T>, T*>,则可能会使用variant或类似类型。

然后你可以给它自己的dynamic/static/reinterpret/const_pointer_cast函数,根据内部variant的状态,根据需要转发操作。

或者,shared_ptr个实例可以被赋予一个不执行任何操作的删除对象。因此,如果您的界面只使用shared_ptr,则用户可以选择传递技术上并非真正拥有的对象。

答案 1 :(得分:2)

通常的解决方案是

template<typename T>
void doSomething(T& p)
{
    //store reference for later use
    this->var1 = &p;
}

这将我内部使用的类型与调用者使用的表示分离。是的,这是一个终身问题,但这是不可避免的。我不能在我的调用者上强制执行生命周期策略,同时接受任何指针。如果我想确保对象保持活动状态,我必须将界面更改为std::shared_ptr<T>

答案 2 :(得分:1)

我认为您想要的解决方案是强制函数的调用者传递常规指针而不是使用模板函数。使用shared_ptrs是一个很好的做法,但是在传递堆栈时没有任何好处,因为对象已经由函数的调用者保存在共享指针中,保证它不会被破坏,并且你的函数不是真的&#34;坚持&#34;对象。存储为成员时(或实例化将存储在成员中的对象时)使用shared_ptrs,但在作为参数传递时不使用。无论如何,调用者从shared_ptr获取原始指针应该是一件简单的事情。

答案 3 :(得分:1)

智能指针的目的

智能指针的目的是管理内存资源。当您拥有智能指针时,您通常会声明唯一或共享所有权。另一方面,原始指针只指向一些由其他人管理的内存。将原始指针作为函数参数基本上告诉调用者函数该函数不关心内存管理。它可以是堆栈内存或堆内存。不要紧。它只需要比函数调用的生命周期更长。

指针参数的语义

unique_ptr传递给函数(按值),然后将责任传递给清理该函数的内存。将shared_ptrweak_ptr传递给函数时,那就是说“我可能会与它所属的函数或对象共享内存所有权”。这与传递一个原始指针完全不同,后者隐含地表示“这是一个指针。你可以访问它直到你返回(除非另有说明)”。

结论

如果你有一个功能,那么你通常知道你拥有哪种所有权语义,98%的时候你不关心所有权,如果你知道这些,你应该只关注原始指针甚至只是引用无论如何,你传递的指针不是nullptr。具有智能指针的呼叫者可以使用p.get()成员函数或&*p,如果他们想要更简洁。因此,我不建议使用模板代码来解决您的问题,因为原始指针可以为调用者提供所有灵活性。避免模​​板还允许您将实现放入实现文件(而不是头文件)。

回答你的具体问题:

  1. 我没有看到增加这种复杂性的重要意义。相反:它不必要地使您的代码复杂化。

  2. 几乎没有必要这样做。即使您使用std::dynamic_pointer_cast,也要以某种方式维护所有权。但是,对此的充分使用是罕见的,因为大多数时候只需使用dynamic_cast<U*>(ptr.get())即可。这样就可以避免共享所有权管理的开销。

  3. 我的偏好是:使用原始指针。你获得了所有的灵活性,智能感知等等,你将永远幸福地生活。

  4. 我宁愿称之为反模式 - 一种不应该使用的模式。如果你想成为泛型,那么使用原始指针(如果它们可以为空)或引用,如果指针参数永远不是nullptr。这为呼叫者提供了所有灵活性,同时保持界面清洁和简单。

  5. 进一步阅读: Herb Sutter在他的Guru of the Week #91.中将智能指针作为功能参数进行了讨论。他在那里深入解释了这个话题。特别是第3点可能对你很有意思。

答案 4 :(得分:0)

在回顾了更多材料之后,我终于决定在我的公共界面中使用普通的旧原始指针。这是推理:

  1. 我们不应该设计界面以适应其他人的糟糕设计决策。 “避免像瘟疫这样的原始指针并用智能指针替换它们”的口号只是bad advice(也是Shutter's GoTW)。试图支持这些错误的决定将它们传播到您自己的代码中。
  2. 原始指针明确设置与调用者的合约,他们是需要担心输入生命周期的人。
  3. 原始指针为拥有shared_ptr,unique_ptr或只是原始指针的调用者提供了最大的灵活性。
  4. 代码现在看起来更具可读性,直观性和合理性,不像那些接管无处不在的鸭子模板。
  5. 我得到了强有力的打字以及智能感知和更好的编译时间检查。
  6. 在每个演员阵容中创建智能指针的新实例时,可以轻而易举地逐步强制执行层次结构,并且不必担心性能影响。
  7. 在内部传递指针时,我不必仔细关心指针是否为shared_ptr或原始指针。
  8. 虽然我不关心它,但是有更好的途径来支持旧的编译器。
  9. 简而言之,尝试容纳那些已经采用从不使用原始指针并用智能指针替换它们的指导方针的潜在客户会导致我的代码污染不必要的复杂性。因此,除非明确想要所有权,否则请保持简单的事情并使用原始指针。