gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-05 00:42:43 24 4
gpt4 key购买 nike

C++20 函数参数包的声明符必须是 placeholderpack 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) {}

我的问题是:

  1. 我是否遗漏了一些技巧,可以让人们编写一个函数,该函数接受可变数量的特定类型的参数? (我想一个异常(exception)是 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)...}) ,在这种情况下很好,但可能还有其他情况,您多次使用包并且转换运算符很重要,您只想调用它一次。

如果你的类型有一个不涉及 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) , 会因为 this 导致编译失败.

更新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> ,这需要在函数调用时使用一组额外的大括号。

最佳答案

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

我认为理由是,虽然该提案写得非常好,但实际的语言工具并不是一个本质上有用的工具,尤其是由于 C varargs 逗号困惑导致它是一个重大变化,因此不足以支撑它的重要性:

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 ruleneeds to be revisited. It would be very natural to interpret thesecond declaration above as a function template with a homogeneousfunction parameter pack, and that is the resolution proposed here. Byrequiring a comma between a parameter list and a varargs ellipsis, thedisambiguation rule can be dropped entirely, simplifying the languagewithout losing any functionality or degrading compatibility with C.

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

关于c++ - 为什么 C++ 函数参数包必须是占位符或包扩展?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72420722/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com