dummydev dummydev - 2 months ago 10
C++ Question

How come pack expanded variables do not get passed by reference?

Please consider the following program:

#include <iostream>


template <typename T, typename ...Ts>
struct Foo {
template <typename ...Us>
static void bar(T& oT0, T& oT1, const T& iT0, const T& iT1, Us... args) {
std::cout << " -> Foo<...>::bar() enter [ " << oT0 << ", " << oT1 << " ]" << std::endl;
Foo<T>::bar(oT0, oT1, iT0, iT1);
Foo<Ts...>::bar(args...);
std::cout << " <- Foo<...>::bar() exit [ " << oT0 << ", " << oT1 << " ]" << std::endl;
}
};

template <typename T>
struct Foo<T> {
static void bar(T& oT0, T& oT1, const T& iT0, const T& iT1) {
std::cout << " -> Foo<>::bar() enter [ " << oT0 << ", " << oT1 << " ]" << std::endl;
oT0 = iT0;
oT1 = iT1;
std::cout << " <- Foo<>::bar() exit [ " << oT0 << ", " << oT1 << " ]" << std::endl;
}
};


int main() {
int i0 = -1,
i1 = 0;
float f0 = -97.18f,
f1 = 3.141592f;
std::cout << "( "<< i0 << ", " << i1 << "; " << f0 << ", " << f1 << " ) " << std::endl;

Foo<int, float, int>::bar(i0, i1, 0, 1, f0, f1, 18.f, -7.f, i0, i1, 4, 17);
std::cout << "( "<< i0 << ", " << i1 << "; " << f0 << ", " << f1 << " ) " << std::endl;

Foo<float>::bar(f0, f1, 18.f, -7.f);
std::cout << "( " << f0 << ", " << f1 << " ) " << std::endl;

Foo<float, int>::bar(f0, f1, 2.71f, 9000.1f, i0, i1, 4, 17);
std::cout << "( "<< i0 << ", " << i1 << "; " << f0 << ", " << f1 << " ) " << std::endl;

return 0;
}


And its commented output (debug output removed for clarity but available at IDEone):

( -1, 0; -97.18, 3.14159 ) // initial values
( 0, 1; -97.18, 3.14159 ) // ints only set once?! floats unchanged?!
( 18, -7 )
( 0, 1; 2.71, 9000.1 ) // ints unchanged?!


I must be missing something obvious here: from the above, calling
Foo<...>::bar(...)
only modifies the first set of two non-const parameters. Why are the values of the next arguments left untouched within
main
?

Answer

You need to use references on the varargs part of the template or the arguments beyond the first four will be passed to the initial call by value (copying to local variables). The subsequent calls will mutate those copied values, not the original arguments passed.

Since you want to accept rvalues for some of the arguments, using perfect forwarding for varargs to preserve the original types. Just change:

static void bar(T& oT0, T& oT1, const T& iT0, const T& iT1, Us... args) {

to:

// Receive varargs as rvalue references to allow perfect forwarding instead
// of receiving them as copies
static void bar(T& oT0, T& oT1, const T& iT0, const T& iT1, Us&&... args) {

and change:

Foo<Ts...>::bar(args...);

to:

// Per earlier link, must explicitly template std::forward call in varargs case
Foo<Ts...>::bar(std::forward<Us>(args)...); 

which should receive and forward the varargs correctly, so they are received in the nested calls as whatever type (const or non-const reference) the innermost "real" call requires.