【问题标题】:Default argument vs overloads in C++C ++中的默认参数与重载
【发布时间】:2018-07-28 17:27:12
【问题描述】:

例如,而不是

void shared_ptr::reset() noexcept;
template <typename Y>
void shared_ptr::reset(Y* ptr);

可能会想到

template <typename Y = T>
void shared_ptr::reset(Y* ptr = nullptr);

我认为这里的性能差异可以忽略不计,第二个版本更简洁。 C++ 标准采用第一种方式有什么具体原因吗?

same question 已被要求使用 Kotlin 语言,并且首选默认参数。

更新:

std::unique_ptr::reset() 遵循默认参数设计(请参阅here)。所以我认为std::shared_ptr::reset() 使用重载的原因是因为它们有不同的异常规范。

【问题讨论】:

  • 你知道,我不再那么确定我的(已删除)答案了。您提出的版本应该没问题(减去较弱的异常规范)。
  • 不知道 ISO 委员会的具体动机,但一般来说,具有不同的功能(无论是重载还是使用不同的名称)会导致程序流的结构更加静态。这本身就是一个优势;例如,您和您的 IDE 确切地知道每个版本的调用位置,这使得手动和自动代码分析更加容易。您还可以专门为一个版本或另一个版本设置调试断点。
  • 哦,我会厌倦在其他编程语言中应用任何 Kotlin 指南。 Kotlin 是一门非常年轻的语言(甚至不到 8 岁),在谷歌将它用于 Android 开发之前它的用户群很小,我想即使是今天的大多数用户也只是在 Android 前端开发的背景下才知道它。 Kotlin 确实看起来很有趣,但对于一种新的编程语言,并没有真正经过验证的指南,因为没有人可以尝试和测试过它们。与已经存在 33 年的 C++ 相比。

标签: c++ overloading language-lawyer api-design default-arguments


【解决方案1】:

关键的区别在于,这两个操作实际上在语义上并不相同。

第一个意思是让shared_ptr 没有托管对象。第二个是让指针管理 another 对象。这是一个重要的区别。在单个函数中实现它意味着我们基本上将让一个函数执行两种不同的操作。

此外,每个操作可能对所讨论的类型有不同的限制。如果我们将它们转储到一个函数中,那么“两个分支”将必须满足相同的约束,这是不必要的限制。 C++17 和 constexpr if 缓解了它,但这些函数是在退出之前指定的。

最终,我认为这个设计符合 Scott Meyers 的建议。如果默认参数让你做了一些语义不同的事情,它可能应该是另一个重载。


好的,现在来解决您的编辑问题。是的,例外规范不同。但是就像我之前提到的那样,它们可能不同的原因是函数在做不同的事情。 semantics of the reset members 需要这个:

void reset() noexcept;

效果:相当于shared_­ptr().swap(*this)

template<class Y> void reset(Y* p);

效果:相当于shared_­ptr(p).swap(*this)

那里没有什么大新闻。每个函数都具有使用给定参数(或缺少参数)构造一个新的shared_ptr 并进行交换的效果。那么shared_ptr 构造函数是做什么的呢?根据a preceding section,他们这样做:

constexpr shared_ptr() noexcept;

效果:构造一个空的 shared_ptr 对象。
后置条件use_­count() == 0 &amp;&amp; get() == nullptr

template<class Y> explicit shared_ptr(Y* p);

后置条件use_­count() == 1 &amp;&amp; get() == p抛出bad_­alloc,或者当无法获取内存以外的资源时,实现定义的异常

注意指针使用计数的不同后置条件。这意味着第二个重载需要考虑任何内部簿记。并且很可能会为它分配存储空间。两个重载的构造函数做不同的事情,就像我之前说的,这是将它们分成不同函数的强烈提示。能够获得更强的异常保证这一事实进一步证明了该设计选择的合理性。

最后,为什么unique_ptr 两个动作都只有一个重载?因为默认值不会改变语义。它只需要跟踪新的指针值。 value 为 null 的事实(来自默认参数或其他)不会彻底改变函数的行为。因此,单个过载是合理的。

【讨论】:

  • >> 如果默认参数让你做了一些语义不同的事情,它可能应该是另一个重载。很好。
  • 在所有的答案中,你最吸引我:)
  • 我相信我过去很自然地遵循了这个建议,但从未真正考虑过。实际上,明确地阅读它很有意义。我现在会更加意识到这一点 - 谢谢!
  • 我很困惑。 shared_ptr&lt;T&gt;::reset(static_cast&lt;T *&gt;(nullptr))shared_ptr&lt;T&gt;::reset() 之间有什么语义区别?第一个不合法吗?
  • @StoryTeller:哦,你是说即使指针是nullptr,它仍然会有控制块?!这真是令人难以置信。谢谢!
【解决方案2】:

如果您经常精确地重置为 nullptr 而不是新值,那么单独的函数 void shared_ptr::reset() noexcept; 将具有空间优势,因为您可以将一个函数用于所有类型 Y,而不是使用对每种Y 类型采用Y 类型的特定函数。另一个空间优势是没有参数的实现不需要将参数传递给函数。

当然,如果函数被多次调用,这都不重要。

异常行为也存在差异,这可能非常重要,我相信这就是为什么标准有多个声明此函数的动机。

【讨论】:

    【解决方案3】:

    虽然其他答案的设计选择都是有效的,但它们确实假设了一个在这里并不完全适用的东西:语义等价!

    void shared_ptr::reset() noexcept;
                          // ^^^^^^^^
    template <typename Y>
    void shared_ptr::reset(Y* ptr);
    

    第一个重载是noexcept,而第二个重载不是。没有办法根据参数的运行时值来决定noexcept-ness,所以需要不同的重载。

    关于不同noexcept 规范原因的一些背景信息: reset() 不会抛出,因为假设先前包含的对象的析构函数不会抛出。但是第二个重载可能还需要为共享指针状态分配一个新的控制块,如果分配失败,它将抛出std::bad_alloc。 (并且 resetting 到 nullptr 可以在不分配控制块的情况下完成。)

    【讨论】:

      【解决方案4】:

      重载和默认指针有根本的区别:

      • 重载是自包含的:库中的代码完全独立于调用上下文。
      • 默认参数不是自包含的,而是取决于调用上下文中使用的声明。它可以通过简单的声明在给定范围内重新定义(例如,不同的默认值,或不再有默认值。

      所以从语义上讲,默认值是嵌入在调用代码中的快捷方式,而重载是嵌入在被调用代码中的含义。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-05
        相关资源
        最近更新 更多