现代 C++ 提供可选参数的方法

34

让我们看以下函数声明:

void print(SomeType const* i);

在这里,参数iconst*特性表明了意图,即该参数是可选的,因为它可能为nullptr。如果这不是预期的,那么参数将是一个const&。显然,使用指针来传达可选语义并不是设计指针时最初的意图,但长期以来这种用法一直很有效。

现在,在现代C++中通常不建议使用原始指针(应该避免使用它们,而改用std::unique_ptrstd::shared_ptr来精确地指示特定的所有权语义),我想知道如何正确地指示函数参数的可选语义,而不需要通过值进行传递,即复制,例如

void print(std::optional<SomeType> i);

经过一段时间的思考,我想到了使用:

would do.

void print(std::optional<SomeType const&> i);

实际上这是最精确的。但事实证明,std::optional 不能使用引用类型。¹

此外,使用

void print(std::optional<SomeType> const& i);

如果这样做,并要求在调用方的 std :: optional 上存在我们的 SomeType ,那么将绝不是最佳选择,这可能需要在那里复制。

问题:那么允许可选参数而不复制的好的现代方法是什么?在现代C ++中,在这里使用原始指针仍然是合理的方法吗?


¹:具有讽刺意味的是,描绘为什么 std :: optional 不能具有引用类型的原因(关于重新绑定或传递赋值的争议)在 const 参考文献的 std :: optional 的情况下不适用,因为它们无法分配给。


2
如果必须这样做,请尝试使用 optional< reference_wrapper<T> > - underscore_d
针对您的注释:理论上,您实际上可以定义一个operator=(...) const,语言中没有任何阻止它的东西,尽管好的习惯应该是如此 :-) 但是即使忽略这一点,您也面临与非const引用相同的问题,重新绑定将与基于值的可选项发生的情况不一致,因此可能会令许多用户感到惊讶。 - Arthur Tacca
2
你是不是想用 std::shared_ptr 而不是 std::smart_ptr - David Z
4
使用原始指针在现代C++中通常不被鼓励。但是我不同意这种说法。在C++中,拥有所有权 的原始指针是不被鼓励的,但是_非拥有所有权_ 的指针通常是可以使用的。 - Mooing Duck
1
还有std::optional<std::cref<SomeType>>,但是我会只使用可选参数的SomeType* - Mooing Duck
1
@MooingDuck 我认为你的意思是 std::optional<std::reference_wrapper<const SomeType>>。而 std::cref 只是一个辅助函数。 - Reizo
5个回答

24

接受原始指针是完全可以的,在许多“现代”代码库中仍然这样做(我注意到这是一个快速移动的目标)。只需在函数上加上一条注释,说明它允许为空,并且在调用之后该函数是否保存了指针的副本(即指向值的生存期要求是什么)。


8
在现代的代码库中使用裸指针在某种程度上比以往更加清晰 - 在过去,这可能涉及到生命周期之类的问题,但在现代C++中,您知道这不是问题,因为您会使用RAII包装器(例如unique_ptr来转移所有权或shared_ptr在函数返回后保持生命周期活着)。正如您所建议的,关于生命周期的注释仍然有必要。 - Arthur Tacca
1
仍然有observer_ptr,以避免在我们不知道它是否是旧用法的情况下与原始指针混淆。 - Jarod42
1
指针(原始指针和有时也包括智能指针)的另一个问题是如何知道它是数组还是单个元素的引用。 - Jarod42
2
我通常会使用 std::span<Sometype> 参数,而不是数组地址和长度对作为参数。 - Daniel Schepler
1
可以通过将原始指针版本设为私有,并在公共接口中公开两个重载函数来增加代码的清晰度。其中一个使用指定类型的引用/智能指针,另一个不带任何参数。这样可以避免外部用户对所有权的混淆,同时明确表明这是可选的。 - Yanick Salzmann
显示剩余2条评论

6

函数重载能提供一个干净的解决方案吗?例如,声明一个既有const引用又有空参数列表版本的函数?
这可能取决于函数在无参数/空值情况下的具体实现方式,以及你如何管理这两个实现方式以最大程度地减少代码重叠。


在简单的情况下,当然可以。但是我旨在寻求一种通用的解决方案(也请参见评论)。 - Reizo

5

对于这种可选参数传递,原始指针通常是可以使用的,实际上是唯一可以使用原始指针的情况之一。这也是推荐的方式。

尽管如此,boost::optional确实允许您使用引用可选项和const引用可选项。决定不在std库中添加此功能(原因我不在此说明)。


2

0
在这里,参数iconst*属性表明了意图,即该参数是可选的,因为它可能为nullptr。

[...]

那么,有没有一种不需要复制就可以允许可选参数的好方法呢?

允许一个可选参数(不是std::optional的意义上,而是语义上的),并根据可选参数是否存在而有不同的实现变化,听起来像是重载的理想候选:

struct SomeType { int value; };

namespace detail {
    void my_print_impl(const SomeType& i) {
        std::cout << i.value;
    }    
}  // namespace detail

void my_print() {
    const SomeType default_i{42};
    detail::my_print_impl(default_i);
}

void my_print(const SomeType& i) { 
    detail::my_print_impl(i);
}

或者

namespace detail {
    void my_print_impl() {
        std::cout << "always print me\n";
    }    
}  // namespace detail

void my_print() {
    detail::my_print_impl();
}

void my_print(const SomeType& i) {
    detail::my_print_impl();
    std::cout << "have some type: " << i.value;
}

或者一些类似的变化,取决于您的实现应该根据可选参数的存在/不存在执行什么操作。


可选的引用,否则基本上就是原始指针,如果没有重载的情况下,后者同样可以使用。


2
对于简单函数(尽管我的稀疏示例表明如此),我确实认为重载是一个合适的解决方案。然而,在一般情况下,可选参数可能仅用于更复杂函数中的单个情况区分,使得(几乎相似的)重载有些笨拙。此外,在一般情况下,可能会有多个可选参数,这些参数可以独立指定。 - Reizo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接