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 );
5
  • 2
    You should post your compiler errors. Commented Feb 17, 2016 at 9:18
  • 1
    std::forward< Callback >(c) is equal to std::move(c) Commented Feb 17, 2016 at 10:10
  • Presumably I should also be using emplace_back for further efficiency? Commented Feb 18, 2016 at 11:56
  • @PiotrSkotnicki Updated my question (at the bottom) to include a reworked version of addCallback where, if my understanding is correct, it would now be correct to use std::forward whereas in the original version std::move would have been appropriate. Commented Feb 18, 2016 at 13:15
  • @Slartibartfast if Callback&& is a forwarding reference, then use std::forward, if it's an rvalue reference, then std::move is sufficient Commented Feb 18, 2016 at 13:18

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.