Search code examples
c++variadic-templatestemplate-meta-programming

Problems when specializing variadic template member function


I have the following state machine example that uses enums to specialize template methods with a variadic parameter pack. Why doesn't the second specialization get called when I pass Properties by reference (Properties&) rather than Properties by value (Properties)?

enum class State
{
    StateA,
    StateB
};

enum class Event
{
    Event1,
    Event2
};

struct Properties
{};

template <typename TState, typename TEvent>
struct StateMachine
{
    template <TState tstate, TEvent tevent, typename ...TArgs>
    void enter(TArgs ...args)
    {
        std::cout << "Default" << std::endl;
    }
};

template <>
template <>
void
StateMachine<State, Event>::enter<State::StateA, Event::Event1>(int i)
{
    std::cout << "Specialized 1" << std::endl;
}

template <>
template <>
void
StateMachine<State, Event>::enter<State::StateA, Event::Event2>(Properties& properties)
{
    std::cout << "Specialized 2" << std::endl;
}

int main(int argc, char* argv[])
{
    StateMachine<State, Event> sm;

    int value = 123;
    sm.enter<State::StateA, Event::Event1>(value);

    Properties properties;
    sm.enter<State::StateA, Event::Event2>(properties);
}

I was expecting output:

Specialized 1
Specialized 2

but got:

Specialized 1
Default

When I pass Properties by value, it works correctly. Is this a perfect forwarding problem? How do I fix?


Solution

  • The problem is that only the generic version(aka primary template) takes part in overloading and in your primary template of enter you have the argument(s) taken by value while in your specialized template for properties the argument is taken by lvalue reference. That is, in the primary template the third argument is deduced as Properties while for the specialization it is Properties&.

    How do I fix?

    This means that there are two way to fix this. You can either args an lvalue reference or explicitly pass Properties& as the third argument as shown below.

    Method 1

    Here we make args an lvalue reference to non-const by changing TArgs ...args to TArgs& ...args. Note that you can also make it as lvalue reference to const since enter doesn't change any internals.

    template <typename TState, typename TEvent>
    struct StateMachine
    {
        template <TState tstate, TEvent tevent, typename ...TArgs>
    //------------------v-------------------->added lvalue ref
        void enter(TArgs& ...args) 
        {
            
            std::cout << "Default" << std::endl;
        }
    };
    template <>
    template <>
    void
    //-----------------------------------------------------------------v-->lvalue ref
    StateMachine<State, Event>::enter<State::StateA, Event::Event1>(int& i)
    {
        std::cout << "Specialized 1" << std::endl;
    }
    int main(int argc, char* argv[])
    {
        StateMachine<State, Event> sm;
        Properties properties;
        sm.enter<State::StateA, Event::Event2>(properties); //prints specialized 2
    }
    

    Demo


    Method 2

    The second way is to explicitly pass the third argument Properties&.

    int main(int argc, char* argv[])
    {
        StateMachine<State, Event> sm;
    
        Properties properties;
    //-----------------------------------------vvvvvvvvvvv--->explicitly pass third argument
        sm.enter<State::StateA, Event::Event2, Properties&>(properties); //prints specialized 2
    }
    

    Demo