【问题标题】:How to simplify enable_if alias in template template parameter如何简化模板模板参数中的 enable_if 别名
【发布时间】:2019-10-01 17:43:57
【问题描述】:

我的目标是有一个结构,它接受一个专门的enable_if_t<> 的别名以及一个类型名可变参数包,然后告诉我包中的所有类型是否满足enable_if 的条件.我有一堆专门的enable_ifs,但需要先为它们编写测试,然后才能将它们放入我们的开源项目中。我有大约 2000 多行代码手动测试这些专业化,但我敢打赌,如果我能弄清楚下面的模式,我可以把它提高到 100 或 200 行。我有一个工作版本(+ Godbolt 链接),但是我不确定它为什么工作,并且该方案在实现接收参数包的情况下会中断

这是我想编写的代码示例及其结果。我正在使用 C++14,并且可以从 C++17 中窃取基本实现,例如连词和 void_t

#include <type_traits>
#include <string>

// enable_if for arithmetic types
template <typename T>
using require_arithmetic = typename std::enable_if_t<std::is_arithmetic<T>::value>;

const bool true_arithmetic = require_tester<require_arithmetic, double, int, float>::value;
// output: true

// If any of the types fail the enable_if the result is false
const bool false_arithmetic = require_tester<require_arithmetic, double, std::string, float>::value;
// output: false

以下内容确实可以满足我的要求,但是我不太了解如何操作。

// Base impl
 template <template <class> class Check, typename T1, typename = void>
 struct require_tester_impl : std::false_type {};

 // I'm not totally sure why void_t needs to be here?
 template <template <class> class Check, typename T1> 
 struct require_tester_impl<Check, T1, void_t<Check<T1>>> : std::true_type {};

 // The recursive version (stolen conjuction from C++17)
 template <template <class> class Check, typename T = void, typename... Types>
 struct require_tester {
  static const bool value = conjunction<require_tester_impl<Check, T>,
   require_tester<Check, Types...>>::value;
 };

// For the end
 template <template <class> class Check>
 struct require_tester<Check, void> : std::true_type {} ;

特别是,我不确定为什么在 impl 的部分特化中需要 void_t 来用于 std::true_type

我想要的是一个require_variadic_tester,它接受一个可变参数模板化别名,比如enable_if&lt;conjunction&lt;check&lt;T...&gt;&gt;::value&gt;,并给出真或假。可悲的是,无论输入什么类型,下面都会返回 false


// impl
template <template <class...> class Check, typename... Types>
struct require_variadic_impl : std::false_type {};

// Adding void_t here causes the compiler to not understand the partial specialiation
template <template <class...> class Check, typename... Types>
struct require_variadic_impl<Check, Check<Types...>> : std::true_type {};

template <template <class...> class Check, typename... Types>
struct require_variadic_tester : require_variadic_impl<Check, Types...> {};

给定输入,我想要以下内容,但似乎无法动摇如何将该连词隐藏到低一级

// Enable if for checking if all types are arithmetic
template <typename... Types>
using require_all_arithmetic = std::enable_if_t<conjunction<std::is_arithmetic<Types>...>::value>;

require_variadic_tester<require_all_arithmetic, double, double, double>::value;
// is true

require_variadic_tester<require_all_arithmetic, double, std::string, double>::value;
// is false

我认为我在第一个元函数中没有理解void_t导致我的误解

以下是神螺栓,非常感谢任何帮助理解这一点!

https://godbolt.org/z/8XNqpo

编辑:

为了提供更多背景信息,说明我为什么要在上面加上 enable_if_t 的连词。我坚持使用 C++14,但我们正在向我们的开源数学库添加一个新功能,如果没有更多的泛型类型(以及对这些泛型类型的要求),我们最终会出现大量代码膨胀。我们目前有这样的东西


template <int R, int C>
inline Eigen::Matrix<double, R, C> add(
    const Eigen::Matrix<double, R, C>& m1, const Eigen::Matrix<double, R, C>& m2) {
  return m1 + m2;
}

我想要更多通用模板并做类似的事情


template <typename Mat1, typename Mat2,
  require_all_eigen<is_arithmetic, Mat1, Mat2>...>
inline auto add(Mat1&& m1, Mat2&& m2) {
  return m1 + m2;
}

我已经设置了所有这些 require_*_&lt;container&gt; 别名,但所有这些的测试需要大约 2000 多行,并且在未来这将是一个必须处理的时髦混乱。

我们有一元和可变模板 enable_if 别名,此时上面的一元案例可以满足我的要求,就像一个很好的测试

#include <gtest/gtest.h>

