knightmare
knightmare

Reputation: 103

How to do a "deep" SFINAE, i.e., when the substitution causes some compilation errors further in the code?

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?

https://godbolt.org/z/Y72Dov

#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

Answers (2)

Brian Bi
Brian Bi

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

aschepler
aschepler

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

Related Questions