Why are optional::transform and optional::and_then not constrained by invocable?
Monadic operations for std::optional
transform is the c++ spelling for map or fmap
and_then is monadic bind for optionalor_else is dual to and_then
or_else also has:
Constraints:
FmodelsinvocableandTmodels{move,copy}_constructible.
transform and and_then do not.
They don't work if you don't give them invocables, and rely on invoke_result_t to compute the result. So why not constrain them?
The problem is if another template has to be instantiated in order to evaluate the template of interest, and that template has an error, you get an error, not a substitution failure. And, it turns out, lambdas are a common source of the problem and a common case in real use.
Consider the code:
void f(int&); auto l = [](auto& y) { f(y); return 42; };
The two important parts are the auto& parameter and the implicit deduced return type.
We can rewrite it, to make things possibly more obvious:
struct Func { template <typename T> auto operator()(T& t) { f(t); return 42; } };
and to slightly spoil things, the problem code is effectively:
static_assert(std::invocable<Func, int const&>);
which produces:
<source>: In instantiation of 'auto Func::operator()(T&) [with T = const int]': type_traits:2565:26: required by substitution of 'template<class _Fn, class ... _Args> static std::__result_of_success<decltype (declval<_Fn>()((declval<_Args>)()...)), std::__invoke_other> std::__result_of_other_impl::_S_test(int) [with _Fn = Func; _Args = {const int&}]' type_traits:2576:55: required from 'struct std::__result_of_impl<false, false, Func, const int&>' type_traits:3038:12: recursively required by substitution of 'template<class _Result, class _Ret> struct std::__is_invocable_impl<_Result, _Ret, true, std::__void_t<typename _CTp::type> > [with _Result = std::__invoke_result<Func, const int&>; _Ret = void]' type_traits:3038:12: required from 'struct std::is_invocable<Func, const int&>' type_traits:3286:71: required from 'constexpr const bool std::is_invocable_v<Func, const int&>' concepts:336:25: required from here <source>:12:10: error: binding reference of type 'int&' to 'const int' discards qualifiers 12 | f(t); | ~^~~ <source>:2:12: note: initializing argument 1 of 'void f(int&)' 2 | void f(int&); | ^~~~ Compiler returned: 1
as the compiler is unhappy about trying to call the function f with a const int&.
If the lambda or Func is changed to have a non-deduced return type, the instantiation errors from the check to invocable go away, although you still get an error calling either with a const int.
So why do we run into this with transform if we were to constrain it with invocable ?
The compiler needs to figure out the overload set in order to resolve which one to use from the set. There are four of them two for the l- and r- value category and two for the const overloads.
template<class F> constexpr auto transform(F&& f) &; template<class F> constexpr auto transform(F&& f) const &; template<class F> constexpr auto transform(F&& f) &&; template<class F> constexpr auto transform(F&& f) const &&;
with differing computations of the resulting optional being returned.
using U = invoke_result_t<F, decltype(std::move(*val))>; //or using U = remove_cv_t<invoke_result_t<F, decltype(*val)>>;
So, in order to work out what the templated transform's signature really is, it has to compute what the invocable returns, and since the invocable has deduced return type, it needs to instantiate it, and instantiating with const int causes an error.
This is unfortunate.
If we constrain transform we get the same errors as above. See here, with just enough of an optional to compile.
Not absolutely nothing.
Constraints in the library:
Constraints: the conditions for the function's participation in overload resolution ([over.match]).
[Note 1: Failure to meet such a condition results in the function's silent non-viability. — end note]
[Example 1: An implementation can express such a condition via a constraint-expression ([temp.constr.decl]). — end example]
[structure.specifications] 3.1
Constraints are for making an overload not exist if the constraint isn't met. It's not a way of signaling an error. Those are Mandates:
Mandates: the conditions that, if not met, render the program ill-formed.
[Example 2: An implementation can express such a condition via the constant-expression in a static_assert-declaration ([dcl.pre]). If the diagnostic is to be emitted only after the function has been selected by overload resolution, an implementation can express such a condition via a constraint-expression ([temp.constr.decl]) and also define the function as deleted. — end example]
[structure.specifications] 3.2
Asking to run and_then on a non-invocable probably ought not to say there is no such function, but instead tell you it can't be invoked. I'm now not convinced that or_else should be constrained this way. It's not significantly better for o.or_else(5) to fail to resolve, mentioning invocable, than produce an error that invoke_result_t doesn't work, or that f can't be invoked. The kind of error is a minor detail.
Constraints that let you control the choice of alternatives are wonderful, and requires clauses are normal programmer accessible, unlike SFINAE, or even enable_if. But without an overload set to constrain, there possibly should not be a constraint.
There are some notes in P0798 that suggest that Deducing This might help, P0847.
The idea would to be to NOT have all the value category overloads that need to be checked, but to just have a single one that deduces what this is and provide it as a template parameter for further use. The contained parameter could be forwarded using forward_like<Self>.
P0847 has discussion about how deducing this might be applied to optional. There's also discussion of deducing this and the SFINAE-unfriendly auto at C++23’s Deducing this: what it is, why it is, how to use it.
With the tools we have today, it looks possible, but still slightly messy. I managed to get my implementation of optional to compile and pass its own tests with.
template <class F, class Self> requires( std::invocable<F, decltype(std::forward_like<Self>(std::declval<T>()))>) constexpr auto transform(this Self&& self, F&& f) -> optional<std::invoke_result_t< F, decltype(std::forward_like<Self>(std::declval<T>()))>> { using U = std::invoke_result_t<F, decltype(std::forward_like<Self>( std::declval<T>()))>; static_assert(!std::is_array_v<U>); static_assert(!std::is_same_v<U, in_place_t>); static_assert(!std::is_same_v<U, nullopt_t>); static_assert(std::is_object_v<U> || std::is_reference_v<U>); if (self.has_value()) { return optional<U>{detail::from_function, std::forward<F>(f), std::forward_like<Self>(self.value_)}; } return optional<U>; }
If there were a std::forward_like_t, it might be possible to reduce some of the noise in computing the value category used for the T. I also have not thought extensively about if the requires clause is truly needed in light of the invoke_result_ that can now be used in the trailing return type.