TEST(requires, arithmetic_test) {
  EXPECT_FALSE((require_tester<require_arithmetic, std::string>::value));
  EXPECT_TRUE((require_tester<require_arithmetic, double, int, float>::value));
}

我遇到的问题是测试可变参数模板 enable_if 别名,我希望能够在其中编写类似的东西


// Enable if for checking if all types are arithmetic 
template <typename... Types>
using require_all_arithmetic = std::enable_if_t<conjunction<std::is_arithmetic<Types>...>::value>;
/// For the tests

TEST(requires, arithmetic_all_test) {
  EXPECT_FALSE((require_variadic_tester<require_all_arithmetic, std::string,
     Eigen::Matrix<float, -1, -1>>::value));
  EXPECT_TRUE((require_variadic_tester<require_all_arithmetic,
     double, int, float>::value));
}

如果我可以测试所有这些,我认为仅我们库的 requires 部分就可能是一个不错的仅标头迷你库,用于我所说的“14 中的坏假概念”(或简称 bfc14 ;- ))

【问题讨论】:

  • 回答你的第一个问题:如果Check&lt;T1&gt; 格式不正确,那么require_tester_impl&lt;Check, T1, void&gt; 将是false_type 的派生,因为模板替换在专业化期间失败,退回到非专业化实现,而如果格式正确,它将成功地专门化为 true_type 的派生。
  • 谢谢!我想我不明白的是为什么Check&lt;T1&gt; 病态不会导致 SFINAE 关闭并使用非部分专业化

标签: c++ c++14 c++17 variadic-templates sfinae


【解决方案1】:

您的require_tester&lt;require_arithmetic, double, double, int&gt; 会发生以下情况:

这与require_tester 的部分特化不匹配,后者只有两个模板参数&lt;Check, void&gt;,因此我们使用主模板

template <template <class> class Check, typename T, typename... Types>
struct require_tester;

Check = require_arithmetic; T = double; Types = double, int。它与require_tester 的部分特化不匹配。成员value

的结果
conjunction<require_tester_impl<Check, T>, require_tester<Check, Types...>>::value

有趣的部分是require_tester_impl&lt;Check, T&gt; = require_tester_impl&lt;require_arithmetic, double&gt;。首先,由于require_tester_impl的模板参数是

template <template <class> class Check, typename T1, typename = void>

并且只给出了两个显式模板参数,我们知道实际的模板参数是&lt;require_arithmetic, double, void&gt;。现在我们需要看看这是否匹配require_template_impl的部分特化,所以我们尝试匹配:

require_template_impl<require_arithmetic, double, void>
require_template_impl<Check, T1, void_t<Check<T1>>>

所以模板参数推导找到Check = require_arithmeticT1 = doublevoid_t&lt;Check&lt;T1&gt;&gt; 类型不会导致对CheckT1 的任何扣除。但是推导出的参数值必须代入,我们发现void_t&lt;Check&lt;T1&gt;&gt;void_t&lt;require_arithmetic&lt;double&gt;&gt;void。这与模板参数中的void 匹配,因此部分特化匹配,并且require_template_impl&lt;require_arithmetic, double, void&gt; 继承std::true_type,而不是std::false_type

另一方面,如果 T1std::string 而不是 double,则通过最终的 enable_if&lt;...&gt;::type 替换推导出的模板参数会发现 void_t&lt;require_arithmetic&lt;std::string&gt;&gt; 无效成员type 存在。当将推导出的模板参数替换为其他模板参数失败时,这意味着部分特化被丢弃为不匹配。所以require_template_impl&lt;require_arithmetic, std::string, void&gt;使用主模板并继承std::false_type

回到require_testervalue成员,它通过require_tester&lt;require_arithmetic, int&gt;::value通过require_tester&lt;require_arithmetic&gt;::value递归找到require_tester&lt;require_arithmetic, double, int&gt;::value,这与require_tester&lt;require_arithmetic, void&gt;::value相同。所有value 成员都是真的,所以最后的value 是真的。

虽然我会简化一下:

  1. voidrequire_tester 递归中是不必要的,并导致require_tester&lt;Anything, void&gt;::value 始终为真这一奇怪的“事实”。最好从主require_tester 模板中删除= void 默认值,并改为使用基本情况template &lt;template &lt;class&gt; class Check&gt; require_tester&lt;Check&gt;

  2. require_tester 主模板中的value 表达式总是给conjunction 提供两个模板参数,所以它并没有真正使用它的可变参数属性,你也可以写require_tester_impl&lt;...@ 987654377@...&gt;::value.由于require_tester 本身正在执行递归,因此不需要将递归定义抽象为conjunction。相反,require_tester 可以简化为依靠 conjunction 并避免自身进行任何递归:

    template <template <class> class Check, typename... Types>
    struct require_tester : conjunction<require_tester_impl<Check, Types>...>
    {};
    // No other specialization needed.
    

