Reputation: 4050
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?
Upvotes: 15
Views: 22755
Reputation: 37513
Values wrapped into optional
and default function parameters are not alternatives. They can be used together to achieve results that can not be achived if one or another is used separately. For example:
// 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}});
Upvotes: 2
Reputation: 118310
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.
Upvotes: 13
Reputation: 302842
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.
Upvotes: 15