1

Sincere apologies if this has been answered elsewhere, I did search but couldn't find a clear match.

I have a variadic function in a template class that does not work exactly as I expected. I have a workaround, but I suspect it's not the best solution.

Consider the following code:

#include <iostream>
#include <functional>
#include <vector>

template< typename... ARGS >
class Kitten
{
public:
    using Callback = std::function< void( ARGS&&... ) >;

    Kitten() = default;

    void addCallback( Callback && c )
    {
        callbacks.push_back( std::forward< Callback >( c ) );
    }

    void processCallbacks( ARGS &&... args )
    {
        for ( Callback const &c : callbacks )
        {
            c( std::forward< ARGS >( args )... );
        }
    }

private:
    std::vector< Callback > callbacks;
};

int main( int argc, const char * argv[] )
{
    ( void ) argc;
    ( void ) argv;

    Kitten<int, float> kitty;
    kitty.addCallback( []( int i, float f )
    {
        std::cout << "Int: " << i << "\nFloat: " << f << "\n";
    } );
    kitty.processCallbacks( 2, 3.141f );

    int ii = 54;
    float ff = 2.13f;
    kitty.processCallbacks( ii, ff );

    return 0;
}

This will not compile, the second call to processCallbacks will generate an error (clang, similar issue seen on vc14).

I can fix the compilation and get things working as expected if I change the definition of processCallbacks to:

template< typename... FORCEIT >
void processCallbacks( FORCEIT &&... args )
{
    for ( Callback const &c : callbacks )
    {
        c( std::forward< ARGS >( args )... );
    }
}

It seems to me to be a bit of a cheesy workaround even if it seems to work, and I suspect I'm missing a better solution.

My understanding of why the first sample fails is because there's no type deduction being done on the argument pack, so the compiler isn't generating the correct code for all cases. The second sample works because it forces the type deduction on the argument pack.

It's been puzzling me for a while on and off. Any help much appreciated.

edit: vc12 compiler error:

error C2664: 'void Kitten<int,float>::processCallbacks(int &&,float &&)' : cannot convert argument 1 from 'int' to 'int &&'

edit: Apple LLVM version 7.0.0 compiler error:

error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'

Regarding the change suggested in the comments to use std::move, addCallback would seem to be even more flexible in the form:

template< typename FUNCTION >
void addCallback( FUNCTION && c )
{
    callbacks.emplace_back( std::forward< FUNCTION >( c ) );
}

Using std::forward because the function now takes a universal reference. As this would allow the following to work:

std::function< void( int, float )> banana( []( int i, float f )
{
    std::cout << "Int: " << i << "\nFloat: " << f << "\n";
} );
kitty.addCallback( banana );

1 Answers1

3
void processCallbacks( ARGS &&... args )
{
    //...
}

For each type in ARGS the allowed value categories will be set by the template arguments to Kitten. E.g. for Kitten<float, int&, const bool&>, processCallbacks will accept rvalues for the first parameter, lvalues for the second (due to reference collapsing rules) and both for the third (because rvalues can bind to const lvalue references). This will not give you perfect forwarding.

template< typename... FORCEIT >
void processCallbacks( FORCEIT &&... args )
{
    //...
}

That function template accepts both lvalues and rvalues because there is type deduction. Such a parameter is known as a forwarding reference parameter. Forwarding reference parameters must be of the form T&&, where T is some deduced template parameter.

If you want to accept lvalues and rvalues and perfect-forward them, you should use the latter form. Although it might seem weird to you at first, this is a fairly common pattern.

Community
  • 1
  • 1
TartanLlama
  • 63,752
  • 13
  • 157
  • 193