限制可变模板参数
Restrict variadic template arguments
我们可以将可变参数模板参数限制为特定类型吗?即,实现这样的目标(当然不是真正的 C++):
struct X {};
auto foo(X... args)
这里我的意图是有一个接受可变数量 X
参数的函数。
最接近的是:
template <class... Args>
auto foo(Args... args)
但这接受任何类型的参数。
是的,这是可能的。首先,您需要决定是只接受类型,还是接受隐式可转换类型。我在示例中使用 std::is_convertible
是因为它更好地模仿了非模板参数的行为,例如long long
参数将接受 int
参数。如果出于某种原因您只需要接受该类型,请将 std::is_convertible
替换为 std:is_same
(您可能需要添加 std::remove_reference
和 std::remove_cv
)。
不幸的是,在 C++
缩小转换中,例如(long long
到 int
甚至 double
到 int
) 都是隐式转换。虽然在经典设置中您可以在这些情况发生时收到警告,但使用 std::is_convertible
则不会。至少不在通话中。如果您进行这样的分配,您可能会在函数体中收到警告。但是通过一些小技巧,我们也可以在调用站点使用模板获取错误。
废话不多说了:
测试台:
struct X {};
struct Derived : X {};
struct Y { operator X() { return {}; }};
struct Z {};
foo_x : function that accepts X arguments
int main ()
{
int i{};
X x{};
Derived d{};
Y y{};
Z z{};
foo_x(x, x, y, d); // should work
foo_y(x, x, y, d, z); // should not work due to unrelated z
};
C++20 概念
还没有,但很快。在 gcc trunk 中可用(2020 年 3 月)。这是最简单、清晰、优雅和安全的解决方案:
#include <concepts>
auto foo(std::convertible_to<X> auto ... args) {}
foo(x, x, y, d); // OK
foo(x, x, y, d, z); // error:
我们得到一个非常好的错误。尤其是
constraints not satisfied
甜甜的
处理缩小:
我没有在库中找到概念,所以我们需要创建一个:
template <class From, class To>
concept ConvertibleNoNarrowing = std::convertible_to<From, To>
&& requires(void (*foo)(To), From f) {
foo({f});
};
auto foo_ni(ConvertibleNoNarrowing<int> auto ... args) {}
foo_ni(24, 12); // OK
foo_ni(24, (short)12); // OK
foo_ni(24, (long)12); // error
foo_ni(24, 12, 15.2); // error
C++17
我们利用了非常好的fold expression:
template <class... Args,
class Enable = std::enable_if_t<(... && std::is_convertible_v<Args, X>)>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d, z); // OK
foo_x(x, x, y, d, z, d); // error
不幸的是,我们得到了一个不太明显的错误:
template argument deduction/substitution failed: [...]
缩小范围
我们可以避免缩小,但我们必须做一个特征is_convertible_no_narrowing
(也许命名不同):
template <class From, class To>
struct is_convertible_no_narrowing_impl {
template <class F, class T,
class Enable = decltype(std::declval<T &>() = {std::declval<F>()})>
static auto test(F f, T t) -> std::true_type;
static auto test(...) -> std::false_type;
static constexpr bool value =
decltype(test(std::declval<From>(), std::declval<To>()))::value;
};
template <class From, class To>
struct is_convertible_no_narrowing
: std::integral_constant<
bool, is_convertible_no_narrowing_impl<From, To>::value> {};
C++14
我们创建一个合取助手:
请注意 C++17
中会有一个 std::conjunction
,但需要 std::integral_constant
个参数
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
现在我们可以拥有我们的功能了:
template <class... Args,
class Enable = std::enable_if_t<
conjunction<std::is_convertible<Args, X>::value...>::value>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
C++11
仅对 C++14 版本进行了细微调整:
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
template <class... Args,
class Enable = typename std::enable_if<
conjunction<std::is_convertible<Args, X>::value...>::value>::type>
auto foo_x(Args... args) -> void {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
下面的解决方案呢?
--- 编辑 --- 改进了 bolov 和 Jarod42 的建议(谢谢!)
#include <iostream>
template <typename ... Args>
auto foo(Args... args) = delete;
auto foo ()
{ return 0; }
template <typename ... Args>
auto foo (int i, Args ... args)
{ return i + foo(args...); }
int main ()
{
std::cout << foo(1, 2, 3, 4) << std::endl; // compile because all args are int
//std::cout << foo(1, 2L, 3, 4) << std::endl; // error because 2L is long
return 0;
}
您可以声明 foo()
以接收所有类型的参数 (Args ... args
),但(递归地)仅针对一种类型实现它(本例中为 int
)。
static_assert
和辅助模板方法(c++11 解决方案)怎么样:
template <bool b>
int assert_impl() {
static_assert(b, "not convertable");
return 0;
}
template <class... Args>
void foo_x(Args... args) {
int arr[] {assert_impl<std::is_convertible<Args, X>::value>()...};
(void)arr;
}
多一个c++11这个使用"one-liner"基于sfinae的解决方案:
template <class... Args,
class Enable = decltype(std::array<int, sizeof...(Args)>{typename std::enable_if<std::is_convertible<Args, X>::value, int>::type{}...})>
void foo_x(Args... args) {
}
C++14
从 C++14 开始,您还可以使用 变量模板 、部分特化和 static_assert
来做到这一点。例如:
#include <type_traits>
template<template<typename...> class, typename...>
constexpr bool check = true;
template<template<typename...> class C, typename U, typename T, typename... O>
constexpr bool check<C, U, T, O...> = C<T, U>::value && check<C, U, O...>;
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible, int, T...>, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won't work, for S is not convertible to int
// f<int, S, int>();
}
如果出于某些未知原因不想使用 static_assert
,您还可以将 check
与 std::enable_if_t
结合使用作为 return 类型:
template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
// ...
}
等等...
C++11
在C++11中,你也可以设计一个方案,当遇到不被接受的类型时,立即停止递归。例如:
#include <type_traits>
template<bool...> struct check;
template<bool... b> struct check<false, b...>: std::false_type {};
template<bool... b> struct check<true, b...>: check<b...> {};
template<> struct check<>: std::true_type {};
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible<int, T>::value...>::value, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won't work, for S is not convertible to int
// f<int, S, int>();
}
如上所述,您也可以在 return 类型或任何您想要的地方使用 check
。
自 C++11 标准以来您已经拥有它。
一个简单的 std::array
(std::tuple
的特例,其中所有元组元素共享同一类型)就足够了。
但是,如果您想在模板函数中使用它,最好使用 ´std::initializer_list`,如下例所示:
template< typename T >
void foo( std::initializer_list<T> elements );
这是一个非常简单的解决方案,可以解决您的问题。使用可变模板参数也是一种选择,但会为您的代码增加不必要的复杂性。请记住,您的代码应该在一段时间后可供其他人阅读,包括您自己。
我们可以将可变参数模板参数限制为特定类型吗?即,实现这样的目标(当然不是真正的 C++):
struct X {};
auto foo(X... args)
这里我的意图是有一个接受可变数量 X
参数的函数。
最接近的是:
template <class... Args>
auto foo(Args... args)
但这接受任何类型的参数。
是的,这是可能的。首先,您需要决定是只接受类型,还是接受隐式可转换类型。我在示例中使用 std::is_convertible
是因为它更好地模仿了非模板参数的行为,例如long long
参数将接受 int
参数。如果出于某种原因您只需要接受该类型,请将 std::is_convertible
替换为 std:is_same
(您可能需要添加 std::remove_reference
和 std::remove_cv
)。
不幸的是,在 C++
缩小转换中,例如(long long
到 int
甚至 double
到 int
) 都是隐式转换。虽然在经典设置中您可以在这些情况发生时收到警告,但使用 std::is_convertible
则不会。至少不在通话中。如果您进行这样的分配,您可能会在函数体中收到警告。但是通过一些小技巧,我们也可以在调用站点使用模板获取错误。
废话不多说了:
测试台:
struct X {};
struct Derived : X {};
struct Y { operator X() { return {}; }};
struct Z {};
foo_x : function that accepts X arguments
int main ()
{
int i{};
X x{};
Derived d{};
Y y{};
Z z{};
foo_x(x, x, y, d); // should work
foo_y(x, x, y, d, z); // should not work due to unrelated z
};
C++20 概念
还没有,但很快。在 gcc trunk 中可用(2020 年 3 月)。这是最简单、清晰、优雅和安全的解决方案:
#include <concepts>
auto foo(std::convertible_to<X> auto ... args) {}
foo(x, x, y, d); // OK
foo(x, x, y, d, z); // error:
我们得到一个非常好的错误。尤其是
constraints not satisfied
甜甜的
处理缩小:
我没有在库中找到概念,所以我们需要创建一个:
template <class From, class To>
concept ConvertibleNoNarrowing = std::convertible_to<From, To>
&& requires(void (*foo)(To), From f) {
foo({f});
};
auto foo_ni(ConvertibleNoNarrowing<int> auto ... args) {}
foo_ni(24, 12); // OK
foo_ni(24, (short)12); // OK
foo_ni(24, (long)12); // error
foo_ni(24, 12, 15.2); // error
C++17
我们利用了非常好的fold expression:
template <class... Args,
class Enable = std::enable_if_t<(... && std::is_convertible_v<Args, X>)>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d, z); // OK
foo_x(x, x, y, d, z, d); // error
不幸的是,我们得到了一个不太明显的错误:
template argument deduction/substitution failed: [...]
缩小范围
我们可以避免缩小,但我们必须做一个特征is_convertible_no_narrowing
(也许命名不同):
template <class From, class To>
struct is_convertible_no_narrowing_impl {
template <class F, class T,
class Enable = decltype(std::declval<T &>() = {std::declval<F>()})>
static auto test(F f, T t) -> std::true_type;
static auto test(...) -> std::false_type;
static constexpr bool value =
decltype(test(std::declval<From>(), std::declval<To>()))::value;
};
template <class From, class To>
struct is_convertible_no_narrowing
: std::integral_constant<
bool, is_convertible_no_narrowing_impl<From, To>::value> {};
C++14
我们创建一个合取助手:
请注意 C++17
中会有一个 std::conjunction
,但需要 std::integral_constant
个参数
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
现在我们可以拥有我们的功能了:
template <class... Args,
class Enable = std::enable_if_t<
conjunction<std::is_convertible<Args, X>::value...>::value>>
auto foo_x(Args... args) {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
C++11
仅对 C++14 版本进行了细微调整:
template <bool... B>
struct conjunction {};
template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
: std::integral_constant<bool, Head && conjunction<Tail...>::value>{};
template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};
template <class... Args,
class Enable = typename std::enable_if<
conjunction<std::is_convertible<Args, X>::value...>::value>::type>
auto foo_x(Args... args) -> void {}
foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
下面的解决方案呢?
--- 编辑 --- 改进了 bolov 和 Jarod42 的建议(谢谢!)
#include <iostream>
template <typename ... Args>
auto foo(Args... args) = delete;
auto foo ()
{ return 0; }
template <typename ... Args>
auto foo (int i, Args ... args)
{ return i + foo(args...); }
int main ()
{
std::cout << foo(1, 2, 3, 4) << std::endl; // compile because all args are int
//std::cout << foo(1, 2L, 3, 4) << std::endl; // error because 2L is long
return 0;
}
您可以声明 foo()
以接收所有类型的参数 (Args ... args
),但(递归地)仅针对一种类型实现它(本例中为 int
)。
static_assert
和辅助模板方法(c++11 解决方案)怎么样:
template <bool b>
int assert_impl() {
static_assert(b, "not convertable");
return 0;
}
template <class... Args>
void foo_x(Args... args) {
int arr[] {assert_impl<std::is_convertible<Args, X>::value>()...};
(void)arr;
}
多一个c++11这个使用"one-liner"基于sfinae的解决方案:
template <class... Args,
class Enable = decltype(std::array<int, sizeof...(Args)>{typename std::enable_if<std::is_convertible<Args, X>::value, int>::type{}...})>
void foo_x(Args... args) {
}
C++14
从 C++14 开始,您还可以使用 变量模板 、部分特化和 static_assert
来做到这一点。例如:
#include <type_traits>
template<template<typename...> class, typename...>
constexpr bool check = true;
template<template<typename...> class C, typename U, typename T, typename... O>
constexpr bool check<C, U, T, O...> = C<T, U>::value && check<C, U, O...>;
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible, int, T...>, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won't work, for S is not convertible to int
// f<int, S, int>();
}
如果出于某些未知原因不想使用 static_assert
,您还可以将 check
与 std::enable_if_t
结合使用作为 return 类型:
template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
// ...
}
等等...
C++11
在C++11中,你也可以设计一个方案,当遇到不被接受的类型时,立即停止递归。例如:
#include <type_traits>
template<bool...> struct check;
template<bool... b> struct check<false, b...>: std::false_type {};
template<bool... b> struct check<true, b...>: check<b...> {};
template<> struct check<>: std::true_type {};
template<typename... T>
void f() {
// use std::is_convertible or whichever is the best trait for your check
static_assert(check<std::is_convertible<int, T>::value...>::value, "!");
// ...
}
struct S {};
int main() {
f<int, unsigned int, int>();
// this won't work, for S is not convertible to int
// f<int, S, int>();
}
如上所述,您也可以在 return 类型或任何您想要的地方使用 check
。
自 C++11 标准以来您已经拥有它。
一个简单的 std::array
(std::tuple
的特例,其中所有元组元素共享同一类型)就足够了。
但是,如果您想在模板函数中使用它,最好使用 ´std::initializer_list`,如下例所示:
template< typename T >
void foo( std::initializer_list<T> elements );
这是一个非常简单的解决方案,可以解决您的问题。使用可变模板参数也是一种选择,但会为您的代码增加不必要的复杂性。请记住,您的代码应该在一段时间后可供其他人阅读,包括您自己。