c++11gccclangvariadic-templatesmsvc12

Different results on a variadic template example among gcc, clang and msvc - can anyone explain?


I needed to make a function that takes a function pointer with variable arguments and some fixed arguments after them and could not make it work on Visual Studio 2013. I assumed that maybe Visual Studio 2013 was missing something which is often a case and made a minimal example that did what I needed and tried it against gcc and clang. And I got totally different results on all three compilers. So the questions which I'd like to resolve are:

  1. Is my example valid at all? If not what am I doing wrong?
  2. If my example is valid, any hints on the behavior of gcc and clang (lets count msvc out since it is a black box)?

The example:

#include <iostream>

struct foo
{
    void work(int first, int second, int third)
    {
        std::cout << "0: " << first << ",1: " << second << ",2: " << third << std::endl;
    }
    void work_with_double(double first, int second, int third, int fourth)
    {
        std::cout << "0: " << first << ",1: " << second << ",2: " << third << ",3: " << fourth << std::endl;
    }
};

template<typename ... argument_types>
void invoke_foo(foo* instance, int first, int second, int third, void (foo::*method)(argument_types ... arguments, int, int, int), argument_types ... arguments)
{
    (instance->*method)(arguments ..., first, second, third);
}

int main(int argc, char** argv)
{
    foo instance;
    invoke_foo(&instance, 1, 2, 3, &foo::work); // gcc ok, clang err, msvc 2013 err
    invoke_foo<>(&instance, 1, 2, 3, &foo::work); // gcc ok, clang err, msvc 2013 err
    invoke_foo(&instance, 1, 2, 3, &foo::work_with_double, 1.0); // gcc err, clang ok, msvc 2013 err
    invoke_foo<double>(&instance, 1, 2, 3, &foo::work_with_double, 1.0); // gcc err, clang err, msvc 2013 ok
    return 0;
}

Modified snippet that makes Visual Studio 2015 (w/o updates) crash

If invoke_foo is made as a member function of an object, Visual Studio 2015 crashes.

#include <iostream>
#include <memory>

struct foo
{
    void work(int first, int second, int third, int fourth, int fifth, int sixth, int seventh, int eight)
    {
        std::cout << "0: " << first << ",1: " << second << ",2: " << third << std::endl;
    }
    void work_with_double(double firstExtra, int first, int second, int third, int fourth, int fifth, int sixth, int seventh, int eight)
    {
        std::cout << "0: " << first << ",1: " << second << ",2: " << third << ",3: " << fourth << std::endl;
    }
};

struct bar
{

};

struct wrapper
{

    template <typename T> struct non_deduced { using type = T; };
    template <typename T> using non_deduced_t = typename non_deduced<T>::type;

    template<typename ... argument_types>
    std::shared_ptr<bar> invoke_foo(int first, int second, int third, int fourth, int fifth, int sixth, int seventh, int eight, void (foo::*method)(non_deduced_t<argument_types>... arguments, int, int, int, int, int, int, int, int), argument_types ... arguments)
    {
        (foo_.get()->*method)(arguments ..., first, second, third, fourth, fifth, sixth, seventh, eight);
        return nullptr;
    }

    std::unique_ptr<foo> foo_ = std::move(std::unique_ptr<foo>(new foo));

};

int main(int argc, char** argv)
{
    wrapper instance;
    instance.invoke_foo(1, 2, 3, 4, 5, 6, 7, 8, &foo::work);
    instance.invoke_foo(1, 2, 3, 4, 5, 6, 7, 8, &foo::work_with_double, 1.0);
}

Solution

  • The problem in each case is that the compiler is trying to infer argument_types from the method argument, which is illegal as variadic template parameters can only be inferred when they are at the end of an argument list.

    void (foo::*method)(argument_types ... arguments, int, int, int)
                        ^^^^^^^^^^^^^^^^^^ can't infer here
                                                    ^^^^^^^^^^^^^^^ because of these
    

    The workaround is to protect argument_types from being deduced in this context, using a helper like identity:

    template<class T> struct identity { using type = T; };
    template<class T> using identity_t = typename identity<T>::type;
    
    // ...
    
    template<typename ... argument_types>
    void invoke_foo(foo* instance, int first, int second, int third,
        void (foo::*method)(identity_t<argument_types> ... arguments, int, int, int), argument_types ... arguments)
    //                      ^^^^^^^^^^^ fix here
    

    Is this a bug in your code, or in the compilers? Actually, it's a bug in the compilers (yes, all of them); the question is whether a parameter pack appearing within a function type and not at the end of an argument list is a non-deduced context. The relevant part of the standard is [temp.deduct.type], which states:

    5 - The non-deduced contexts are: [...]

    • A function parameter pack that does not occur at the end of the parameter-declaration-list.

    6 - When a type name is specified in a way that includes a non-deduced context, all of the types that comprise that type name are also non-deduced. However, a compound type can include both deduced and non-deduced types.

    Here, argument_types is in a non-deduced context when deducing the type of method, but a deduced context when deducing the types for the trailing arguments of invoke_foo.

    Another compiler you could test is ICC (Intel C++ Compiler); ICC rejects the first two forms and accepts the last two, the exact opposite to gcc. The reason that compilers can be so different in their behavior is that dealing with this kind of code is essentially a matter of error handling, specifically recognising when template parameters appear in non-deduced contexts and using the types deduced elsewhere instead. The compilers are (each in their own way) recognising that argument_types cannot be deduced within method, but failing to realise or accept that it can be deduced elsewhere.

    Specifically, it appears that: