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

Posted

技术标签:

【中文标题】提供可选参数的现代 C++ 方法【英文标题】:Modern C++ approach for providing optional arguments 【发布时间】:2021-02-03 04:29:03 【问题描述】:

让我们来看看下面的函数声明:

void print(SomeType const* i);

这里,参数iconst* 属性表明该参数的意图是可选,因为它可能是nullptr。如果这不是故意的,那么参数将只是一个const&。传达可选-语义当然不是设计指针的初衷,但使用它们来这样做恰好可以很好地工作很长时间。

现在,由于在现代 C++ 中通常不鼓励使用原始指针(应该避免使用 std::unique_ptrstd::shared_ptr 以准确指示特定的所有权-语义),我想知道如何正确指示函数参数的可选-语义不按值传递,即。 e. 复制,作为

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

会的。

想了一会儿,我想出了使用的想法:

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

这实际上是最准确的。但事实证明std::optionalcannot have reference types.¹

另外,使用

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

绝不会是最优的,从那时起我们将要求我们的SomeType 存在于调用者端的std::optional 中,同样可能(或而是可能)需要一份副本那里

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


¹:具有讽刺意味的是,std::optional 不能有引用类型的描述原因(关于重新绑定或转发分配的争议)不适用于 const 引用的std::optionals 的情况,因为他们不能被分配到。

【问题讨论】:

如果必须尝试optional&lt; reference_wrapper&lt;T&gt; &gt; 您的意思是std::shared_ptr 而不是std::smart_ptr "在现代 C++ 中通常不鼓励使用原始指针" 不同意。不鼓励拥有原始指针,但非拥有指针通常没问题 还有std::optional&lt;std::cref&lt; SomeType&gt;&gt;,但我只是使用SomeType*作为可选参数。 @MooingDuck 我想你的意思是std::optional&lt;std::reference_wrapper&lt;const SomeType&gt;&gt;,不过。 std::crefis just the helper function. 【参考方案1】:

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

【讨论】:

确实,在现代代码库中使用原始指针在某种意义上比以往任何时候都更加清晰 - 在过去,这可能意味着生命周期,但在现代 C++ 中,您知道情况并非如此,因为您会使用了 RAII 包装器(例如 unique_ptr 转移所有权或 shared_ptr 在函数返回后保持生命周期)。正如你所建议的,关于生命周期的评论仍然不会受到伤害。 仍有observer_ptr 以避免与我们不知道它是否是旧用法的原始指针混淆。 指针的另一个问题(原始的,有时也是智能的)是知道它是一个数组还是对单个元素的引用。 我几乎总是接受 std::span&lt;Sometype&gt; 参数,而不是数组地址 + 长度的一对参数。 还可以通过将原始指针版本设为私有并向公众公开两个重载来增加清晰度,一个使用指定类型的引用/智能指针,一个不带任何参数,不会混淆外部用户了解所有权,并且很明显它是可选的。【参考方案2】:

函数重载在这里提供了一个干净的解决方案吗?例如。声明函数的 const ref 和空参数列表版本? 这可能取决于函数体在无参数/空情况下的作用 - 以及如何管理这两个实现以最大限度地减少代码重叠。

【讨论】:

在简单的情况下,当然可以。但是,我的目标是找到一个通用的解决方案(另请参阅 this 评论)。【参考方案3】:

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

话虽如此,boost::optional 确实允许您使用引用可选和 const 引用可选。已决定不在 std 库中提供此功能(原因我在此省略)。

【讨论】:

【参考方案4】:

这实际上就是 std::reference_wrapper 的用途。另请参阅Does it make sense to combine optional with reference_wrapper?,了解有关何时使用它以及何时不使用它的更多理由。

【讨论】:

【参考方案5】:

这里,const* 参数 i 的性质暗示了意图,参数是可选的,因为它可能是 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_i42;
    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;

或一些类似的变体,具体取决于您的实现应该做什么,具体取决于可选参数的存在/不存在。


否则,可选引用基本上是原始指针,也可以使用后者(如果重载不适用)。

【讨论】:

在简单函数的情况下(我的稀疏示例诚然暗示了这一点),我当然认为重载是一个合适的解决方案。然而,在一般情况下,可选参数只能用于更复杂函数中的单个案例区分,使得(几乎相似)重载有些笨拙。同样在一般情况下,可能有多个可选参数,它们可以相互独立地指定。

以上是关于提供可选参数的现代 C++ 方法的主要内容,如果未能解决你的问题,请参考以下文章

在CLI函数中忽略可选参数

C++ 宏的可选参数

C++ - 通过引用的可选参数

Java 可选参数

从 C++ 调用带有可选参数的 Fortran 子例程

从 C++ 调用带有可选参数的 Fortran 子例程