12

我正在将代码移植到 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*() 和 operator->(),可选对象也建模对象,而不是指针。

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

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

4

3 回答 3

11

Astd::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::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;
}

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

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

于 2017-12-16T03:42:41.240 回答
9

如果不知道您的函数具体在做什么,很难给出一个好的通用答案,但是是的,使用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开始支持引用(许多 3rd 方库都支持),optional<T const&>那么对于可默认的、不可修改的参数来说,这是一个绝佳的选择。确实没有等效于默认参数。

于 2017-12-16T01:52:12.947 回答
1

包含在函数参数中的值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}});
于 2017-12-16T14:59:07.693 回答