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

发布于 2025-02-02 11:00:39 字数 5413 浏览 3 评论 0 原文

C ++ 20函数参数包的声明器必须是或a pack扩展。例如:

// 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&lt; 0,0,0,0&gt;(1,2,3) good3 这样做,除非您调用 good3(1,2,3)它将失败,您必须编写 good3(1u,2u,2u,3u) )。我想在您说 good(1,2U,'\ 003')时可以起作用的函数 - 基本上好像您有无限数量的过载函数 good() good(unsigned)好(无符号,无符号)等。

good4 将有效类型 unsigned ,这可能是根据上下文的问题。具体来说,它可能会导致额外的 std :: string 在这样的函数中复制:

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

我的问题是:

  1. 我缺少一些技巧,允许一个人编写一个函数,该函数采用变量号码特定类型的论点? (我猜一个例外是C字符串,因为您可以将长度作为 template&lt; std :: size_t ... n&gt; void do_cstrings(const char(&amp; ... s)[n ]){/*....*/} ,但是我想这样做,例如std :: size_t)

  2. 为什么标准施加此限制?

update

康桓玮问为什么不使用 good4 与转发引用以避免额外的副本。我同意 good4 是我想做的最接近的,但是对于参数是不同类型的事实,有些烦恼,在某些地方参考也不起作用。例如,假设您以这样的方式编写代码:

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

您用好(1,2,3)对其进行测试,并且似乎有效。然后后来有人使用您的代码并写入好(1,2,sizeof(x)),并且由于编译器错误消息而失败。当然,答案是为(auto n:{unsigned(i)...})编写时代和转换操作员是不平凡的,您只想调用一次。

如果您的类型具有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&lt&lt; exptype(cnst&lt; 1&gt; gt; gt; ;)&gt;自动noreferrer“> this 。

update2

的示例

#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 :: prinitizer_list&lt; tracer&gt; ,该>需要在功能调用上进行额外的括号。

The declarator for a C++20 function parameter pack must either be a placeholder or a pack expansion. For example:

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

This seems to make it impossible to declare a function that takes a variable number of parameters of a specific type. Of course, good2 above will do it, but you have to specify some number of dummy template arguments, as in good2<0,0,0>(1,2,3). good3 sort of does it, except if you call good3(1,2,3) it will fail and you have to write good3(1U,2U,3U). I'd like a function that works when you say good(1, 2U, '\003')--basically as if you had an infinite number of overloaded functions good(), good(unsigned), good(unsigned, unsigned), etc.

good4 will work, except now the arguments aren't actually of type unsigned, which could be a problem depending on context. Specifically, it could lead to extra std::string copies in a function like this:

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

My questions are:

  1. Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type? (I guess the one exception is C strings, because you can make the length a parameter pack as in template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/}, but I want to do this for a type like std::size_t)

  2. Why does the standard impose this restriction?

update

康桓瑋 asked why not use good4 in conjunction with forwarding references to avoid extra copies. I agree that good4 is the closest to what I want to do, but there are some annoyances with the fact that the parameters are different types, and some places where references do not work, either. For example, say you write code like this:

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

You test it with good(1, 2, 3) and it seems to work. Then later someone uses your code and writes good(1, 2, sizeof(X)) and it fails with a confusing compiler error message. Of course, the answer was to write for (auto n : {unsigned(i)...}), which in this case is fine, but there might be other cases where you use the pack multiple times and the conversion operator is non-trivial and you only want to invoke it once.

Another annoying problem arises if your type has a constexpr conversion function that doesn't touch this, because in that case the function won't work on a forwarding reference. Admittedly this is highly contrived, but imagine the following program that prints "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;
}

If you change the argument to stringify to a forwarding reference stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i), it will fail to compile because of this.

update2

Here's a more comprehensive example showing why good4 isn't quite good enough if you want to avoid extra moves/copies:

#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;
}

The output of the program is:

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 ===

We are trying to replicate an inifinite sequence of overloaded functions that behave like f1, which is what the rejected P1219R2 would have given us. Unfortunately, the only approach that doesn't require an extra copy is to take a std::initializer_list<Tracer>, which requires an extra set of braces on function invocation.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

秋凉 2025-02-09 11:00:39

为什么标准施加此限制?

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

)的延伸到EWG

 #ewg孵化器:有利
sf fna sa
5 2 3 0 0
 

但是最终被EWG拒绝 c ++ 23

  sf fna sa
2 8 8 9 2
 

我认为理由是,尽管该提案写得很好,但实际的语言设施本质上不是一个有用的,尤其是由于C varargs逗号的混乱而导致的变化,尤其不足以承担重量:

最初与功能原型一起在C ++中引入了Varargs Ellipsis。当时,该功能在省略号之前不允许逗号。当C后来采用这些功能时,语法被更改以需要中间逗号,并强调了最后一个正式参数与varargs参数之间的区别。为了保留与C的兼容性,对C ++语法进行了修改,以允许用户添加中间逗号。因此,用户可以选择提供逗号或遗漏。

与函数参数包配对时,这会创建句法歧义,目前通过歧义规则解决:当出现在功能参数列表中的省略号可能是摘要(无名)声明器的一部分时,它被视为一个被视为一个如果参数的类型名称为未注释的参数包或包含auto,则包装声明;否则,它是varargs省略号。目前,该规则有效地歧义参数包时,每当这样做会产生良好的结果。

