为什么 C++ 函数参数包必须是占位符或包扩展?
Why must C++ function parameter packs be placeholders or pack expansions?
C++20 函数参数包的声明符必须是 placeholder or a pack expansion。例如:
// 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, '[=21=]3')
时起作用的函数——基本上就好像你有无限数量的重载函数 good()
、good(unsigned)
、good(unsigned, unsigned)
等
good4
将起作用,除了现在参数实际上不是 unsigned
类型,这可能是一个问题,具体取决于上下文。具体来说,它可能会导致函数中有额外的 std::string
副本,如下所示:
void do_strings(std::convertible_to<std::string_view> auto...s) {}
我的问题是:
我是否遗漏了一些技巧,可以让人们编写一个函数,该函数接受特定类型的可变数量的参数? (我想唯一的例外是 C 字符串,因为你可以像 template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/}
那样将长度作为参数包,但我想对 std::size_t 这样的类型执行此操作)
为什么标准有这个限制?
更新
康桐薇询问为什么不将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)...})
,在这种情况下没问题,但可能还有其他情况,您多次使用 pack 并且转换运算符很重要,您只想调用它一次.
如果您的类型具有不触及 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)..., '[=13=]' };
return str;
}
int
main()
{
std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
}
如果将 stringify
的参数更改为转发引用 stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i)
,它将无法编译,因为 this.
update2
这里有一个更全面的示例,说明如果您想避免额外的 moves/copies:
为什么 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>
,这需要在函数调用时使用一组额外的大括号。
Why does the standard impose this restriction?
很可能是因为这会使用户与旧的 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
{
}
IMO 由于 C 可变参数和函数参数包语法的相似性,以上内容似乎至少有点模棱两可。
来自 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, '[=28=]3')
您可以使用 std::common_type
来实现。
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?
您的示例中给出的 good3
可读性很强,应该从 C++20 开始使用。尽管如果使用 C++17,那么一种方法是使用 SFINAE 原理结合 std::conjunction
和 std::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) 所有这些都匹配特定类型,例如 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");
方法二
这里使用上面显示的 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
}
Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific 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)...);
}
Why does the standard impose this restriction?
我将专注于“为什么”,因为其他答案已经访问了各种解决方法。
P1219R2(齐次可变函数参数)达到了 EWG
# EWG incubator: in favor
SF F N A SA
5 2 3 0 0
但是 was eventually rejected EWG 的 C++23
SF F N A SA
2 8 8 9 2
我认为理由是虽然提案非常 well-written 但实际的语言工具并不是一个本质上有用的工具,特别是由于 C可变参数逗号混乱:
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. [...]
C++20 函数参数包的声明符必须是 placeholder or a pack expansion。例如:
// 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, '[=21=]3')
时起作用的函数——基本上就好像你有无限数量的重载函数 good()
、good(unsigned)
、good(unsigned, unsigned)
等
good4
将起作用,除了现在参数实际上不是 unsigned
类型,这可能是一个问题,具体取决于上下文。具体来说,它可能会导致函数中有额外的 std::string
副本,如下所示:
void do_strings(std::convertible_to<std::string_view> auto...s) {}
我的问题是:
我是否遗漏了一些技巧,可以让人们编写一个函数,该函数接受特定类型的可变数量的参数? (我想唯一的例外是 C 字符串,因为你可以像
template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/}
那样将长度作为参数包,但我想对 std::size_t 这样的类型执行此操作)为什么标准有这个限制?
更新
康桐薇询问为什么不将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)...})
,在这种情况下没问题,但可能还有其他情况,您多次使用 pack 并且转换运算符很重要,您只想调用它一次.
如果您的类型具有不触及 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)..., '[=13=]' };
return str;
}
int
main()
{
std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
}
如果将 stringify
的参数更改为转发引用 stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i)
,它将无法编译,因为 this.
update2
这里有一个更全面的示例,说明如果您想避免额外的 moves/copies:
为什么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>
,这需要在函数调用时使用一组额外的大括号。
Why does the standard impose this restriction?
很可能是因为这会使用户与旧的 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
{
}
IMO 由于 C 可变参数和函数参数包语法的相似性,以上内容似乎至少有点模棱两可。
来自 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, '[=28=]3')
您可以使用 std::common_type
来实现。
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?
您的示例中给出的 good3
可读性很强,应该从 C++20 开始使用。尽管如果使用 C++17,那么一种方法是使用 SFINAE 原理结合 std::conjunction
和 std::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) 所有这些都匹配特定类型,例如 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");
方法二
这里使用上面显示的 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
}
Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific 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)...);
}
Why does the standard impose this restriction?
我将专注于“为什么”,因为其他答案已经访问了各种解决方法。
P1219R2(齐次可变函数参数)达到了 EWG
# EWG incubator: in favor SF F N A SA 5 2 3 0 0
但是 was eventually rejected EWG 的 C++23
SF F N A SA 2 8 8 9 2
我认为理由是虽然提案非常 well-written 但实际的语言工具并不是一个本质上有用的工具,特别是由于 C可变参数逗号混乱:
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. [...]