C++20函数参数包的声明符必须是
placeholder
或
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, '\003')
--基本上就好像你有无限多的重载函数
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这样的类型执行此操作)
-
为什么标准会施加这种限制?
使现代化
·询问为什么不使用
好4
结合转发引用以避免额外的副本。我同意
好4
是最接近我想要做的,但参数是不同类型的,有些地方引用也不起作用,这让我很恼火。例如,假设您编写的代码如下:
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)...})
,在这种情况下,这是可以的,但在其他情况下,您可能会多次使用该包,并且转换运算符非常简单,您只想调用它一次。
如果您的类型有一个不接触的constexpr转换函数,就会出现另一个恼人的问题
this
,因为在这种情况下,函数将无法在转发引用上工作。诚然,这是高度人为的,但想象一下下面打印“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)
,它将无法编译,因为
this
。
更新2
下面是一个更全面的例子来说明为什么
好4
如果你想避免额外的移动/副本,还不够好:
#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>
,在函数调用时需要一组额外的大括号。