如何强制SFINAE选择第二个结构定义?

How to force SFINAE to choose the second definition of structure?

在此之前,我想告诉大家我已经尝试自己实现 is_assignable。没有必要再给我看其他例子了——我已经看到了一些实现。

感谢您(当然,如果可能的话),我想修复我的解决方案。

所以,这是我的代码:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename LambdaT>
struct is_valid_construction {
    is_valid_construction(LambdaT) {}
    
    typedef typename LambdaT lambda_prototype;

    template<typename ValueTypeT, typename ExprTypeT = decltype(std::declval<lambda_prototype>()(std::declval<ValueTypeT>()))>
    struct evaluate {
        evaluate(ValueTypeT val) {
            std::cout << "Right!";
        }
        typedef typename std::true_type value;
    };

    template<typename ValueTypeT> //The compiler ignores this definition
    struct evaluate<ValueTypeT, decltype(std::declval<lambda_prototype>()(std::declval<int>()))> {
        evaluate(ValueTypeT val) {
            std::cout << "Nope";
        }
        typedef typename std::false_type value;
    };

    template<typename ValueTypeT>
    void print_value(ValueTypeT val) {
        evaluate evaluation(val);
    }
};

struct ForTest {};

int main() {
    is_valid_construction is_assignable([](auto x) -> decltype(x = x) { });
    is_valid_construction is_less_comparable([](auto x) -> decltype(x < x) {});
    is_valid_construction is_more_comparable([](auto x) -> decltype(x > x) {});

    is_assignable.print_value(int{});
    is_less_comparable.print_value(char{});
    is_more_comparable.print_value(ForTest{});

    return 0;
}

如您所见,我正在尝试在模板结构中定义模板结构。因此,我排除了如果 调用 (使用 declval 具有此类型参数的此 lambda 表达式(粗略地,就替换而言) 失败,然后 SFINAE 更进一步,应该看到第二个模板定义可以方便实例化。我在问如何修复我的模板结构及其默认参数以推动 SFINAE 使用第二个定义?

SFINAE 可用于指示编译器选择特定的函数重载,或 class 模板的特定部分特化。在第一种情况下,替换失败从重载集中删除了声明,在第二种情况下,替换失败从考虑中删除了部分特化声明(导致使用主模板,或者替换成功的不同部分特化)。

但是您在这里尝试做的是落后的:您遇到的情况是主模板可能会出现替换错误,并且您提供了部分专业化作为替代方案。这永远行不通。部分特化匹配在主模板的模板参数列表完全已知后开始,因此如果主模板的模板参数列表中出现替换错误,则不考虑特化。

例如,如果我们有

template <typename T, typename U = some_metafunction_of_T>
struct S;

template <typename T>
struct S<T, T>;

然后 S<int> 的实例化过程将首先为主模板计算 U,然后,只有当 TU 都已知时,编译器可以确定它们是否相同(这将允许使用部分特化)。如果计算U时出现代入错误,偏特化是否适用的问题就更问不出来了

要修复您的代码,您必须切换 evaluate 的两个定义。主模板必须是“回退”,部分特化必须可能会出现替换错误。

正如@Brian 所说,如果要求适用于所有专业,则应将要求放在主模板中,并将每个专业的其他要求放在它们自己的声明中:

template<typename T, typename = std::void_t</* global requirements */>>
struct S;
template<typename T>
struct S<T, std::void_t</* requirements for this specialization */>>;

如果你希望其中一个专业优先于其他专业,你可以将其负面要求添加到其他专业中:

template<typename T, typename = std::void_t</* global requirements */>>
struct S;
template<typename T>
struct S<T, std::void_t<std::enable_if_t</* conditions for this specialization */>>>;
template<typename T>
struct S<T, std::void_t<std::enable_if_t<!/* conditions for the former specialization */>, /* requirements for this specialization */>>;

对于你的例子,它应该是这样的:

template<typename Lambda>
struct is_valid_construction{
    template<typename T, typename = void>
    struct helper : std::false_type{};
    template<typename T>
    struct helper<T, std::void_t<decltype(std::declval<Lambda>()(std::declval<T>()))>> : std::true_type{};

    template<typename V, typename = void>
    struct evaluate;
    template<typename V>
    struct evaluate<V, std::enable_if_t<helper<V>::value>>;
    template<typename V>
    struct evaluate<V, std::void_t<std::enable_if_t<!helper<V>::value>, decltype(std::declval<Lambda>()(std::declval<int>()))>>;
};

顺便说一句,你可以使用std::is_invocable来简化这段代码:

template<typename Lambda>
struct is_valid_construction{
    template<typename V, typename = void>
    struct evaluate;
    template<typename V>
    struct evaluate<V, std::enable_if_t<std::is_invocable_v<Lambda, V>>>;
    template<typename V>
    struct evaluate<V, std::enable_if_t<!std::is_invocable_v<Lambda, V> && std::is_invocable_v<Lambda, int>>>;
};

感谢@RedFog 和@Brian 我可以完成我的代码并且得到了这样的结果:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename LambdaT>
struct is_valid_construction {
    is_valid_construction(LambdaT) {}

    typedef LambdaT lambda_prototype;

    template<class ValueT, class = void>
    struct is_void_t_deducable : std::false_type {};

    template<class ValueT>
    struct is_void_t_deducable<ValueT,
                               std::void_t<decltype(std::declval<lambda_prototype>()(std::declval<ValueT>()))>> : std::true_type {};

    template<class ValueT>
    bool is_valid_for(ValueT value) {
        if constexpr (is_void_t_deducable<ValueT>::value)
            return true;
        else
            return false;
    }
};

struct ForTest {};

int main() {
    is_valid_construction is_assignable([](auto x) -> decltype(x * x) { });
    std::cout << is_assignable.is_valid_for(0) << std::endl;
    std::cout << is_assignable.is_valid_for(ForTest{});

    return 0;
}

正如他们所说,当我这样声明模板参数时:

template<typename ValueTypeT, typename ExprTypeT = decltype(std::declval<lambda_prototype>()(std::declval<ValueTypeT>()))>

编译器不理解第二个模板参数应该分配什么默认值,因为两个声明不兼容。

我是模板编程的新手,我可以尝试尽可能简单地解释解决方案:

第二个模板参数(如果说不严格的话!)应该是void。因此,编译器可以通过第一个声明或第二个声明以两种方式实例化带有第二个 void 参数的模板。

(应该说std::void_t<TemplateParam>要是TemplateParam就变成了void!)

  • 如果第二个声明的实例化很好,那么 第二个模板参数是 void.

  • 如果第一个声明的实例化很好,那么 第二个模板参数是 void.

所以,我们应该用第二个模板参数void帮助编译器推导出这两个结构。当它首先尝试实例化 is_valid_for(ForTest{}) 时,它会尝试推断 std::void_t<decltype(std::declval<lambda_prototype>()(std::declval<ValueT>()))> 但得到替换错误。然而,没有什么能阻止以另一种方式推导出第二个模板参数 void,并且编译器采用第一个声明。

P.S。我知道这个解释不好,但对我这样的傻瓜可能有帮助!