示例(状态):

 模板&lt; class ... t&gt;
void f(t ...); //用功能参数包宣布变异功能模板

t&gt; lt; lt; lt;
void f(t ...); //与void f(t,...)相同
 

使用均匀函数参数包,此歧义规则
需要重新访问
。解释
上面的第二个声明为具有同质的函数模板
功能参数包,这是这里提出的分辨率。经过
在参数列表和varargs Ellipsis之间需要一个逗号,
可以完全删除歧义规则,简化语言
不会失去任何功能或与c。

的兼容性

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

Why does the standard impose this restriction?

I'll focus on the "why's", as other answer already visits various workarounds.

P1219R2 (Homogeneous variadic function parameters) went as far as EWG

# EWG incubator: in favor
SF F N A SA
5  2 3 0 0

But was eventually rejected for C++23 by EWG

SF F N A SA
2  8 8 9 2

I think the rationale was that whilst the proposal was very well-written the actual language facility was not an essentially useful one, and particularly not enough to hold its weight given that it's a breaking change due to the C varargs comma mess:

The varargs ellipsis was originally introduced in C++ along with function prototypes. At that time, the feature did not permit a comma prior to the ellipsis. When C later adopted these features, the syntax was altered to require the intervening comma, emphasizing the distinction between the last formal parameter and the varargs parameters. To retain compatibility with C, the C++ syntax was modified to permit the user to add the intervening comma. Users therefore can choose to provide the comma or leave it out.

When paired with function parameter packs, this creates a syntactic ambiguity that is currently resolved via a disambiguation rule: When an ellipsis that appears in a function parameter list might be part of an abstract (nameless) declarator, it is treated as a pack declaration if the parameter's type names an unexpanded parameter pack or contains auto; otherwise, it is a varargs ellipsis. At present, this rule effectively disambiguates in favor of a parameter pack whenever doing so produces a well-formed result.

Example (status quo):

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, ...)

With homogeneous function parameter packs, this disambiguation rule
needs to be revisited
. It would be very natural to interpret the
second declaration above as a function template with a homogeneous
function parameter pack, and that is the resolution proposed here. By
requiring a comma between a parameter list and a varargs ellipsis, the
disambiguation rule can be dropped entirely, simplifying the language
without losing any functionality or degrading compatibility with C.

This is a breaking change, but likely not a very impactful one. [...]

戏剧牡丹亭 2025-02-09 11:00:39

为什么标准施加此限制?

最有可能是因为这会使(或创建并发症)用户与旧 c varargs函数具有几乎相同的语法(未命名参数),如下所示:

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

现在,如果第四函数 bad bad 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 
{
}

IMO由于C varargs的语法和功能参数包的相似性,上述似乎至少有些模棱两可。

来自 dcl.fct#22

当椭圆形出现在参数 - 解析范围的末尾而没有前面逗号的情况下时,就有句法歧义
在这种情况下,如果参数的类型将尚未扩展或包含自动的模板参数包称为模板参数包,则将省略号解析为抽象 - 单位的一部分;否则,将其解析为参数 - 删除范围的一部分。


我想要一个在您说良好时起作用的功能(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,那么这样做的一种方法是使用 std :: Conjunction std :: IS_SAME 组合的Sfinae原理 。

方法1

这里我们只需检查所有传递的参数是否为相同的类型。

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
    
}

工作demo


查看您的评论,看来您似乎想添加一个限制,只有一个程序才能在程序中起作用。

a)所有参数均为相同的类型

b)所有这些匹配的特定类型是 int std :: string

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

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

方法2

此处使用 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
    
}

工作demo


Why does the standard impose this restriction?

Most likely because this would confuse(or create complications) users with the old C varargs function that have almost same syntax(with unnamed parameter) as shown below:

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

Now if the fourth function bad was allowed:

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

IMO the above seems atleast a little ambiguous due to the similarity in the syntax of C varargs and a function parameter pack.

From dcl.fct#22:

There is a syntactic ambiguity when an ellipsis occurs at the end of a parameter-declaration-clause without a preceding comma.
In this case, the ellipsis is parsed as part of the abstract-declarator if the type of the parameter either names a template parameter pack that has not been expanded or contains auto; otherwise, it is parsed as part of the parameter-declaration-clause.


I'd like a function that works when you say good(1, 2U, '\003')

You can use std::common_type for this.

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

Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type?

The good3 given in your example is very readable and should be used from C++20. Though if one is using C++17 then one way to do this is using the SFINAE principle with a combination of std::conjunction and std::is_same as shown below.

Method 1

Here we simply check if all the arguments passed are of the same type.

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
    
}

Working demo


Looking at your comment it seems you want to add one more restriction that the program should work only when

a) all the arguments are of the same type

b) all of those matches a specific type say int, or std::string.

This can be done by adding a static_assert inside the function template:

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

Method 2

Here use the static_assert shown above to check if the arguments passes are of a specific type like 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
    
}

Working demo


虫児飞 2025-02-09 11:00:39

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

如果我正确理解这个问题,则可以通过添加代理函数来围绕它进行工作,该函数将参数投入并转发到实际实施中:

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

Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type?

If I understand the question correctly, you can work around it by adding a proxy function that casts and forwards arguments to actual implementation:

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)...);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文