如何执行 "deep" SFINAE,即当替换在代码中进一步导致一些编译错误时?

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

我需要实现一个检测类型,比如 is_invocable,但看起来 SFINAE 只对替换失败进行浅层检查,而对于我的 is_invocable 我需要能够优雅地检测该调用是否会编译。用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'

编辑:

来自 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)]

那么有办法extend/workaround吗?

不,这是不可能的,正是因为 [temp.fct.spec]/8 中的 "immediate context" 规则以及您的 cppreference.com link.

所描述的

当然,如果 f1 在其 enable_if_t 检查中直接检查 not_supported_by_f2,或者直接检查 f2(t) 是否可调用,那么它将是 "more SFINAE-correct",这不是问题。但是如果你不能改变f1的声明,你能做的就是:

  • 为你的特征添加额外的检查以解决特定的已知故障(尽管如果 f1 在一个不受你控制的库中,并且它的实现在更高版本中发生变化...... )

    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 {};
    
  • 记录限制,以警告使用此特性的程序员。

您正在寻找的概念是 f1 SFINAE 友好。这需要 f1author 采取一些措施来确保 user 有某种方法检测对 f1 将是 ill-formed,导致软错误。如果 f1 没有写成对 SFINAE 友好,则没有解决方法。

为了使 f1 SFINAE 友好,我们需要确保在实例化 f1body 时出现一些编译错误之前,首先,导致该错误的条件会导致 f1signature 无效,因此当封闭实例化尝试调用或获取 f1 的地址时,SFINAE开始从重载集中删除 f1,因为在实例化 f1 的签名的直接上下文中遇到错误。

换句话说,在这种情况下,由于我们认为 f1 主体中调用 f2(t) 的实例化可能会导致错误,因此我们应该在签名中复制该调用f1。例如,我们可以这样做:

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);
}

所以现在,f1(std::declval<T>()) 的实例化启动了 f1 的代入和推导过程,从而启动了 f2 的代入和推导过程。在这一点上,由于 enable_iff2 的签名中发生了替换失败,它位于 f2 实例化的直接上下文中,因此删除了 f2 来自重载集的模板。因此,必须从空重载集解析 f1 签名中对 f2 的调用,这意味着重载解析失败发生在 f1 实例化的直接上下文中。最后,这也从重载集中删除了 f1 模板,再次导致由于重载集为空而导致重载解析失败,这次是在 is_f1_invocable 实例化的直接上下文中,这就是我们想要。

类似地,如果在实例化 f2 的主体时出现问题,那么我们需要修改 f2 的签名以解决这种可能性,并确保 SFINAE 在类似的环境中传播时尚

当然,您必须决定要走多远。在某些时候,您可能会决定此时确实要引起硬错误,而不是简单地从重载集中删除签名,将软错误传播到封闭的实例化中。