We often have to represent in software a value that might be missing. Different programming languages have abstraction for this purpose.
A recent version of C++ (C++17) introduces std::optional templates. It is kind of neat. You can write code that prints a string, or a warning if no string is available as follows:
void f(std::optional<std::string> s) { std::cout << s.value_or("no string") << std::endl; }
I expect std::optional to be relatively efficient when working with trivial objects (an integer, a std::string_view, etc.). However if you want to avoid copying your objects, you should use std::optional with care.
Let us consider this example:
A f(std::optional<A> z) { return z.value_or(A()); } A g() { A a("message"); auto z = std::optional<A>(a); return f(z); }
How many instances of the string class are constructed when call the function g()? It is up to five instances:
- At the start of the function we construct one instance of A.
- We then copy-construct this instance when passing it to std::optional<A>.
- Passing std::optional<A> to the function f involves another copy.
- In the value_or construction, we have a default construction of A (which is wasteful work in this instance).
- Finally, we copy construct an instance of A when the call to value_or terminates.
It is possible for an optimizing compiler to elide some or all of the superfluous constructions, especially if you can inline the functions, so that the compiler can see the useless work and prune it. However, in general, you may encounter inefficient code.
I do not recommend against using std::optional. There are effective strategies to avoid the copies. Just apply some care if performance is a concern.
Most of this seems quite expected, just like what you get with std::tuple or any other abstraction embedding one or several values of type T.
The value_or() issue is avoided by making default construction cheap, which is a good idea in general anyway, and easily achievable for purely “data” classes (but may not be possible if embedding something like a mutex).
I agree with your sentiment.
Rule of thumb (for me) is pointer types are nullable and therefore inherently ‘optional’ – thus the need for something that specifically isn’t a pointer type.
I actually forgot this today, so your blurb is quite timely for me.
The std::optional template is exactly as efficient or inefficient as any other value type. It works well with objects implementing move-only semantics.
It works well with objects implementing move-only semantics.
Here is a code sample with such a class:
smart pointers can efficiently accommodate the optional pattern for everything other than primitives and rvalues.
This creates an issue when templating code because neither style is optimal in all cases.
Minor nitpick, I think you meant to pass z to f(). Otherwise, good stuff!
The 5th case will not make a copy in most use cases (e.g., A a = g(); is guranteed to *not* make a copy). And other comments are right too — there’s nothing special about std::optional. You will get the similar behaviour with just std::string or any other value type.
Several times I have already wished for an emplacing value_or(), i.e., one that accepts ctor arguments and constructs the alternative iff the optional is empty.
Of course one can write that oneself as a free function, but it would be nice to have it in the std::lib.
I don’t think this has much to do with optional but more to do with pass by value vs reference. I think most of the superfluous copies could have been avoided but passing const T& vs T.
is there anything specific to optional here… you’d have the same behavior with any type
Thanks for the code and thoughts.
The line
return f(a);
probably should pass ‘z’ instead of ‘a’.
I’m going to assume that the last example should include
f(z)
In Rust it is actually really clear what happens. It’s a move. No copies involved:
fn f(z: Option<A>) -> A {
// the use of the lambda avoids constructing the default unless z is None
return z.unwrap_or_else(|| A::default());
}
fn g() -> A {
let a = A::new(“message”.into());
// move a
let z = Some(a);
// move z
return f(z);
}
fn main() {
let _ = g();
}
// no copy / clone
struct A {
c: String,
}
impl Default for A {
fn default() -> A {
return A {
c: “default”.into()
};
}
}
impl A {
fn new(s: String) -> A {
A {
c: s
}
}
}
Useful article. Would be interesting to see the assembly output if you put on godbolt, with optimization. Then can see how many copies are really made.
Scary how the potentials of Artificial Intelligence can shape our lives
The way the function returns the value is actually very clever f(z).