使用std :: optional来避免函数中的默认参数是否有任何优势?

时间:2017-12-16 01:36:07

标签: c++ c++11 c++17 stdoptional

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

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

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));
   // ...
}

根据官方文件:

  

如果可选项包含值,则保证该值   作为可选对象足迹的一部分分配,即没有动态   内存分配一直在发生。因此,可选对象建模   对象,而不是指针,即使运算符*()和operator-&gt;()   已定义。

因此,每次我们使用std :: optional来传递参数时,如果对象很大,它意味着创建副本而不是惩罚性能。

我喜欢这个主意,因为它使代码更简单易懂,但有什么优势吗?

3 个答案:

答案 0 :(得分:4)

如果不知道你的功能具体是什么,很难给出一个很好的通用答案,但是,使用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); }

所以这不是问题。

其次,您提供参数或回退到默认值的情况。但也有一种情况,即简单地缺乏论证也具有语义含义。如果我把它给你,请使用这个论点,否则什么都不做。使用默认参数时,必须使用sentinel表示:

// 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&>开始支持引用(并且许多第三方库都支持引用),{{1}}对于一个不可更改的,不可修改的参数是一个很好的选择。确实没有与默认参数等效的东西。

答案 1 :(得分:1)

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);

  

所以,每次我们使用std :: optional来传递参数时,它都意味着一个   副本的创建,如果对象是,则可以是惩罚性能   大。

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

但是,您可以将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 ++实现中,这只是一些额外的字节。

答案 2 :(得分:0)

包含在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}});