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) {}
我的问题是:
-
我缺少一些技巧,允许一个人编写一个函数,该函数采用变量号码特定类型的论点? (我猜一个例外是C字符串,因为您可以将长度作为 template&lt; std :: size_t ... n&gt; void do_cstrings(const char(&amp; ... s)[n ]){/*....*/} ,但是我想这样做,例如std :: size_t)
-
为什么标准施加此限制?
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:
-
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)
-
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.
发布评论
评论(3)
我将重点关注“为什么”,因为其他答案已经访问了各种解决方法。
)的延伸到EWG
但是最终被EWG拒绝 c ++ 23
我认为理由是,尽管该提案写得很好,但实际的语言设施本质上不是一个有用的,尤其是由于C varargs逗号的混乱而导致的变化,尤其不足以承担重量:
I'll focus on the "why's", as other answer already visits various workarounds.
P1219R2 (Homogeneous variadic function parameters) went as far as EWG
But was eventually rejected for C++23 by EWG
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:
最有可能是因为这会使(或创建并发症)用户与旧 c varargs函数具有几乎相同的语法(未命名参数),如下所示:
现在,如果第四函数
bad bad bad :
IMO由于C varargs的语法和功能参数包的相似性,上述似乎至少有些模棱两可。
来自 dcl.fct#22 :
可以使用
std :: common_type
。您的示例中给出的
good3
非常可读性,应从C ++ 20使用。尽管如果有人使用C ++ 17,那么这样做的一种方法是使用std :: Conjunction
和std :: IS_SAME
组合的Sfinae原理 。方法1
这里我们只需检查所有传递的参数是否为相同的类型。
工作demo
查看您的评论,看来您似乎想添加一个限制,只有一个程序才能在程序中起作用。
a)所有参数均为相同的类型
b)所有这些匹配的特定类型是
int
或std :: string
。这可以通过在函数模板中添加
static_assert
来完成:方法2
此处使用
static_assert
上面显示的检查参数通过是否为特定类型,例如>
int
。工作demo
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:
Now if the fourth function
bad
was allowed: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:
You can use
std::common_type
for this.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 ofstd::conjunction
andstd::is_same
as shown below.Method 1
Here we simply check if all the arguments passed are of the same type.
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
, orstd::string
.This can be done by adding a
static_assert
inside the function template:Method 2
Here use the
static_assert
shown above to check if the arguments passes are of a specific type likeint
.Working demo
如果我正确理解这个问题,则可以通过添加代理函数来围绕它进行工作,该函数将参数投入并转发到实际实施中:
If I understand the question correctly, you can work around it by adding a proxy function that casts and forwards arguments to actual implementation: