【问题标题】:Writing more general pointer code编写更通用的指针代码
【发布时间】:2016-12-20 01:19:39
【问题描述】:

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

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

    //do something here
}

上面将使用鸭子类型,可以传递裸指针或智能指针。当传递的值是基指针时会出现问题,我们需要看看我们是否可以转换为派生类型。

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&lt;MyBase&gt;,这会牺牲调用者传递任何指针中包含的任何内容的灵活性,但我的代码的读者会更有信心,并且可以在应该输入的内容上建立更好的模型。在 C++ 中公共图书馆 API,是否存在一种或另一种一般偏好/优势?
  4. 我在其他 SO 答案中看到了另一种方法,作者建议您应该只使用 template&lt;typename T&gt; 并让调用者决定 T 是某种指针类型、引用还是类。如果我必须在 T 中调用某些东西,这种超级通用方法显然不起作用,因为 C++ 需要取消引用指针类型,这意味着我可能必须使用 create utility method like universal_deref 将 * 运算符应用于指针类型但对普通对象没有任何作用。我想知道是否有任何设计模式可以更轻松地实现这种超级通用方法。再一次,最重要的是,是否值得解决所有这些麻烦,或者只是保持简单并在任何地方使用shared_ptr

【问题讨论】:

  • (*p)-&gt;someMember() 取消引用太多了。我想你的意思是p-&gt;someMember(),或者,如果出于某种原因你想更详细一点,(*p).someMember()
  • @IgorTandetnik - 哎呀,对不起。固定。
  • 似乎设计了很多复杂性只是为了引起混乱。让您的 API 采用一种已知类型并坚持使用它!
  • 您通常可以使用&amp;*bstd::addressof(*b) 来获取底层原始指针,以更加迂腐。这至少适用于原始指针、标准智能指针和迭代器。然后 dynamic_cast 指向你内心深处的原始指针。
  • 你一直在使用“通用”这个词,但你显然只有原始指针和std::shared_ptr。例如。我看不出你假设的 universal_dynamic_cast 将如何与 std::unique_ptr 一起工作,在这种情况下,你不能有两个实例持有指向同一个对象的指针。

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


【解决方案1】:

在类中存储shared_ptr 具有语义意义。这意味着该类现在正在声明该对象的所有权:销毁它的责任。在 shared_ptr 的情况下,您可能会与其他代码分担该责任。

存储一个赤裸裸的T*...好吧,这没有明确的意义。 Core C++ Guidelines 告诉我们,不应该使用裸指针来表示对象所有权,但其他人会做不同的事情。

根据核心准则,您所说的函数可能会或可能不会根据用户调用对象的方式声明对对象的所有权。我会说你有一个非常混乱的界面。所有权语义通常是代码基本结构的一部分。一个函数要么取得所有权,要么不取得所有权;它不是根据调用位置来确定的。

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

在这些情况下,我会说您应该开发一种具有这种特定语义的智能指针类型。它可以从shared_ptr&lt;T&gt;T* 构造。在内部,如果您无权访问 variant,它可能会使用 variant&lt;shared_ptr&lt;T&gt;, T*&gt; 或类似类型。

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

另外,shared_ptr 实例可以被赋予一个不执行任何操作的删除器对象。所以如果你的界面只使用shared_ptr,用户可以选择传递一个它在技术上并不真正拥有的对象。

【讨论】:

  • 您确实在这里带来了一些清晰度,但问题是您无法真正判断来电者是否总是希望您承担所有权。一些简单的调用者可能正在使用 T* 并且不想为您构造 shared_ptr 而其他调用者可能完全使用 shared_ptr ,因为他们不想参与跟踪所有权的业务。对于类可以在各种应用程序中使用的库开发人员来说,这种困境更多。我见过一些应用程序,它们的指导方针是永远不要在任何地方使用裸指针。虽然一些简单的小应用程序一直都在这样做。
  • 所以我想说的是:应用程序使用两种所有权模型:一种是中心化所有者,将 T* 分发给其他所有人,另一种是分散模型,其中仅传递 shared_ptr ,禁止循环引用,避免像瘟疫一样避免裸指针。作为库开发人员,您不知道调用应用程序将使用什么模型。所以开发这个通用接口有一些争论。
  • @ShitalShah:“其他调用者可能完全使用 shared_ptr,因为他们不想从事跟踪所有权的业务”这不是 shared_ptr做。 shared_ptr 和垃圾回收不是一回事;它不允许您忽略所有权关系。我不想帮助用户做出糟糕的编码决定。 “作为库开发人员,您不知道调用应用程序将使用什么模型。”作为库开发人员,您必须根据库的正在做什么来做出决定 .声明所有权与否是其中的一部分。
【解决方案2】:

