【问题标题】:How to force SFINAE to choose the second definition of structure?如何强制 SFINAE 选择结构的第二种定义?
【发布时间】:2021-02-09 02:52:55
【问题描述】:

在此之前,我想告诉你,我已经尝试自己实现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 使用第二个定义?

【问题讨论】:

    标签: c++ templates metaprogramming sfinae


    【解决方案1】:

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

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

    例如,如果我们有

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

    那么S&lt;int&gt;的实例化过程将首先评估U作为主模板,然后,只有当TU都已知时,编译器才能确定它们是否相同(这将允许使用部分专业化)。如果在计算U时出现替换错误,甚至不能问偏特化是否适用的问题。

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

    【讨论】:

      【解决方案2】:

      正如@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>>>;
      };
      

      【讨论】:

      • 还是谢谢你,但你的帖子似乎与@Brian 的帖子有点矛盾。他说替换失败使我的模板损坏,这有一些逻辑。但是为什么std::void_tstd::enable_if 不会替换失败呢?是因为在我的情况下我完全破坏了模板,而在你的情况下模板始终有效并且编译器永远不会崩溃?
      • @AyratArifullin 因为std::void_t&lt;...&gt; 始终是void,并且std::enable_if_t 默认也是voidstd::enable_if_t&lt;true, T&gt;TT 默认是void) .第二个参数总是void,所以效果很好。
      • 如果你的主模板有一个 SFINAE 的默认模板参数,并且你想提供一个特化,你必须让这个特化的参数与默认参数的类型相同,否则特化不会如果您没有明确提供该参数,则匹配。
      • 不知道解释清楚了没有。如果您仍然对此感到困惑,您可以只使用voidvoid 的某些类型别名(例如std::void_t)作为SFINAE 的附加模板参数。
      【解决方案3】:

      感谢@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&lt;TemplateParam&gt;好了,void就变成了TemplateParam!)

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

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

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

      附:我知道这个解释不好,但它可能会帮助像我这样的傻瓜!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-12-19
        • 1970-01-01
        • 2013-01-14
        • 1970-01-01
        • 1970-01-01
        • 2012-05-15
        • 1970-01-01
        相关资源
        最近更新 更多