使用 std::optional 来避免函数中的默认参数有什么好处吗?

moh*_*uje 10 c++ c++11 c++17 stdoptional

我正在将代码移植到 C++17,尝试尽可能使用新功能。我喜欢的一件事是使用std::optional在某些情况下可能会失败的函数中返回或不返回值的想法。

我很好奇这个新特性的可能用途,我正在考虑开始使用它来替换函数中的可选参数,所以:

void compute_something(int a, int b, const Object& c = Object(whatever)) {
   // ...
}
Run Code Online (Sandbox Code Playgroud)

变成:

void compute_something(int a, int b, std::optional<Object> c) {
   auto tmp = c.value_or(Object(whatever));
   // ...
}
Run Code Online (Sandbox Code Playgroud)

根据官方文档:

如果一个可选项包含一个值,则保证该值作为可选对象占用空间的一部分进行分配,即永远不会发生动态内存分配。因此,即使定义了 operator*() 和 operator->() ,可选对象也建模对象,而不是指针。

因此,每次我们使用 std::optional 传递参数时,它意味着创建副本,如果对象很大,这可能会导致性能下降。

我喜欢这个想法,因为它使代码更简单易懂,但是有什么好处吗?

Sam*_*hik 9

Astd::optional不是函数参数默认值的替代品:

void compute_something(int a, int b, const Object& c = Object(whatever))
Run Code Online (Sandbox Code Playgroud)

这可以调用 compute_something(0, 0);

void compute_something(int a, int b, std::optional<Object> c) 
Run Code Online (Sandbox Code Playgroud)

这是无法编译的。compute_something(0, 0);不会编译。至少,你必须做一个compute_something(0, 0, std::nullopt);.

因此,每次我们使用 std::optional 传递参数时,它意味着创建副本,如果对象很大,这可能会导致性能下降。

正确的。但请注意,还需要构造一个默认的函数参数。

但是,可以通过组合做了一些小技巧std::optional的std ::的reference_wrapper

#include <optional>
#include <utility>
#include <functional>
#include <iostream>

class X {

public:
    X()
    {
        std::cout << "Constructor" << std::endl;
    }

    ~X()
    {
        std::cout << "Destructor" << std::endl;
    }

    void foo() const
    {
        std::cout << "Foo" << std::endl;
    }

    X(const X &x)
    {
        std::cout << "Copy constructor" << std::endl;
    }

    X &operator=(const X &)
    {
        std::cout << "operator=" << std::endl;
    }
};

void bar(std::optional<std::reference_wrapper<const X>> arg)
{
    if (arg)
        arg->get().foo();
}

int main()
{
    X x;

    bar(std::nullopt);

    bar(x);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

使用 gcc 7.2.1,唯一的输出是:

Constructor
Foo
Destructor
Run Code Online (Sandbox Code Playgroud)

这确实增加了一些语法,并且可能很麻烦。但是,一些额外的语法糖可以减轻额外的绒毛。例如:

if (arg)
{
    const X &x=arg->get();

    // Going forward, just use x, such as:

    x.foo();
}
Run Code Online (Sandbox Code Playgroud)

现在,让我们再迈出一步:

void bar(std::optional<std::reference_wrapper<const X>> arg=std::nullopt)
Run Code Online (Sandbox Code Playgroud)

有了这个,两个函数调用可以简单地是:

bar();
bar(x);
Run Code Online (Sandbox Code Playgroud)

你可以吃蛋糕,也可以吃。您不必显式提供 a std::nullopt,由默认参数值提供;您不必构建整个默认对象,并且在显式传递对象时,它仍然通过引用传递。您只有自己的开销std::optional,在大多数 C++ 实现中,它只是一些额外的字节。


Bar*_*rry 7

在不知道您的函数具体在做什么的情况下,很难给出一个好的通用答案,但是是的,使用optional. 没有特定的顺序:


首先,在包装函数时如何传播默认参数?使用标准语言默认参数,您只需要知道所有默认值是什么:

int foo(int i = 4);
int bar(int i = /* foo's default that I have to know here */) { return foo(i); }
Run Code Online (Sandbox Code Playgroud)

现在,如果我将foo的默认值更改为5,我必须知道要更改bar- 通常它们最终会不同步。用optional,只有执行foo需要知道默认值:

int foo(optional<int> );
int bar(optional<int> o) { return foo(o); }
Run Code Online (Sandbox Code Playgroud)

所以这不是问题。


其次,存在您提供参数或回退到默认值的情况。但也有一种情况,简单地没有参数也具有语义意义。比如,如果我给你这个参数,就使用它,否则什么都不做。使用默认参数,这必须用标记表示:

// you just have to know that -1 means no fd
int foo(int fd = -1);
Run Code Online (Sandbox Code Playgroud)

但是使用optional,这在签名和类型中清楚地表达了 - 您不必知道哨兵是什么:

int foo(std::optional<int> fd);
Run Code Online (Sandbox Code Playgroud)

对于较大尺寸的对象,缺少哨兵也会对性能产生积极影响,因为不必构造一个具有该哨兵值的对象,您只需使用nullopt.


第三,如果optional开始支持引用(许多 3rd 方库都支持),optional<T const&>对于默认的、不可修改的参数来说,这是一个极好的选择。确实没有等效于默认参数。