【问题标题】:Is there any advantage in using std::optional to avoid default arguments in a function?使用 std::optional 来避免函数中的默认参数有什么好处吗?
【发布时间】:2018-05-30 05:18:21
【问题描述】:

我正在将代码移植到 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 传递参数时,都意味着创建副本,如果对象很大,则可能会导致性能下降。

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

【问题讨论】:

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


    【解决方案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】:

      如果不知道您的函数具体在做什么,很难给出一个好的通用答案,但是是的,使用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&lt;T const&amp;&gt; 是可默认的、不可修改的参数的绝佳选择。确实没有等效于默认参数。

      【讨论】:

        【解决方案3】:

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

        【讨论】:

          猜你喜欢
          • 2021-10-27
          • 2019-02-14
          • 1970-01-01
          • 1970-01-01
          • 2018-09-20
          • 2010-12-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多