0

我一直在试验可变参数模板和参数转发。我想我发现了一些不一致的行为。

为了说明,这个程序:

#include <iostream>
#include <typeinfo>
#include <tuple>
#include <cxxabi.h>

template <typename... Args> struct X {};

struct A {
    A () {}
    A (const A &) {
        std :: cout << "copy\n";
    }
};

template <typename T> const char * type_name () {
    return abi :: __cxa_demangle (typeid (T) .name (), 0, 0, NULL);
}

void foo () {}

template <typename... Args>
void foo (const A &, Args ... args) {
    std :: cout << type_name <X <Args...>> () << "\n“;    foo (args...);
}

int main () {
    foo (A(), A());
}

输出以下内容:

X<A, A>
copy
X<A>

即使 foo 的模板特化有一个 const 引用第一个参数,可变参数是按值传递的,因为模板类型被推导出为非引用类型,如X<A>输出所示。

所以,我咨询了Thomas Becker

template<typename T>
void foo(T&&);

在这里,以下适用:

  1. 当 foo 在 A 类型的左值上调用时,T 会解析为 A&,因此,根据上面的引用折叠规则,参数类型实际上变成了 A&。

  2. 当 foo 在类型 A 的右值上调用时,T 解析为 A,因此参数类型变为 A&&。

试试这个:

template <typename... Args>
void foo (const A &, Args && ... args) {
    std :: cout << type_name<X<Args...>>() << "\n";
    foo (args...);
}

哪个输出:

X<A, A>
X<A&>

现在我难住了。这里有 foo 的三个调用。在我的脑海中,main()应该推断foo<A,A,A>(A&&,A&&,A&&)(因为 A() 是一个未命名的右值,因此是一个引用),它重载解析为foo<A,A,A>(const A&,A&&,A&&). 这反过来又推导foo<A,A>(A&&,A&&)出来等等。

问题是:为什么X<A,A>有 non-reference As 但X<A&>有 reference A

这会导致问题,因为我不能std::forward在递归中使用。

4

1 回答 1

2

首先,我假设您粘贴了错误的主要内容,而正确的主要内容是:

 int main () {
        foo (A(), A(), A());
 }

我希望我猜对了,否则这篇文章的其余部分是无效的:)
其次,我不太擅长标准术语,所以我希望以下内容不会太不精确。

我经常觉得理解可变参数模板发生的事情的最简单方法就是自己生成编译器将有效执行的操作。

例如你的第一次尝试

void foo (const A &, Args ... args)

实际上会被编译器解压成类似这 3 个函数的东西:

void foo3 (const A & a, A a1, A a2)
void foo2 (const A & a, A a0)
void foo1 (const A & a)

在 main 中调用foo3(A(), A(), A());时,编译器将进行优化,只使用默认构造 a1 和 a2 而不是默认构造和复制。但是,当在 foo3 中调用 foo2 时,编译器无法再优化,因此在调用foo2(a1, a2) 时,foo2 中的参数 a0 必须是复制构造的。我们可以在痕迹中看到 a0 的复制构造(“复制”)

顺便说一句,我不明白为什么你通过 const ref 传递第一个元素,而其余的则通过值传递。为什么不通过 const-ref 传递所有内容?

void foo (const A &, const Args& ... args)

好的,现在关于您第二次尝试使用 rvalue-ref :

void foo (const A &, Args && ... args)

foo3(A(), A(), A());在 main 中调用时,A()是一个右值,因此适用此规则:

当 foo 在类型 A 的右值上调用时,T 解析为 A,因此参数类型变为 A&&。

所以 foo3 看起来像这样:

void foo3 (const A &, A && a1, A && a2) {
    std :: cout << type_name<X<A, A>>() << "\n";
    foo2 (a1, a2);
}

但是这里要小心。a1 和 a2 不再是右值。他们有一个名字,因此他们是左值。

所以现在在 foo2 里面,这个规则适用:

当 foo 在 A 类型的左值上调用时,T 会解析为 A&,因此,根据上面的引用折叠规则,参数类型实际上变成了 A&。

所以 foo2 中的 a0 类型实际上是 A& ,这就是我们可以X<A&>在跟踪中看到的原因。

要使用完美转发,我认为您需要放弃这个奇怪的假设:

这会导致问题,因为我不能在递归中使用 std::forward。

我不明白为什么。以下代码应该是正确的:

void foo(A&&, Args&&... args)
{
   std::cout << type-name<X<Args...>> () << "\n";
   foo(std::forward<Args>(args...));
}

当为递归调用 foo() 时,std::forward 会将每个 args... 从左值再次转换为右值,因此推导是正确的。

于 2011-06-17T13:00:59.327 回答