require_variadic_tester 模板可以遵循类似的模式,除了我将给只是typename = void 的虚拟模板参数命名为typename Enable。而且它需要在模板参数包之前,所以实际上默认它为void并没有那么有用,我们需要确保在相应的位置使用适当的void模板参数。

template <template <class...> class Check, typename Enable, typename... Types>
struct require_variadic_impl : std::false_type {};

template <template <class...> class Check, typename... Types>
struct require_variadic_impl<Check, void_t<Check<Types...>>, Types...> : std::true_type {};

template <template <class...> class Check, typename... Types>
struct require_variadic_tester : require_variadic_impl<Check, void, Types...> {};

请参阅the modified program on godbolt,获得所需的结果。

【讨论】:

  • 这个答案很漂亮,时尚,我敢说甚至很性感。非常感谢您的解释和代码!
【解决方案2】:

不确定是否了解您的所有需求,但...

我想要的是一个require_variadic_tester,它接受一个可变参数模板化别名,比如enable_if&lt;conjunction&lt;check&lt;T...&gt;&gt;::value&gt;,并给出真或假。可悲的是,无论输入什么类型,下面都会返回 false

您确定要conjunction&lt;check&lt;T...&gt;&gt; 吗?

或者你想要conjunction&lt;check&lt;T&gt;...&gt;

我的意思是...检查必须接收类型的可变参数列表,或者您是否要检查别名,(如您的示例中)接收单个类型和一个为真的连词当且仅当) 检查是否满足所有类型?

在第二种情况下,std::void_t 非常方便验证是否满足所有检查。

我提出以下require_variadic_implrequire_variadic_tester

template <template <typename> class, typename, typename = void>
struct require_variadic_impl
   : public std::false_type 
 { };

template <template <typename> class C, typename ... Ts>
struct require_variadic_impl<C, std::tuple<Ts...>, std::void_t<C<Ts>...>>
   : public std::true_type
 { };

template <template <typename> class C, typename ... Ts>
struct require_variadic_tester
   : public require_variadic_impl<C, std::tuple<Ts...>>
 { };

现在来自

template <typename T>
using require_arithmetic = typename std::enable_if_t<std::is_arithmetic<T>::value>;

// ...

printf("\nGeneric Variadic: \n\n");
const char* string_generic_var_check = 
  require_variadic_tester<require_arithmetic, std::string>::value ? "true" : "false";
const char* double_generic_var_check = 
  require_variadic_tester<require_arithmetic, double, double, double>::value ? "true" : "false";
std::printf("\t String: %s\n", string_generic_var_check);
std::printf("\t Double: %s\n", double_generic_var_check);

你得到

Generic Variadic: 

     String: false
     Double: true

认为我在第一个元函数中未能理解 void_t 导致我的误解

尝试将std::void_t&lt;Ts...&gt; 视为“如果所有Ts 都已启用,则启用”。

【讨论】:

  • 感谢您的回答! void_t 现在更有意义了。虽然我确实需要 enable_if 内部的连词。我用更多上下文更新了原始问题
  • @Steve_Corrin - 在我看来你需要template &lt;typename... Types&gt; using require_all_arithmetic = std::void_t&lt;std::enable_if_t&lt;std::is_arithmetic&lt;Types&gt;&gt;...&gt;。 “里面的连词”是std::void_t
  • 我想把 void_t 放在 require_all_arithmetic 的外面,因为在函数模板中使用 require_*s 时不需要它
【解决方案3】:
template <template <class> class Check, typename T1, typename = void>
struct require_tester_impl : std::false_type {};

// I'm not totally sure why void_t needs to be here?
template <template <class> class Check, typename T1> 
struct require_tester_impl<Check, T1, void_t<Check<T1>>> : std::true_type {};

这里,您需要require_tester_impl 的第三个参数是void 类型,因为您将其写为默认值。如果用户在特化require_tester_impl 时没有指定其第三个参数,则为void。所以编译器会搜索一个偏特化,其中第一个模板参数是一元类模板,第二个模板参数是类型,第三个是void,否则找不到偏特化,因为第三个参数任何部分专业化都将失败。

这就是void_t 发挥作用的地方。由于您想将Check 注入到参数中,但您需要void,这时void_t 就派上用场了,因为用于专门化的每种类型都映射到void,这正是您真正需要的。当部分特化没有失败时,您将拥有两种启用的特化,一种是默认特化,另一种是部分特化。

