Lambda作为模板类参数的编译问题及std::forward用法疑问
问题代码与疑问
先看测试代码:
#include <iostream> template <typename Func> class Test { public: Test(Func &&func) : m_func(std::forward<Func &&>(func)) {} void Run() { m_func(); } private: Func &&m_func; }; template <typename Func> class Foo { public: Foo(Func &func) : m_func(func) {} void Run() { m_func(); } private: Func &m_func; }; int main() { const auto print = []() { std::cout << "Hello" << std::endl; }; using Print = decltype(print); Test<decltype(print)> t0(std::forward<Print&&>(print)); t0.Run(); Test<void()> t1(Print{}); t1.Run(); Foo<decltype(print)> t3(std::forward<Print&&>(print)); t3.Run(); Foo<void()> t4(Print{}); t4.Run(); }
第一个疑问
为什么t0加了std::forward就能编译通过,而t1直接报错?
更新后的疑问
把t1改成下面的代码后就能编译了:
Test<void(*)()> t1([]() { std::cout << "Hello" << std::endl; });
但对t0还有疑问:如果删掉t0里的std::forward就编译失败,换成Test<void (*)()> t0(print);却能正常运行,这两种写法到底有什么区别?
逐一解答
1. t0编译通过、t1报错的原因
关于
t0:decltype(print)拿到的是这个Lambda专属的闭包类型(每个无捕获Lambda都有唯一的匿名类型),因为print是const auto定义的,所以这个类型带const修饰。
显式指定Test的模板参数为这个闭包类型后,构造函数的参数就变成了const 闭包类型&&(右值引用)。std::forward<Print&&>(print)的作用是把左值print强制转换成const 闭包类型&&,刚好匹配构造函数的参数,因此编译通过。成员变量m_func是这个右值引用,只要t0还处于生命周期内,调用Run()就能正常执行Lambda。关于
t1:
指定Test的模板参数为void()时,这是函数类型而非函数指针。构造函数参数随之变成void()&&(函数类型的右值引用)。但Print{}是Lambda的闭包对象,它只能隐式转换成函数指针void(*)(),没法直接转成函数类型void()的引用——函数类型的引用本质是绑定到实际函数上,而Lambda闭包对象不是函数本身,只是能转成指向函数的指针。类型不匹配,自然编译报错。
改成Test<void(*)()> t1(...)后,模板参数是函数指针类型,构造函数参数变成void(*)()&&,Lambda闭包对象可以隐式转成这个指针类型的右值,刚好匹配参数,所以编译通过。
2. t0两种写法的差异
写法一:
Test<decltype(print)> t0(std::forward<Print&&>(print));
这里直接用Lambda的闭包类型作为模板参数,构造函数需要该类型的右值引用。但print是左值(const闭包类型的左值),左值不能直接绑定到右值引用,必须用std::forward<Print&&>把它转换成右值引用类型,才能匹配构造函数参数,编译才能通过。写法二:
Test<void (*)()> t0(print);
这里用函数指针类型void(*)()作为模板参数,构造函数需要该指针类型的右值引用。因为这个Lambda没有捕获变量,符合隐式转换为函数指针的条件,左值print会自动转换成void(*)()类型的右值,刚好能绑定到构造函数的右值引用参数上,所以不需要std::forward也能编译通过。核心区别总结:
- 写法一操作的是Lambda的原生闭包类型,必须手动转换为右值引用才能匹配构造函数;
- 写法二操作的是函数指针类型,Lambda可以自动转换为该指针,不需要额外转换。
内容的提问来源于stack exchange,提问作者Hovin

