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

问题描述 投票:5回答:2

我遇到了一个问题。当我使用std::function<A(Fs...)>之类的东西时,它不起作用,但std::function<A(Fs..., B)>的确起作用。这是在Clang 8.0下;在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仅在最后一个函数调用时失败。谁能解释这里发生了什么?

c++ templates c++17 variadic-templates std-function
2个回答
2
投票

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

问题是,当显式指定与模板参数包相对应的模板参数时,模板参数包是否仍参与模板参数推导,如果这样做,推导是否失败会使整个调用格式错误?

来自[temp.arg.explicit]/9

模板参数推导可以扩展模板的顺序与模板参数包相对应的参数,即使序列包含显式指定的模板参数。

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

func_tmpl1的声明中,std::function<A(Fs..., B)>是一个非推论上下文([temp.deduct.type]/9:“如果P的模板参数列表包含的包扩展不是最后一个模板参数,则整个模板参数列表为非推论上下文。”),因此应忽略Fs的模板参数推论,并且#1和#2的格式都正确。有一个GCC bug report

对于#3,模板参数推导显然会失败(std::function<A(Fs...)>与lambda类型),但是推导失败真的会使代码格式错误吗?在我看来,标准对此尚不明确,并且存在related issue。从CWG的响应来看,#3确实是不正确的。


1
投票

这看起来像是编译器错误;当所有参数都已明确指定后,编译器将尝试模板参数推导,因此无需推导。也许该错误是因为替换,它应该会成功。

根据标准,可以显式指定可变参数包参数。参见[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

© www.soinside.com 2019 - 2024. All rights reserved.