【问题标题】:Why does template parameter unpacking sometimes not work for std::function?为什么模板参数解包有时不适用于 std::function?
【发布时间】:2020-04-17 22:11:37
【问题描述】:

我遇到了一个问题。当我使用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 只在最后一次函数调用时失败。谁能解释这里发生了什么?

【问题讨论】:

  • 没有 Cls1 是 B,float 是 Fs。所以它是正确的,它在clang中编译得很好。
  • 对不起,我看错了。
  • 这在 MSVC 上编译和运行。

标签: c++ templates c++17 variadic-templates std-function


【解决方案1】:

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

问题是,当一个模板形参包对应的模板实参被显式指定时,模板形参包是否仍然参与模板实参推导,如果参与,推导失败是否会导致整个调用格式不正确?

来自[temp.arg.explicit]/9

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

我们可以推断,模板参数推导还是应该进行的。

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

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

【讨论】:

  • 非常感谢。我没有注意到 cwg.1982。如果不支持,他们可能应该将其定义为格式错误。到目前为止,只有 MSVC 认为它是格式良好的。不幸的是,2015 年提交的 GCC 错误 66670 仍然存在。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-15
    • 2014-01-03
    • 1970-01-01
    • 1970-01-01
    • 2012-11-19
    • 1970-01-01
    相关资源
    最近更新 更多