最终会选择部分的,因为它比另一个更专业,因为 void 的计算方式取决于其他模板参数。

这是第一部分。对于第二部分(可变参数模板),请记住,如果 enable_if 成功,则返回 void

所以你的require_variadic_impl

template <template <class...> class Check, typename... Types>
struct require_variadic_impl : std::false_type {};

// Adding void_t here causes the compiler to not understand the partial specialiation
template <template <class...> class Check, typename... Types>
struct require_variadic_impl<Check, Check<Types...>> : std::true_type {};

这里有个问题,就是Check&lt;Types...&gt;,因为别名为enable_if,成功时返回void,但是require_variadic_impl的第二个参数不是void,所以部分当检查正确时,专业化最终失败。如果不是,则 enable_if 没有定义内部类型,部分特化也失败,并且再次使用基本情况。

但是,做起来很简单。我在这里提出一个更具可读性的实现,最终结果相同:

#include <iostream>
#include <type_traits>
#include <string>

template<class... Ts>
struct require_all_arithmetic : std::conjunction<std::is_arithmetic<Ts>...>
{};

template<template<class...> class Check, class... Ts>
struct require_variadic_tester : Check<Ts...>
{};

int main()
{
    std::cout << require_variadic_tester<require_all_arithmetic, double, double, double>::value << std::endl;
    std::cout << require_variadic_tester<require_all_arithmetic, double, std::string, double>::value << std::endl;
}

https://coliru.stacked-crooked.com/a/f9fb68e04eb0ad40

或者只是:

#include <iostream>
#include <type_traits>
#include <string>

template<class... Ts>
struct require_all_arithmetic : std::conjunction<std::is_arithmetic<Ts>...>
{};

int main()
{
    std::cout << require_all_arithmetic<double, double, double>::value << std::endl;
    std::cout << require_all_arithmetic<double, std::string, double>::value << std::endl;
}

但是,如果您需要对 sfinae 友好的检查,以及将“sfinae”友好检查映射到 true/false 的结构,则可以改用 constexpr 方法。它更简单:

template<class... Ts>
using require_all_arithmetic = std::enable_if_t<std::conjunction<std::is_arithmetic<Ts>...>::value>;

template<template<class...> class Check, class... Ts, class = Check<Ts...> >
constexpr bool require_variadic_tester_impl(int)
{ return true; }

template<template<class...> class Check, class... Ts>
constexpr bool require_variadic_tester_impl(unsigned)
{ return false; }

template<template<class...> class Check, class... Ts>
struct require_variadic_tester
{ static constexpr bool value = require_variadic_tester_impl<Check, Ts...>(42); };

int main()
{
    std::cout << require_variadic_tester<require_all_arithmetic, double, double, double>::value << std::endl;
    std::cout << require_variadic_tester<require_all_arithmetic, double, std::string, double>::value << std::endl;
}

该技术的工作原理如下:如果Check 失败,则只有第二个重载将编译,它返回false。但是,如果检查有效并且定义了内部 enable_if,则两个重载都将有效,但是,由于您传递了 int (42),并且第二个重载接收到 unsigned,第一个重载将是一个更好的匹配,返回true

https://coliru.stacked-crooked.com/a/bfe22ea099dd5749

最后,如果您希望检查始终是true_typefalse_type,那么您可以使用别名std::conditional,而不是继承:


template<template<class...> class Check, class... Ts>
using require_variadic_tester = 
   std::conditional_t<require_variadic_tester_impl<Check, Ts...>(42),
                      std::true_type, std::false_type>;

【讨论】:

  • 感谢您的回答!我确实需要 enable_if 内部的连词。我用更多上下文更新了原始问题
  • @Steve_Corrin 已编辑。
  • 哇!我很感激答案!我也很抱歉,我上面的评论是为了另一个答案,但刷新帖子让我一头雾水。您提出的方法很好,我一直在努力让我们的团队升级到 C++17,并且看到一些 constexpr 很酷可能有助于说服他们!
  • @Steve_Corrin constexpr 可用于 C++14(以及 C++11)。我已经用C++17std::conditional 编译了示例,仅此而已。其他所有内容都必须在功能齐全的C++14 编译器中有效。
  • 哦,我认为 constexpr 在 C++14 中的使用有限(仅在函数定义中)。我觉得我不久前尝试过它们,而我们的 Windows 版本对此很不满意。不过我会玩这个!
猜你喜欢
  • 2017-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-21
  • 2012-06-18
  • 2014-01-29
相关资源
最近更新 更多