4

我需要制作一个函数,该函数采用带有可变参数和一些固定参数的函数指针,并且无法使其在 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(无更新)崩溃的修改片段

如果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);
}
4

1 回答 1

4

每种情况下的问题是编译器试图argument_typesmethod参数推断,这是非法的,因为可变参数模板参数只能在它们位于参数列表的末尾时进行推断。

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覆盖失败推演,但如果明确指定就OK;
  • ICC 假设如果argument_types推断为空,则这一定是一个错误。
于 2016-06-22T14:22:57.570 回答