繁体   English   中英

为什么 C++ 函数参数包必须是占位符或包扩展?

[英]Why must C++ function parameter packs be placeholders or pack expansions?

C++20 函数参数包的声明符必须是占位符包扩展 例如:

// OK, template parameter pack only, no function parameter pack
template<unsigned ...I> void good1() {}

// OK function parameter pack is pack expansion of decltype(I)
template<unsigned ...I> void good2(decltype(I)...i) {}

// OK contains placeholder auto
void good3(std::same_as<unsigned> auto...i) {}

// OK contains placeholder auto
void good4(std::convertible_to<unsigned> auto...i) {}

// Error, no pack expansion or placeholder
template<unsigned = 0> void bad(unsigned...i) {}

这似乎使得声明一个接受可变数量的特定类型参数的函数变得不可能。 当然,上面的good2会这样做,但是您必须指定一些虚拟模板参数,如good2<0,0,0>(1,2,3) good3可以做到这一点,除非您调用good3(1,2,3)它将失败并且您必须编写good3(1U,2U,3U) 我想要一个在你说good(1, 2U, '\003')时可以工作的函数——基本上就像你有无数个重载函数good() , good(unsigned) , good(unsigned, unsigned) , ETC。

good4将起作用,但现在参数实际上不是unsigned类型,这可能是一个问题,具体取决于上下文。 具体来说,它可能会在这样的函数中导致额外的std::string副本:

void do_strings(std::convertible_to<std::string_view> auto...s) {}

我的问题是:

  1. 我是否错过了一些技巧,可以让人们编写一个函数,该函数采用可变数量的特定类型的参数? (我猜一个例外是 C 字符串,因为您可以将长度作为参数包,如template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/} ,但我想为 std::size_t 之类的类型执行此操作)

  2. 为什么标准会施加这种限制?

更新

康桓玮问为什么不将good4与转发引用一起使用以避免多余的副本。 我同意good4是最接近我想要做的,但是参数是不同类型的事实存在一些烦恼,并且在某些地方引用也不起作用。 例如,假设您编写如下代码:

void
good4(std::convertible_to<unsigned> auto&&...i)
{
  for (auto n : {i...})
    std::cout << n << " ";
  std::cout << std::endl;
}

你用good(1, 2, 3)测试它,它似乎工作。 然后后来有人使用您的代码并编写good(1, 2, sizeof(X))并失败并显示令人困惑的编译器错误消息。 当然,答案是写for (auto n : {unsigned(i)...}) ,在这种情况下这很好,但可能还有其他情况,您多次使用包并且转换运算符是非-trivial,你只想调用它一次。

如果您的类型有一个不涉及this的 constexpr 转换函数,则会出现另一个烦人的问题,因为在这种情况下,该函数将无法在转发引用上工作。 诚然,这是非常做作的,但想象一下以下打印“11”的程序:

template<std::size_t N> std::integral_constant<std::size_t, N> cnst = {};

constexpr std::tuple tpl ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');

inline const char *
stringify(std::convertible_to<decltype(cnst<1>)> auto...i)
{
  static constexpr const char str[] = { get<i>(tpl)..., '\0' };
  return str;
}

int
main()
{
  std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
}

如果将stringify的参数更改为转发引用stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i) ,它将因此编译失败。

更新2

这是一个更全面的示例,说明如果您想避免额外的移动/复制,为什么good4不够好:

#include <concepts>
#include <iostream>
#include <initializer_list>
#include <concepts>

struct Tracer {
  Tracer() { std::cout << "default constructed" << std::endl; }
  Tracer(int) { std::cout << "int constructed" << std::endl; }
  Tracer(const Tracer &) { std::cout << "copy constructed" << std::endl; }
  Tracer(Tracer &&) { std::cout << "move constructed" << std::endl; }
  void do_something() const {}
};

void
f1(Tracer t1, Tracer t2, Tracer t3)
{
  t1.do_something();
  t2.do_something();
  t3.do_something();
}

void
f2(std::convertible_to<Tracer> auto ...ts)
{
  (Tracer{ts}.do_something(), ...); // binary fold over comma
}

void
f3(std::convertible_to<Tracer> auto&& ...ts)
{
  (Tracer{std::forward<decltype(ts)>(ts)}.do_something(), ...);
}

void
f4(std::initializer_list<Tracer> tl)
{
  for (const auto &t : tl)
    t.do_something();
}

void
f5(std::convertible_to<Tracer> auto&& ...ts)
{
  std::initializer_list<Tracer> tl { std::forward<decltype(ts)>(ts)... };
  for (const auto &t : tl)
    t.do_something();
}

int
main()
{
  Tracer t;
  std::cout << "=== f1(t, 0, {}) ===" << std::endl;
  f1(t, 0, {});
  std::cout << "=== f2(t, 0, Tracer{}) ===" << std::endl;
  f2(t, 0, Tracer{});
  std::cout << "=== f3(t, 0, Tracer{}) ===" << std::endl;
  f3(t, 0, Tracer{});
  std::cout << "=== f4({t, 0, {}}) ===" << std::endl;
  f4({t, 0, {}});
  std::cout << "=== f5(t, 0, Tracer{}) ===" << std::endl;
  f5(t, 0, Tracer{});
  std::cout << "=== done ===" << std::endl;
}

程序的输出是:

default constructed
=== f1(t, 0, {}) ===
default constructed
int constructed
copy constructed
=== f2(t, 0, Tracer{}) ===
default constructed
copy constructed
copy constructed
int constructed
copy constructed
=== f3(t, 0, Tracer{}) ===
default constructed
copy constructed
int constructed
move constructed
=== f4({t, 0, {}}) ===
copy constructed
int constructed
default constructed
=== f5(t, 0, Tracer{}) ===
default constructed
copy constructed
int constructed
move constructed
=== done ===

