Reputation: 103
I need to implement a detection type, like is_invocable
, but it looks like SFINAE does only a shallow check of a substitution failure, whereas with my is_invocable
I need to be able to gracefully detect if that invocation will compile at all. Is it possible to achieve with C++17?
#include <type_traits>
struct supported_by_f1_and_f2 {};
struct not_supported_by_f1 {};
struct not_supported_by_f2 {};
template<typename T>
auto f2(T t, std::enable_if_t<!std::is_same_v<T, not_supported_by_f2>>* = 0) {}
template<typename T>
auto f1(T t, std::enable_if_t<!std::is_same_v<T, not_supported_by_f1>>* = 0) {
return f2(t);
}
template <typename T, typename = void>
struct is_f1_invocable : public std::false_type {};
template <typename T>
struct is_f1_invocable<T, std::void_t<decltype(f1(std::declval<T>()))>> : public std::true_type {};
using supported_by_f1_and_f2_ok_t = is_f1_invocable<supported_by_f1_and_f2>;
using not_supported_by_f1_ok_t = is_f1_invocable<not_supported_by_f1>;
using not_supported_by_f2_ok_t = is_f1_invocable<not_supported_by_f2>;
supported_by_f1_and_f2_ok_t supported_by_f1_and_f2_ok;
not_supported_by_f1_ok_t not_supported_by_f1_ok;
// Why substitution failure, that occures during 'return f2(t);', is not detected here during the instantiation of 'is_f1_invocable'?
not_supported_by_f2_ok_t not_supported_by_f2_ok; // error: no matching function for call to 'f2'
EDIT:
From https://en.cppreference.com/w/cpp/language/sfinae :
Only the failures in the types and expressions in the immediate context of the function type or its template parameter types [or its explicit specifier (since C++20)] are SFINAE errors. If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. [A lambda expression is not considered part of the immediate context. (since C++20)]
So is there a way to extend/workaround this?
Upvotes: 3
Views: 88
Reputation: 119467
The concept you're looking for is for f1
to be SFINAE friendly. This requires the author of f1
to take some action to ensure that a user has some way of detecting that a call to f1
will be ill-formed, resulting in a soft error. If f1
is not written to be SFINAE friendly, then there is no workaround.
To make f1
SFINAE friendly, we need to ensure that before some compilation error that would occur when instantiating the body of f1
is reached, first, the condition that would cause that error renders the signature of f1
invalid so that when the enclosing instantiation attempts to call or take the address of f1
, SFINAE kicks in to remove f1
from the overload set as the error was encountered in the immediate context of instantiating f1
's signature.
In other words, in this case, since we think that the instantiation of the call f2(t)
in the body of f1
may cause an error, we should duplicate that call in the signature of f1
. For example, we could do so as follows:
template <typename T>
auto f1(T t, std::enable_if_t<...>* = 0) -> decltype(f2(t)) { // actually you may want to decay the type but w/e
return f2(t);
}
So now, instantiation of f1(std::declval<T>())
kicks off the substitution and deduction process for f1
, which kicks off the substitution and deduction process for f2
. At that point, thanks to the enable_if
, a substitution failure occurs in the signature of f2
, which is in the immediate context of the f2
instantiation, so that removes the f2
template from the overload set. As a result, the call to f2
in the signature of f1
must be resolved from an empty overload set, which means the overload resolution failure is in the immediate context of the f1
instantiation. Finally, that removes the f1
template from the overload set as well, again resulting in overload resolution failing due to an empty overload set, this time in the immediate context of the is_f1_invocable
instantiation, which is what we want.
Similarly if something could go wrong when instantiating the body of f2
then we need to modify the signature of f2
to account for that possibility as well to ensure the propagation of SFINAE in an analogous fashion.
Of course, you must decide how far you want to take this. At some point you may decide that you really want to cause a hard error at this point instead of simply having the signature removed from the overload set, propagating a soft error up into the enclosing instantiation.
Upvotes: 4
Reputation: 72431
No, this is not possible, precisely because of the "immediate context" rule in [temp.fct.spec]/8 and described by your cppreference.com link.
Of course, if f1
checked for not_supported_by_f2
in its enable_if_t
check, either directly or by checking if f2(t)
is invokable, then it would be "more SFINAE-correct" and this wouldn't be an issue. But if you can't change the declaration of f1
, all you can do is:
add extra checks to your traits to work around specific known failures (though if f1
is in a library not under your control, and its implementation changes in a later version...)
template <typename T>
struct is_f1_invocable<T,
std::void_t<decltype(f1(std::declval<T>())),
decltype(f2(std::declval<T>()))>> // hack
: public std::true_type {};
document the limitation, to warn programmers using this trait.
Upvotes: 1