限制可变参数模板参数

Restrict variadic template arguments

提问人:bolov 提问时间:9/23/2016 最后编辑:Yakk - Adam Nevraumontbolov 更新时间:8/7/2022 访问量:7882

问:

我们可以将可变参数模板参数限制为某种类型吗?即,实现这样的东西(当然不是真正的 C++):

struct X {};

auto foo(X... args)

在这里,我的目的是拥有一个接受可变数量参数的函数。X

我们最接近的是:

template <class... Args>
auto foo(Args... args)

但这接受任何类型的参数。

C 模板 variadic-templates ++17 c ++-faq

评论

0赞 AndyG 9/23/2016
使用非类型模板参数,可以直接使用类型。例如int... args
5赞 Jarod42 9/23/2016
std::initializer_list<X>可能是您想要的接口。
0赞 bolov 9/23/2016
@Jarod42 这也是一种方式。更像是一种解决方法。两者都有缺点。例如,您不能从 .你必须使用语法。使用可变参数模板,实现更加复杂,您很容易搞砸。对于概念来说,情况将不再如此,因为概念变得非常容易和干净。std::initializer_list{p1, p2, p3}
0赞 L. F. 11/3/2019
@AndyG 不,你不能。扩展模式必须至少包含一个模板参数包。
0赞 AndyG 11/3/2019
@L.F.这是一个参数包。周围的环境是暗示的。这就是我的意思。template<>

答:

58赞 bolov 9/23/2016 #1

是的,这是可能的。首先,您需要决定是只接受该类型,还是要接受隐式可转换类型。我在示例中使用它是因为它更好地模仿了非模板化参数的行为,例如,参数将接受参数。如果出于某种原因,您只需要接受该类型,请替换为(您可能需要添加 和 )。std::is_convertiblelong longintstd::is_convertiblestd:is_samestd::remove_referencestd::remove_cv

不幸的是,在缩小转换中,例如(到甚至到)是隐式转换。虽然在经典设置中,您可以在发生这些警告时收到警告,但 .至少不是在电话中。如果进行此类分配,则可能会在函数正文中收到警告。但是通过一个小技巧,我们也可以使用模板在呼叫站点上获得错误。C++long longintdoubleintstd::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:

我们得到了一个非常好的错误。尤其是

未满足的约束条件

是甜蜜的。

处理缩小:

我在库中没有找到一个概念,所以我们需要创建一个:

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

我们利用了非常漂亮的折叠表达式

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

不幸的是,我们得到了一个不太清楚的错误:

模板参数推导/替换失败:[...]

缩小

我们可以避免缩小范围,但我们必须烹饪一个特征(也许以不同的方式命名):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

评论

1赞 AndyG 9/23/2016
SFINAE 使用折叠表达式真是太棒了。
0赞 Yakk - Adam Nevraumont 9/23/2016
你有倒退的概念论点吗?To From 似乎更正确。
1赞 9/23/2016
这个问答是在stackoverflow中发布博客的正确方式吗?
8赞 bolov 9/23/2016
@DieterLücking它不是博客。是的,这绝对是正确的方式:stackoverflow.com/help/self-answer.这是主题。一个清晰而具体的编程问题。
5赞 W.F. 9/23/2016
@DieterLücking只要问答值得像这里一样分享,我认为没有理由不......
4赞 max66 9/23/2016 #2

下面的解决方案呢?

---编辑---改进了 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 ... argsint

评论

1赞 bolov 9/23/2016
这是一个很好的解决方案。但是你不能总是做递归。或者你不想。我可以删除吗?否则会令人困惑。template <class... Args> auto foo(Args... args) = delete;
0赞 max66 9/23/2016
@bolov - 是的:递归是一个极限;关于,我在第一次尝试中就使用了它,但仅适用于我的 g++ (4.9.2);我的 clang++ (3.5) 给了我很多错误delete
1赞 Jarod42 9/23/2016
您可能更喜欢重载而不是专业化。
0赞 max66 9/23/2016
@Jarod - 我是个白痴:我必须在专业化之前实现重载;谢谢。
0赞 max66 9/23/2016
@bolog - 没有工作,因为我是个白痴;我必须将基础定义为重载、非模板(遵循 Jarod42 建议)并在专业化之前完成;再次感谢。deletefoo()
1赞 W.F. 9/23/2016 #3

怎么样和帮助模板方法(c++11解决方案):static_assert

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 使用基于 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) {
}
7赞 skypjack 9/23/2016 #4

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>();
}

如果由于某些未知原因不想使用,也可以与 as as a return type 结合使用:checkstd::enable_if_tstatic_assert

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>();
}

如上所述,您也可以在返回类型或任何您想要的地方使用。check

评论

0赞 edmz 9/24/2016
好。只是个人喜好:我会或任何其他更有意义的名字。单个字母通常保留给类型,而 callables et similia 具有更多解释性名称。再次,恕我直言。s/typename C/typename Condition
0赞 skypjack 9/24/2016
@black我同意你的看法(或多或少),但我通常不关心这些简短例子的有意义的名称。;-)
0赞 edmz 9/24/2016
这也是事实。:)
1赞 Jarod42 9/24/2016
all_of/check可以在没有递归的情况下实现template <bool...> struct bools{}; template <bool...bs> struct all_of : std::is_same<bools<true, bs...>, bools<bs..., true>> {};
0赞 skypjack 9/24/2016
@Jarod42起首部分。这个好把戏。我想展示如何使用 C++14 中的变量模板做到这一点(因为没有提到答案),但我可以在第二个示例中使用此模式。谢谢。
1赞 Jorge Bellon 9/24/2016 #5

自 C++11 标准以来,您已经拥有了它。

一个简单的(所有元组元素共享相同类型的特殊情况)就足够了。std::arraystd::tuple

但是,如果要在模板函数中使用它,最好使用“std::initializer_list”,如以下示例所示:

template< typename T >
void foo( std::initializer_list<T> elements );

这是一个非常简单的解决方案,可以解决您的问题。使用可变参数模板参数也是一种选择,但会给代码增加不必要的复杂性。请记住,您的代码应该在一段时间后被其他人(包括您自己)阅读。

0赞 user3188445 8/7/2022 #6

@463035818_is_not_a_number的答案没有解决这个问题的一个方面是如何避免创建无端的论点副本。由于这篇博文,这里有一个想法,可以最大限度地减少复制成本高昂的复制参数:

struct X { void use() { /* ... */ } /* ... */ };

template<typename Want, typename Have>
inline std::conditional_t<std::is_same_v<Want, Have>, Want &&, Want>
local_copy(Have &in)
{
  return static_cast<Have&&>(in);
}

template<std::convertible_to<X> ...T>
void
foo1(T&&...t)
{
  // Unary fold over comma operator
  (local_copy<X, T>(t).use(), ...);
}

// Another way to do it
template<std::convertible_to<X> ...T>
void
foo2(T&&...t)
{
  auto use = []<typename U>(U &&arg) {
    decltype(auto) x = local_copy<X, U>(arg);
    x.use();
  };
  (use(std::forward<T>(t)), ...);
}

关键思想是将返回其参数的副本,除非该参数是完全所需类型的右值,在这种情况下,它将返回对其参数的右值引用。因此,如果您打电话,请说:local_copy

X x;
foo1(X{}, x);

第一个参数,将在 调用之前构造一个临时对象,并且只能就地修改这个临时对象,并且永远不会复制它。相比之下,第二个参数被复制,确保对参数的任何修改都保留在函数的本地。X{}foo1foo1xfoo1