如何简化模板模板参数中的enable_if别名

How to simplify enable_if alias in template template parameter

本文关键字:if 别名 enable 何简化 参数      更新时间:2023-10-16

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

这是我想编写的代码示例及其结果。我正在使用 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

下面确实做了我想要的,但 tbf 我并不真正了解如何。

// 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 {} ;

特别是,我不确定为什么在std::true_typeimpl部分专业化中需要void_t.

我想得到的是一个require_variadic_tester,它接受一个可变参数模板化别名,类似于enable_if<conjunction<check<T...>>::value>,并给我真或假。可悲的是,无论出现什么类型,下面的都返回 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_*_<container>别名,但所有这些要求的测试大约是 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 ;-))

以下是您的require_tester<require_arithmetic, double, double, int>发生的情况:

这与require_tester的部分特化不匹配,<Check, void>只有两个模板参数,所以我们使用主模板

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<Check, T> = require_tester_impl<require_arithmetic, double>.首先,由于require_tester_impl的模板参数是

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

并且只给出了两个明确的模板参数,我们知道实际的模板参数是<require_arithmetic, double, void>的。现在我们需要看看这是否与require_template_impl的部分特化相匹配,所以我们尝试匹配:

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

所以模板参数推导找到Check = require_arithmeticT1 = double.void_t<Check<T1>>类不会导致扣除任何CheckT1。但是推导的参数值必须代入,我们发现void_t<Check<T1>>void_t<require_arithmetic<double>>void。这确实匹配模板参数中的void,因此部分专用化确实匹配,并且require_template_impl<require_arithmetic, double, void>继承std::true_type,而不是std::false_type

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

回到require_testervalue成员,它通过require_tester<require_arithmetic, int>::valuerequire_tester<require_arithmetic>::value递归地查找require_tester<require_arithmetic, double, int>::value,这与require_tester<require_arithmetic, void>::value相同。 所有value成员都是真的,所以最后的value都是真的。

虽然我会简化一点:

  1. voidrequire_tester递归中是不必要的,并导致奇怪的"事实",即require_tester<Anything, void>::value总是正确的。最好从主require_tester模板中删除= void默认值,并改为template <template <class> class Check> require_tester<Check>基本情况。

  2. 您在require_tester主模板中的value表达式总是为conjunction提供两个模板参数,因此它并没有真正使用它的可变参数属性,您也可以编写require_tester_impl<...>::value && require_tester<......>::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...> {};

在 godbolt 上查看修改后的程序,并得到所需的结果。

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

我想得到的是一个require_variadic_tester,它接受一个可变参数模板化别名,类似于enable_if<conjunction<check<T...>>::value>,并给我真或假。可悲的是,无论出现什么类型,下面的都返回 false

你确定要conjunction<check<T...>>吗?

还是你想要conjunction<check<T>...>

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

在第二种情况下,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: nn");
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: %sn", string_generic_var_check);
std::printf("t Double: %sn", double_generic_var_check);

你得到

Generic Variadic: 
String: false
Double: true

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

尝试将std::void_t<Ts...>视为"如果启用了所有Ts则启用"。

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<Types...>,因为它被别名为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>;