通常的解决方案是

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

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 我想我在这里尝试了太多来简化场景。假设我们正在设计类接口,方法可以存储传递给成员变量的指针。
    • @ShitalShah:那么那些“类接口和方法”需要知道他们存储的什么样的指针。存储unique_ptr 的界面与存储shared_ptr 的界面看起来会有很大不同。甚至这将与存储T* 的不同。您不应该尝试设计这样的界面,它们对它们存储的内容视而不见。
    • @ShitalShah 您可能会考虑仅在存储到成员中的函数中使用 shared_ptrs 。在不了解更多信息的情况下,我认为您可能还会考虑是否真的需要共享对象的所有权,或者您是否可以进行设计以便不需要共享所有权。 shared_ptrs 是一个很棒的工具,但并不总是(根据我的经验通常不是)正确的工具。
    • @NicolBolas 我认为 T* 和 shared_ptr 共享相同的语义,因此应该可以互换。我同意 unique_ptr 需要不同的界面。对于不保存指针的方法, T* 可能就足够了,这是一个简单的场景,但是我的问题更多是关于我们如何概括 T* 和 shared_ptr 假设我们将它存储在类中。使用 shared_ptr 点缀公共库类接口是否可以接受?如果在一般情况下,调用者总是会取得所有权,你显然不能总是打赌。
    • @JohnThoits 是的,问题是关于我们可能将指针存储在成员变量中的一般场景。我会添加这个问题。
    【解决方案4】:

    智能指针的用途

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

    指针参数的语义

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

    结论

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

    回答您的具体问题:

    1. 我认为增加这种复杂性没有多大价值。相反:它不必要地使您的代码复杂化。

    2. 几乎不需要这个。即使您在其中使用std::dynamic_pointer_cast,也是以某种方式维护所有权。但是,很少能充分使用它,因为大多数时候只需要使用dynamic_cast&lt;U*&gt;(ptr.get()) 即可。这样您就可以避免共享所有权管理的开销。

    3. 我的偏好是:使用原始指针。你得到了所有的灵活性,智能等等,你会从此过上幸福的生活。

    4. 我宁愿称其为反模式 - 一种不应使用的模式。如果您想要通用,则使用原始指针(如果它们可以为空)或引用,如果指针参数永远不会是 nullptr。这为调用者提供了所有灵活性,同时保持界面简洁明了。

    进一步阅读: Herb Sutter 在他的Guru of the Week #91. 中谈到了智能指针作为函数参数,他在那里深入解释了这个话题。特别是第 3 点可能会让您感兴趣。

    【讨论】:

      【解决方案5】:

      在查看了更多材料后,我最终决定在我的公共界面中使用普通的旧原始指针。原因如下:

      1. 我们不应该设计界面来适应其他人的错误设计决策。 “避免像瘟疫一样的原始指针,并在任何地方用智能指针替换它们”的口头禅只是bad advice(也请参见Shutter's GoTW)。试图支持这些错误的决定会将它们传播到您自己的代码中。
      2. 原始指针明确地与调用者建立契约,即他们需要担心输入的生命周期。
      3. 原始指针为具有 shared_ptr、unique_ptr 或仅原始指针的调用者提供了最大的灵活性。
      4. 与那些无处不在的鸭子类型模板不同,现在的代码看起来更易读、更直观、更合理。
      5. 我恢复了强类型以及智能感知和更好的编译时间检查。
      6. 向上和向下转换层次结构轻而易举,不必担心每次转换时可能会创建新的智能指针实例的性能影响。
      7. 在内部传递指针时,我不必仔细关心指针是 shared_ptr 还是原始指针。
      8. 虽然我不关心它,但有更好的途径来支持旧编译器。

      简而言之,试图满足那些接受了从不使用原始指针的指导方针的潜在客户并在任何地方用智能指针替换它们会导致我的代码受到不必要的复杂性污染。所以保持简单的事情简单,只使用原始指针,除非你明确想要所有权。

      【讨论】:

      • "向上和向下转换层次结构轻而易举,不必担心每次转换时可能会创建新的智能指针实例的性能影响。" 考虑到dynamic_cast 的成本,我认为创建新的 shared_ptr 无法与之相比。确实,我不得不质疑为什么你需要频繁地“上下层级”。
      • 我有存储为 Base* 的多态对象容器。对这些对象采取的操作取决于它们支持的层次结构中的接口级别。例如,您可以拥有 Shape* 的容器,实际对象可能是 Rectangle、Square(派生自 Rectangle)、Oval、Circle(派生自 Oval)。然后你可能有方法使所有椭圆都变成绿色。
      • 现在你是violating OOP principles。如果您有一个Base*,那么您选择了不在乎,如果它是它的任何派生类。您是说从Base* 派生的任何类都可以被该接口充分使用。如果你有一个Bases 的容器,那么你应该将你的操作限制在Bases 可以做的事情上。 dynamic_cast 不应该是你的代码的共同元素;它应该只在例外的情况下使用。考虑these answers
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-05
      • 2015-07-01
      • 1970-01-01
      • 2013-03-09
      • 1970-01-01
      • 1970-01-01
      • 2014-12-10
      相关资源
      最近更新 更多