开发者

Is this variadic template argument deduction correct?

开发者 https://www.devze.com 2023-03-13 20:37 出处:网络
I\'ve been experimenting with variadic templates and argument forwarding. I think I\'ve found some inconsistent behaviour.

I've been experimenting with variadic templates and argument forwarding. I think I've found some inconsistent behaviour.

To illustrate, this program:

#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());
}

Outputs the following:

X<A, A>
copy
X<A>

Even though the template specialisation of foo has a const reference first argument, the variadic arguments are passed by value because the template types are deduced as non-reference types, as the X<A> output indicates.

So, I consult Thomas Becker:

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

Here, the following apply:

  1. When foo is called on an lvalue of type A, then T resolves to A& and hence, by the reference collapsing rules above, the argument type effectively becomes A&.

  2. When foo is called on an rvalue of type A, then T resolves to A, and hence the argument type becomes A&&.

And try this:

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

Which outputs:

X<A, A>
X<A&>

Now I'm stumped. There are three invocations of foo here. In my head, main() should deduce foo<A,A,A>(A&&,A&&,A&&) (because A() is an unnamed rvalue, hence a reference) which overload-resolves to foo<A,A,A>(const A&,A&&,A&&). This in turn deduces foo<A,A>(A&&,A&&) and so on.

Question is: how come X<A,A> has non-reference As but X<A&> has a reference A?

This causes a problem because I c开发者_StackOverflow社区an't use std::forward in the recursion.


First, I assume you pasted the wrong main and the correct one is :

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

I hope I guessed right, otherwise the rest of this post is invalid :)
Second, I'm not really good with standard terminology, so I hope the following is not too imprecise.

I often feel that the simplest way to understand what happen with variadic template is just to generate yourself what the compiler will effectively do.

For example your first try

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

will actually be unpack by the compiler to something like those 3 functions :

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

When calling foo3(A(), A(), A()); in the main the compiler will do an optimization and just default-construct a1 and a2 instead of default-construct and copy. However when calling foo2 inside foo3 the compiler cannot optimize anymore hence when calling foo2(a1, a2) the parameter a0 inside foo2 must be copy-constructed. And it's the copy-construction of a0 that we can see in the traces ("copy")

By the way, I don't understand why you pass the first element by const ref but the rest by value. Why not pass everything by const-ref ?

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

OK, now about your second try with rvalue-ref :

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

When calling foo3(A(), A(), A()); in the main, A() is an rvalue so this rule applies :

When foo is called on an rvalue of type A, then T resolves to A, and hence the argument type becomes A&&.

So foo3 looks like this:

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

But be careful here. a1 and a2 are NOT rvalue anymore. They have a name hence they are lvalue.

So now inside foo2, this rule applies :

When foo is called on an lvalue of type A, then T resolves to A& and hence, by the reference collapsing rules above, the argument type effectively becomes A&.

So a0 type inside foo2 will effectively be A& and that's why we can see X<A&> in the traces.

To use perfect forwarding I think you need to give-up this weird assumption :

This causes a problem because I can't use std::forward in the recursion.

I don't understand why. The following code should be correct :

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

When calling foo() for the recursion , std::forward will turns each args... from lvalue to rvalue again and so the deduction will be correct.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号