wyer33
wyer33

Reputation: 6340

Perfect forwarding for functions inside of a templated C++ class

Is there a good way to get perfect forwarding for functions inside of a templated class? Specifically, in the code

#include <iostream>

// Forward declare a Bar
struct Bar;

// Two different functions that vary based on the kind of argument
void printme(Bar const & bar) {
    std::cout << "printme: constant reference bar" << std::endl;
}
void printme(Bar && bar) {
    std::cout << "printme: r-value reference bar" << std::endl;
}

void printme2(Bar const & bar) {
    std::cout << "printme2: constant reference bar" << std::endl;
}
void printme2(Bar && bar) {
    std::cout << "printme2: r-value reference bar" << std::endl;
}

// Some class with a bunch of functions and possible some data (though, not
// in this one)
template <typename T>
struct Foo {
    void baz(T && t) {
        printme(std::forward <T> (t)); 
    }
    void buz(T && t) {
        printme2(std::forward <T> (t)); 
    }
};

struct Bar {};

int main() {
    Foo <Bar> foo;        
    foo.baz(Bar());

    // Causes an error
    Bar bar;
    //foo.buz(bar);
}

Uncommenting the last bit of code, we get the error:

    test03.cpp: In function 'int main()':
    test03.cpp:51:16: error: cannot bind 'Bar' lvalue to 'Bar&&'
         foo.buz(bar);
                    ^
    test03.cpp:30:10: note: initializing argument 1 of 'void Foo<T>::buz(T&&) [with T = Bar]'
         void buz(T && t) {
              ^
    Makefile:2: recipe for target 'all' failed
    make: *** [all] Error 1

Now, we can fix this problem by moving the template argument inside of the class:

#include <iostream>

// Forward declare a Bar
struct Bar;

// Two different functions that vary based on the kind of argument
void printme(Bar const & bar) {
    std::cout << "printme: constant reference bar" << std::endl;
}
void printme(Bar && bar) {
    std::cout << "printme: r-value reference bar" << std::endl;
}

void printme2(Bar const & bar) {
    std::cout << "printme2: constant reference bar" << std::endl;
}
void printme2(Bar && bar) {
    std::cout << "printme2: r-value reference bar" << std::endl;
}

// Some class with a bunch of functions and possible some data (though, not
// in this one)
template <typename T>
struct Foo {
    void baz(T && t) {
        printme(std::forward <T> (t)); 
    }
    template <typename T_>
    void buz(T_ && t) {
        printme2(std::forward <T_> (t)); 
    }
};

struct Bar {
    Bar() {} 
};

int main() {
    Foo <Bar> foo;        
    foo.baz(Bar());

    Bar bar;
    foo.buz(bar);
}

However, this seems like it'll be really verbose. For example, imagine that we have a large number of functions that all depended on the type T and needed perfect forwarding. We'll need separate template declarations for each. Also, the class Foo may contain a data of type T and we want functions that are consistent with this data. Certainly, the typechecker will catch mismatches, but this system isn't as straightforward as just having a single template argument, T.

Basically, what I'm wondering is if there's a better way to do this or are we stuck just templating each function in the class separately?

Upvotes: 5

Views: 1683

Answers (1)

Mooing Duck
Mooing Duck

Reputation: 66912

The templates are very different.

In the first code, your template parameter is Bar, and so I'm not sure even what it's doing, because you should NOT be using std::forward except with reference-qualified-types. I think it's void buz(Bar&& t) {printme((Bar)t);}, and the compiler is balking at passing the lvalue bar in main to a function expecting Bar&&.

In the second codeblock, the template parameter is Bar& due to the universal reference, so the code is void buz(Bar& t) {printme((Bar&)t);}, which binds to to the lvalue bar in main just fine.

If you want perfect forwarding, the template parameter must be the rvalue-qualified type you want passed on, which means you almost always have to have the function itself be templated. However, do yourself a favor and name it different. The T and the T_ will be different types.

template <typename U>
void buz(U&& t) {
    printme2(std::forward<U>(t)); 
}

If you want SFINAE, you can add that too:

template <typename U,
    typename allowed=typename std::enable_if<std::is_constructible<Bar,U>::value,void*>::type
    >
void buz(U && t) {
    printme2(std::forward <U> (t)); 
}

Upvotes: 6

Related Questions