gcc、clang 和 msvc 中可变参数模板示例的不同结果 - 谁能解释一下?

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

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

  1. 我的例子是否有效?如果不是我做错了什么?
  2. 如果我的示例有效,是否有任何关于 gcc 和 clang 行为的提示(让我们将 msvc 排除在外,因为它是一个黑盒子)?

例子:

#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 年(w/o 更新)崩溃的修改代码段

如果invoke_foo作为一个对象的成员函数,Visual Studio2015崩溃。

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

每种情况下的问题是编译器试图从 method 参数推断出 argument_types,这是非法的,因为可变参数模板参数只能在它们位于参数列表。

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

解决方法是使用像 identity:

这样的助手来保护 argument_types 不被推导出来
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 - 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.

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

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

具体来说,似乎是:

  • gcc假设如果它不能从method推导出argument_types,它一定是空的;
  • clang假设如果argument_types被推导为空或明确指定,这一定是一个错误;
  • MSVC 无法让 argument_types 的推导覆盖推导失败,但如果明确指定则可以;
  • ICC 假设如果 argument_types 被推断为空,这一定是一个错误。