Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm porting code to C++17, trying to use the new features while possible. One thing I like is the idea to use std::optional to return or not a value in a function that may fail in some conditions.

I was curious about the possible usages of this new feature, and I'm thinking in start to use it to replace optional arguments in functions, so:

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

Becomes:

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

According to the official doc:

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.

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.

I love the idea, because it make the code simpler and easy to understand but, is there any advantage?

like image 939
mohabouje Avatar asked Dec 16 '17 01:12

mohabouje


People also ask

What is the use of std :: optional?

std::optional contains the object within itself, depending on where it is stored (stack/data/heap) std::optional makes a copy of the contained object. Monadic functions will be added in C++23 to improve the abstraction in our code by removing the needs of writing boilerplate code.

Is std optional efficient?

As a conclusion, std::optional is as efficient as a custom type to represent an optional integer value. Don't implement your own type, simply use the standard type.

Why do we need to use default arguments in a function in C++?

Default Arguments in C++ A default argument is a value provided in a function declaration that is automatically assigned by the compiler if the calling function doesn't provide a value for the argument. In case any value is passed, the default value is overridden.

Are optional arguments bad?

The thing with optional parameters is, they are BAD because they are unintuitive - meaning they do NOT behave the way you would expect it. Here's why: They break ABI compatibility ! so you can change the default-arguments at one place.


2 Answers

A std::optional is not a drop-in replacement for a function parameter default:

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

This can be invoked a compute_something(0, 0);

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

This cannot be compiled. compute_something(0, 0); won't compile. At the very least, you must do a 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.

Correct. But note that a defaulted function argument also needs to be constructed.

But you can do a few tricks by combining std::optional with a 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;
}

With gcc 7.2.1, the only output from that is:

Constructor
Foo
Destructor

This does add a bit of syntax, and may be cumbersome. But, some additional syntactic sugar can mitigate the extra fluff. For example:

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

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

    x.foo();
}

Now, let's take one more step:

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

With that, the two function calls can simply be:

bar();
bar(x);

You can have your cake, and eat it too. You don't have to explicitly supply a std::nullopt, courtesy of the default parameter value; you do not have to construct an entire defaulted object, and when passing an object explicitly it still gets passed by reference. You just have an overhead of std::optional itself which, on most C++ implementation, is just a few extra bytes.

like image 82
Sam Varshavchik Avatar answered Sep 30 '22 05:09

Sam Varshavchik


It's hard to give a good generic answer without knowing what specifically your function is doing, but yes, there are clear advantages to using optional. In no particular order:


First, how do you propagate default arguments when wrapping functions? With standard language default arguments, you just have to know what all the defaults are:

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

And now if I change foo's default to 5, I have to know to change bar - which typically they'll just end up out of sync. With optional, only the implementation of foo needs to know the default:

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

So that's just not a problem.


Second, there's the case where you provide an argument or fallback to a default. But there's also a case where simply the absence of an argument also has semantic meaning. As in, use this argument if I give it to you, otherwise do nothing. With default arguments, this has to be expressed with a sentinel:

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

But with optional, this is expressed clearly in the signature and the type - you don't have to know what the sentinel is:

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

The lack of sentinel can have positive performance impact too for larger sized objects, since instead of having to construct one to have that sentinel value, you just use nullopt.


Third, if optional ever starts supporting references (and many 3rd party libraries do), optional<T const&> is a fantastic choice for a defaultible, non-modifiable argument. There really is no equivalent to default arguments.

like image 23
Barry Avatar answered Sep 30 '22 07:09

Barry