我遇到了一个问题。当我使用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仅在最后一个函数调用时失败。谁能解释这里发生了什么?
为方便起见,让我们在代码#1,#2和#3中调用三个失败的电话。
问题是,当显式指定与模板参数包相对应的模板参数时,模板参数包是否仍参与模板参数推导,如果这样做,推导是否失败会使整个调用格式错误?
模板参数推导可以扩展模板的顺序与模板参数包相对应的参数,即使序列包含显式指定的模板参数。
我们可以推断出仍应执行模板参数推导。
在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确实是不正确的。
这看起来像是编译器错误;当所有参数都已明确指定后,编译器将尝试模板参数推导,因此无需推导。也许该错误是因为替换,它应该会成功。
根据标准,可以显式指定可变参数包参数。参见[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;
}