C++ 中的默认参数与重载
Default argument vs overloads in C++
例如,而不是
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++ 标准走在前面有什么具体原因吗?
已被要求使用 Kotlin 语言,默认参数是首选。
更新:
std::unique_ptr::reset()
遵循默认参数设计(参见 here)。所以我认为 std::shared_ptr::reset()
使用重载的原因是因为它们有不同的异常规范。
如果您经常重置为精确 nullptr
而不是新值,那么单独的函数 void shared_ptr::reset() noexcept;
将具有 space 优势,因为您可以将一个函数用于所有类型 Y
,而不是有一个特定的函数,它为每种类型的 Y
采用一个 Y
类型。另一个 space 优点是没有参数的实现不需要将参数传递给函数。
当然,函数被多次调用也没关系
异常行为也有差异,这可能非常重要,我相信这就是为什么标准对此函数有多个声明的动机。
关键的区别在于,这两个操作实际上在语义上并不相同。
第一个是指离开 shared_ptr
没有托管对象。第二个是让指针管理 另一个 对象。这是一个重要的区别。在单个函数中实现它意味着我们基本上让一个函数执行两个不同的操作。
此外,每个操作可能对相关类型有不同的约束。如果我们将它们转储到一个函数中,那么 "both branches" 将必须满足相同的约束,这是不必要的限制。 C++17 和 constexpr if
减轻了它,但这些函数是在退出之前指定的。
最终,我认为这个设计符合Scott Meyers的建议。如果默认参数让你做一些语义上不同的事情,它可能应该是另一个重载。
好的,现在来解决您的编辑问题。是的,异常规范不同。但正如我之前提到的,原因它们可能不同,因为函数在做不同的事情。 semantics of the reset
members 要求:
void reset() noexcept;
Effects: Equivalent to shared_ptr().swap(*this)
.
template<class Y> void reset(Y* p);
Effects: Equivalent to shared_ptr(p).swap(*this)
.
那里不是什么大新闻。每个函数都具有使用给定参数(或缺少参数)构造新 shared_ptr
和交换的效果。那么 shared_ptr
构造函数是做什么的呢?根据 a preceding section,他们这样做:
constexpr shared_ptr() noexcept;
Effects: Constructs an empty shared_ptr object.
Postconditions: use_count() == 0 && get() == nullptr
.
template<class Y> explicit shared_ptr(Y* p);
Postconditions: use_count() == 1 && get() == p
.
Throws: bad_alloc
, or an implementation-defined exception when a resource other than memory could not be obtained
请注意指针使用计数的不同 post 条件。这意味着第二个重载需要考虑任何内部簿记。并且很可能为其分配存储空间。两个重载的构造函数做不同的事情,正如我之前所说,这是将它们分成不同函数的强烈暗示。可以获得更强的异常保证这一事实进一步证明了该设计选择的可靠性。
最后,为什么 unique_ptr
两个动作只有一个重载?因为默认值不会改变语义。它只需要跟踪新的指针值。 value 为 null 的事实(无论是来自默认参数还是其他),都不会彻底改变函数的行为。因此,单个重载是合理的。
重载和默认指针之间存在根本区别:
- 重载是自包含的:库中的代码完全独立于调用上下文。
- 默认参数不是自包含的,而是取决于调用上下文中使用的声明。它可以在给定的范围内通过简单的声明重新定义(例如,不同的默认值,或者不再有默认值。
所以从语义上来说,默认值是嵌入在调用代码中的一个short-cut,而重载是嵌入在被调用代码中的一个含义。
虽然其他答案的设计选择都是有效的,但它们确实假设了一件事在这里并不完全适用:语义等价!
void shared_ptr::reset() noexcept;
// ^^^^^^^^
template <typename Y>
void shared_ptr::reset(Y* ptr);
第一个重载是 noexcept
,而第二个重载不是。无法根据参数的 运行时值 来决定 noexcept
-ness,因此需要不同的重载。
关于不同 noexcept
规范原因的一些背景信息: reset()
不会抛出,因为假定先前包含的析构函数对象不抛出。但是第二个重载可能还需要为共享指针状态分配一个新的控制块,如果分配失败,它将抛出 std::bad_alloc
。 (并且 reset
可以在不分配控制块的情况下完成对 nullptr
的调整。)
例如,而不是
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++ 标准走在前面有什么具体原因吗?
更新:
std::unique_ptr::reset()
遵循默认参数设计(参见 here)。所以我认为 std::shared_ptr::reset()
使用重载的原因是因为它们有不同的异常规范。
如果您经常重置为精确 nullptr
而不是新值,那么单独的函数 void shared_ptr::reset() noexcept;
将具有 space 优势,因为您可以将一个函数用于所有类型 Y
,而不是有一个特定的函数,它为每种类型的 Y
采用一个 Y
类型。另一个 space 优点是没有参数的实现不需要将参数传递给函数。
当然,函数被多次调用也没关系
异常行为也有差异,这可能非常重要,我相信这就是为什么标准对此函数有多个声明的动机。
关键的区别在于,这两个操作实际上在语义上并不相同。
第一个是指离开 shared_ptr
没有托管对象。第二个是让指针管理 另一个 对象。这是一个重要的区别。在单个函数中实现它意味着我们基本上让一个函数执行两个不同的操作。
此外,每个操作可能对相关类型有不同的约束。如果我们将它们转储到一个函数中,那么 "both branches" 将必须满足相同的约束,这是不必要的限制。 C++17 和 constexpr if
减轻了它,但这些函数是在退出之前指定的。
最终,我认为这个设计符合Scott Meyers的建议。如果默认参数让你做一些语义上不同的事情,它可能应该是另一个重载。
好的,现在来解决您的编辑问题。是的,异常规范不同。但正如我之前提到的,原因它们可能不同,因为函数在做不同的事情。 semantics of the reset
members 要求:
void reset() noexcept;
Effects: Equivalent to
shared_ptr().swap(*this)
.template<class Y> void reset(Y* p);
Effects: Equivalent to
shared_ptr(p).swap(*this)
.
那里不是什么大新闻。每个函数都具有使用给定参数(或缺少参数)构造新 shared_ptr
和交换的效果。那么 shared_ptr
构造函数是做什么的呢?根据 a preceding section,他们这样做:
constexpr shared_ptr() noexcept;
Effects: Constructs an empty shared_ptr object.
Postconditions:use_count() == 0 && get() == nullptr
.template<class Y> explicit shared_ptr(Y* p);
Postconditions:
use_count() == 1 && get() == p
. Throws:bad_alloc
, or an implementation-defined exception when a resource other than memory could not be obtained
请注意指针使用计数的不同 post 条件。这意味着第二个重载需要考虑任何内部簿记。并且很可能为其分配存储空间。两个重载的构造函数做不同的事情,正如我之前所说,这是将它们分成不同函数的强烈暗示。可以获得更强的异常保证这一事实进一步证明了该设计选择的可靠性。
最后,为什么 unique_ptr
两个动作只有一个重载?因为默认值不会改变语义。它只需要跟踪新的指针值。 value 为 null 的事实(无论是来自默认参数还是其他),都不会彻底改变函数的行为。因此,单个重载是合理的。
重载和默认指针之间存在根本区别:
- 重载是自包含的:库中的代码完全独立于调用上下文。
- 默认参数不是自包含的,而是取决于调用上下文中使用的声明。它可以在给定的范围内通过简单的声明重新定义(例如,不同的默认值,或者不再有默认值。
所以从语义上来说,默认值是嵌入在调用代码中的一个short-cut,而重载是嵌入在被调用代码中的一个含义。
虽然其他答案的设计选择都是有效的,但它们确实假设了一件事在这里并不完全适用:语义等价!
void shared_ptr::reset() noexcept;
// ^^^^^^^^
template <typename Y>
void shared_ptr::reset(Y* ptr);
第一个重载是 noexcept
,而第二个重载不是。无法根据参数的 运行时值 来决定 noexcept
-ness,因此需要不同的重载。
关于不同 noexcept
规范原因的一些背景信息: reset()
不会抛出,因为假定先前包含的析构函数对象不抛出。但是第二个重载可能还需要为共享指针状态分配一个新的控制块,如果分配失败,它将抛出 std::bad_alloc
。 (并且 reset
可以在不分配控制块的情况下完成对 nullptr
的调整。)