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?
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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With