The fact that monads are often associated with category theory mambo jumbo, they are no longer limited to functional programming languages like Haskell and have been used to implement core language libraries dealing with concurrency (e.g. non-broken futures like folly futures), error handling (e.g. std::expected in C++23), optionality (e.g. std::optional in C++23) and many others - after all monads are about composition and good APIs should compose. And today, I’d like to point out a common API design issue that occurs when patterns are moved from languages with different properties. Let’s take a look at std::optional<T>::value_or:
template< class U >
constexpr T value_or( U&& default_value ) const&;(1)(since C++17)template< class U >
constexpr T value_or( U&& default_value ) &&;(2)(since C++17)Returns the contained value if *this has a value, otherwise returns default_value.
1) Equivalent to bool(*this) ? **this : static_cast<T>(std::forward<U>(default_value))
2) Equivalent to bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(default_value))
Documentation clearly points that value_or is equivalent to ternary operator. This looks like a great way to improve readability and cut down on unnecessary boilerplate.
Ok, so it looks like there is nothing to see here… How about we do some trivial experiments just for fun. According to documentation
should be equivalent, so following code should have the same behavior for experiment 1 and 2 which process non-empty optional and 3, 4 for empty optional
You’re welcome to make a guess as to the output produced by this program, since it’s pretty straightforward. You can also use trusty compiler explorer to get the following output:
As you’ve probably guessed, the behavior most certainly doesn’t look the same - we end up with extra constructor/destructor invocations for experiment 1 vs 2. There is no magic and the default value argument that is passed to value_or is evaluated eagerly, so even if it’s not needed, it’s still going to be constructed. This is already pretty bad, but you can also see that there is also extra move constructor and destructor invocations for experiment 3 vs 4, since default value has to be moved into value_or.
In languages like Haskell this is not an issue because they evaluate their arguments lazily, which is not the case with C++. That’s why this pattern cannot be blindly copied into C++. It makes sense for small trivially copyable types, but can have significant performance impact when objects with expensive constructors are involved.
As such, when designing your monadic APIs, consider restricting function overloads or using lambdas to prevent unnecessary overhead.
I have been playing with these recently. The reason why you get the inefficiency is because the invocable can change the type, so the methods have to return by value.
It gets even more complicated with std::expected, where the unhappy path needs to potentially pass through the and_then that can change both the result and error types.
It's an interesting API choice, it does lean very heavily on the compiler to optimize.