关于gcc,clang和msvc中可变参数模板示例的不同结果 - 任何人都可以解释一下吗?

时间:2016-06-22 13:32:06

标签: c++11 gcc clang variadic-templates msvc12

我需要创建一个函数,它带有一个带有变量参数的函数指针和一些固定的参数,并且无法使它在Visual Studio 2013上运行。我假设Visual Studio 2013可能缺少一些常见的情况。做了一个最小的例子,做了我需要的东西,并尝试对抗gcc和clang。我在所有三个编译器上得到了完全不同的结果。所以我想解决的问题是:

  1. 我的例子有效吗?如果不是我做错了什么?
  2. 如果我的示例有效,有关gcc和clang行为的任何提示(由于它是黑盒子,我们可以将msvc计算出来)吗?
  3. 示例:

    #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;
    }
    

    使Visual Studio 2015(没有更新)崩溃的修改后的片段

    如果将invoke_foo作为对象的成员函数,Visual Studio 2015将崩溃。

    #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);
    }
    

1 个答案:

答案 0 :(得分:4)

每种情况下的问题是编译器试图从argument_types参数推断出method,这是非法的,因为只有当参数结束时才能推断出可变参数模板参数列表。

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

解决方法是使用argument_types之类的帮助器保护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

这是您的代码或编译器中的错误吗?实际上,它是编译器中的一个错误(是的,所有这些);问题是出现在函数类型中而不是在参数列表末尾的参数包是否是非推导的上下文。标准的相关部分是[temp.deduct.type],其中指出:

  

5 - 未推断的上下文是:[...]

     
      
  • 不在参数声明列表末尾出现的函数参数包。
  •   
     

6 - 当以包含非推导上下文的方式指定类型名称时,包含该类型名称的所有类型也是非推断的。但是,复合类型可以包括推导类型和非推导类型。

此处,argument_types在推导method的类型时处于非推导的上下文中,而在推导invoke_foo的尾随参数的类型时是推导的上下文。

您可以测试的另一个编译器是ICC(英特尔C ++编译器); ICC拒绝前两个表格并接受最后两个表格,与gcc完全相反。编译器在行为上如此不同的原因在于处理这种代码本质上是一个错误处理问题,特别是识别模板参数何时出现在非推导的上下文中并使用其他地方推导出的类型。编译器(每个都以他们自己的方式)认识到argument_types不能在method内推断,但没有意识到或接受它可以在其他地方推断出来。

具体来说,似乎:

  • gcc假设如果无法从argument_types推断method,则必须为空;
  • clang假设如果argument_types被推断为空或明确指定,则必须是错误;
  • MSVC无法让argument_types的推论覆盖推断它的失败,但如果明确指定则没有问题;
  • ICC假设如果argument_types被推断为空,则必须是错误。