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

Is there any advantage in using std::optional to avoid default arguments in a function?

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

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

void compute_something(int a, int b, const Object& c = Object(whatever)) {
   // ...
}

变为:

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

根据官方文档:

If an optional contains a value, the value is guaranteed to be allocated as part of the optional object footprint, i.e. no dynamic memory allocation ever takes place. Thus, an optional object models an object, not a pointer, even though the operator*() and operator->() are defined.

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

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

如果不知道你的函数具体在做什么,很难给出一个好的通用答案,但是,是的,使用 optional 有明显的优势。排名不分先后:


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

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

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

int foo(optional<int> );
int bar(optional<int> o) { return foo(o); }

所以这不是问题。


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

// you just have to know that -1 means no fd
int foo(int fd = -1);

但是对于optional,这在签名和类型中表达得很清楚——你不必知道哨兵是什么:

int foo(std::optional<int> fd);

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


第三,如果 optional 曾经开始支持引用(许多第三方库都这样做),optional<T const&> 是默认的、不可修改的参数的绝佳选择。确实没有等同于默认参数的东西。

A std::optional 不是函数参数默认值的直接替换:

void compute_something(int a, int b, const Object& c = Object(whatever))

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

void compute_something(int a, int b, std::optional<Object> c) 

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

So, every time we use a std::optional to pass arguments, it implies a creation of copies than can be a penalty performance if the object is big.

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

但是您可以通过将 std::optionalstd::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;
}

对于 gcc 7.2.1,唯一的输出是:

Constructor
Foo
Destructor

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

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

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

    x.foo();
}

现在,让我们更进一步:

void bar(std::optional<std::reference_wrapper<const X>> arg=std::nullopt)

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

bar();
bar(x);

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

包装到 optional 中的值和默认函数参数不是替代项。它们可以一起使用,以达到单独使用一种或另一种无法达到的效果。例如:

// user may or may not supply an item value
// if item is not supplied then the stock item will be constructed
// user can not choose to supply an empty item
void foo(t_Item item = t_Item{42});

// user must supply an optional item value
// though he can choose to supply an empty item
void foo(optional<t_Item> item);

// user may or may not supply an optional item value
// but he can choose to supply an empty item as well
// if no optional item value is supplied then the stock item will be constructed
void foo(optional<t_Item> item = optional<t_Item>{t_Item{42}});