为什么模板参数解包有时对 std::function 不起作用?

Why does template parameter unpacking sometimes not work for std::function?

我遇到了问题。当我使用 std::function<A(Fs...)> 之类的东西时,它不起作用,但 std::function<A(Fs..., B)> 确实起作用。这是在 Clang 8.0 下; none 它在 GCC 下工作。 这是示例:

#include <functional>
template<typename A, typename B, typename ...Fs>
void func_tmpl1(std::function<A(Fs..., B)> callable)
{
}
template<typename A, typename ...Fs>
void func_tmpl2(std::function<A(Fs...)> callable)
{
}
class Cls1{};
void func0(std::function<void(float, Cls1)> callable)
{

}

int main()
{
    std::function<void(float, Cls1)> f1 = [](float a, Cls1 b){};
    func0(f1);
    func0([](float a, Cls1 b){});
    func_tmpl1<void, Cls1, float>(f1); // fails in GCC
    func_tmpl2<void, float, Cls1>(f1);

    func_tmpl1<void, Cls1, float>( // fails in GCC
        [](float a, Cls1 b)
        {

        }
    );
    func_tmpl2<void, float, Cls1>( // fails in both
        [](float a, Cls1 b)
        {}
    );

    return 0;
}

Godbolt,我们可以看到GCC总是失败,但Clang只在最后一次函数调用时失败。谁能解释一下这里发生了什么?

为方便起见,我们调用代码中的三个失败调用 #1、#2 和 #3。

问题是,当模板参数包对应的模板参数被显式指定时,模板参数包是否仍然参与模板参数推导,如果参与,推导失败是否导致整个调用格式错误?

来自[temp.arg.explicit]/9

Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments.

我们可以推断模板参数推导仍然应该执行。

func_tmpl1的声明中,std::function<A(Fs..., B)>是一个非推导上下文([temp.deduct.type]/9: "If the template argument list of P contains a pack expansion that is not the last template argument, the entire template argument list is a non-deduced context."), so template argument deduction for Fs should be ignored and #1 and #2 are both well-formed. There is a GCC bug report.

对于#3,模板参数推导显然失败了(std::function<A(Fs...)> 与 lambda 类型相比),但推导失败真的会使代码格式错误吗?在我看来,标准对此并不清楚,还有一个related issue。从 CWG 的回复来看,#3 确实格式错误。

这看起来像是一个编译器错误;当所有参数都已明确指定且因此不需要推导时,编译器会尝试模板参数推导。 或者错误可能与替换有关,应该会成功。

根据标准,可以显式指定可变包参数。请参阅 [temp.arg.explicit]/5 中的示例:

template<class ... Args> void f2();
void g() {
  f2<char, short, int, long>(); // OK
}

当所有模板参数都已知时,编译器应该简单地实例化模板并完成它;重载决议然后正常进行。

要解决此问题,我们可以通过引入非推导上下文来禁用模板参数推导。例如像这样:

template<typename T> using no_deduce = typename std::common_type<T>::type;

template<typename A, typename B, typename ...Fs>
void func_tmpl1(no_deduce<std::function<A(Fs..., B)>> callable)
{
}

template<typename A, typename ...Fs>
void func_tmpl2(no_deduce<std::function<A(Fs...)>> callable)
{
}

(这里的::type是依赖类型,成为非推导上下文)

现在它可以在 g++clang++ 中正常编译。 link to coliru


话虽如此,请注意 std::function 主要用于 类型擦除 并且是 昂贵的抽象 因为它会产生在 运行 时间的额外间接寻址是一个沉重的传递对象,因为它试图存储任何可能的仿函数的副本,同时避免堆分配(这仍然经常发生 - 然后它是一个大的 empty 对象加上堆分配。

因为你的函数已经是模板,你真的不需要类型擦除;将 callable 作为模板参数更容易和更有效。

template<typename Func>
void func_tmpl(Func callable) // that's all
{
}

或者,如果您必须通过 callable 个参数来区分,可以使用一些 SFINAE:

#include <functional>
class Cls1{};

template<typename A, typename B, typename ...Fs, typename Func,
    typename = std::enable_if_t<std::is_invocable_r_v<A, Func, Fs..., B> > >
void func_tmpl1(Func callable)
{
}
template<typename A, typename B, typename ...Fs, typename Func,
    typename = std::enable_if_t<std::is_invocable_r_v<A, Func, B, Fs...> > >
void func_tmpl2(Func callable)
{
}
void func0(std::function<void(float, Cls1)> callable)
{
}

int main()
{
    std::function<void(float, Cls1)> f1 = [](float a, Cls1 b){};
    func0(f1); // func0 is not a template - so it requires type erasure
    func0([](float a, Cls1 b){});
    func_tmpl1<void, Cls1, float>(f1); // #1 OK
    func_tmpl2<void, float, Cls1>(f1); // #2 OK

    func_tmpl1<void, Cls1, float>([](float a, Cls1 b) {}); // #3 OK
    func_tmpl2<void, float, Cls1>([](float a, Cls1 b) {}); // #4 OK

    return 0;
}

link to coliru