我们正在尝试复制一个无限序列的重载函数,其行为类似于f1 ,这是被拒绝的 P1219R2 会给我们的。 不幸的是,唯一不需要额外副本的方法是采用std::initializer_list<Tracer> ,这需要在函数调用时使用一组额外的大括号。

为什么标准会施加这种限制?

我将专注于“为什么”,因为其他答案已经访问了各种解决方法。

P1219R2齐次变参函数参数)达到 EWG

 # EWG incubator: in favor SF FNA SA 5 2 3 0 0

最终被 EWG 拒绝了 C++23

 SF FNA SA 2 8 8 9 2

我认为理由是,虽然提案写得非常好,但实际的语言工具并不是一个本质上有用的工具,特别是不足以保持其重要性,因为它是由于 C varargs 逗号混乱而造成的重大变化:

可变参数省略号最初是在 C++ 中与函数原型一起引入的。 当时,该功能不允许在省略号之前使用逗号。 当 C 后来采用这些特性时,语法被改变为需要中间的逗号,强调最后一个形式参数和可变参数之间的区别。 为了保持与 C 的兼容性,修改了 C++ 语法以允许用户添加中间逗号。 因此,用户可以选择提供逗号或将其省略。

当与函数参数包配对时,这会产生语法歧义,目前通过消歧规则解决:当出现在函数参数列表中的省略号可能是抽象(无名)声明符的一部分时,如果满足以下条件,则将其视为包声明参数的类型命名为未扩展的参数包或包含 auto; 否则,它是一个可变参数省略号。 目前,只要这样做会产生格式良好的结果,此规则就会有效地消除歧义,支持参数包。

示例(现状):

 template <class... T> void f(T...); // declares a variadic function template with a function parameter pack template <class T> void f(T...); // same as void f(T, ...)

对于同构函数参数包,需要重新审视这个消歧规则 将上面的第二个声明解释为具有同质函数参数包的函数模板是很自然的,这就是这里提出的解决方案。 通过在参数列表和可变参数省略号之间使用逗号,可以完全删除消歧规则,从而在不丢失任何功能或降低与 C 的兼容性的情况下简化语言。

这是一个突破性的变化,但可能不是一个非常有影响力的变化。 [...]

为什么标准会施加这种限制?

很可能是因为这会使用户与具有几乎相同语法(带有未命名参数)的旧C 可变参数函数混淆(或产生复杂性),如下所示:

//this is a C varargs function
void good3(int...) 
           ^^^^^^
{
    
}

现在如果允许第四个函数bad

 
template<int = 0> void bad(int...i)
                           ^^^^^^^ --->this is very likely to confuse users as some may consider it as a C varargs function instead of a function parameter pack 
{
}

由于 C varargs 的语法和函数参数包的相似性,IMO 以上似乎至少有点模棱两可。

dcl.fct#22

当省略号出现在参数声明子句的末尾而没有前面的逗号时,就会出现句法歧义 在这种情况下,如果参数的类型命名为尚未扩展的模板参数包或包含 auto,则省略号将被解析为抽象声明符的一部分; 否则,它被解析为参数声明子句的一部分。


我想要一个在你说好时起作用的函数(1, 2U, '\003')

您可以为此使用std::common_type

template <typename... T, typename l= std::common_type_t<T...>>
void func(T&&... args) {
}

我是否错过了一些技巧,可以让人们编写一个函数,该函数接受可变数量的特定类型的参数

您的示例中给出的good3非常易读,应该从 C++20 开始使用。 虽然如果使用 C++17,那么一种方法是使用 SFINAE 原理与std::conjunctionstd::is_same的组合,如下所示。

方法一

在这里,我们简单地检查传递的所有参数是否属于同一类型。

template <typename T, typename ... Args>
std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
func(const T & first, const Args & ... args)
{
    std::cout<<"func called"<<std::endl;
    return true;
}
int main()
{
    func(4,5,8);    //works
    //func(4,7.7); //wont work as types are different
    std::string s1 = "a", s2 = "b"; 
    func(s1, s2);  //works
    //func(s1, 2);   //won't work as types are different
    
}

工作演示


查看您的评论,您似乎想再添加一个限制,即该程序仅在以下情况下才能工作

a) 所有参数都是同一类型

b)所有这些都匹配特定类型,例如intstd::string

这可以通过在函数模板中添加static_assert来完成:

static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");

方法二

这里使用上面显示的static_assert来检查传递的参数是否属于特定类型,例如int

template <typename T, typename ... Args>
std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
func(const T & first, const Args & ... args)
{
    static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");
    std::cout<<"func called"<<std::endl;
    return true;
}

int main()
{
    func(3,3);    //works
    //func(4,7.7); //wont work as types are different
    std::string s1 = "a", s2 = "b"; 
    //func(s1, s2);  //won't work as even though they are of the same type but not int type
    
}

工作演示


我是否错过了一些技巧,可以让人们编写一个函数,该函数采用可变数量的特定类型的参数?

如果我正确理解了这个问题,您可以通过添加一个代理函数来解决它,该函数将参数转换并转发到实际实现:

unsigned wrap_sum(auto&&...args) {
  return []<std::size_t...I>(std::index_sequence<I...>,
                             std::conditional_t<true, unsigned, decltype(I)> ... args) {

    return (0u + ... + args);

  }(std::make_index_sequence<sizeof...(args)>{},
    std::forward<decltype(args)>(args)...);
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM