带捕获的 lambda 与函数指针

C++ 的函数类型包括了以下几种:

  • 函数指针;
  • 成员函数指针;
  • 上述两种函数类型的引用、c-v-noexcept 修饰符的排列组合。

在 C++11 后,语言标准引入了更灵活的 lambda 函数;因此在函数类型中又新增了 lambda 类型和一堆修饰符的排列组合。

实际上,C++ 中 lambda 函数是一个匿名类的类对象;这个匿名类由编译器生成,并且重载了 operator() 运算符。所以任何一个重载了 operator() 的类对象都可以被视作是广义上的 lambda。

如果 lambda 函数带有捕获(包括引用和值捕获),那么这个生成的匿名类就会持有对应值类型的数据成员;否则匿名类是一个大小为 1 的空类(标准要求不含数据成员的类大小必须为 1)。

并且,不带有捕获的 lambda 函数可以被隐式类型转换为一个函数指针;这一行为可以通过在 lambda 表达式的捕获列表(方括号)左侧添加一个一元运算符 + 显式触发。

最重要的是,任意两个 lambda 表达式的实际类型永远不可能相同(见 cppref),即使它们的函数签名和捕获列表完全一致。

这些类型的排列组合背后是沉重的历史包袱,以至于函数类型被称作是 Abominable Function Types(糟糕的函数类型)。

这里的历史包袱说的就是 C 语言中的函数类型(呕。

在与一些需要传递自定义函数的场景下,我们往往会传递一个 lambda 或指向某个函数的指针;这种用法在与一些提供了 Modern Cpp 封装的接口交互时会显得很优雅和简洁。

这些接口往往都被编写为模板函数,使得它能够接收任意可调用类型;而在无法模板化的接口中,则使用 std::function 作为可调用类型的存储容器。

但对于一些没有提供 Modern Cpp 支持,或者干脆就是由 C 语言编写的接口(例如 Linux 的系统调用)来说,我们在传递这类可调用对象时能选择的类型就只剩下函数指针了。

函数指针很万能,但有一点致命缺陷:它只能指向一个已存在的全局函数,并且带有捕获列表的 lambda 函数无法经类型转换变成函数指针。所以如果我想传递一个指针,那么我必须在全局作用域创建一个新函数。

在(C++ 语境的)大多数情况下,我们传递的回调函数往往都是一个临时创建的 lambda 对象;假若这个 lambda 需要经由捕获列表绑定到某个对象的状态上,我们就无法获得一个指向该 lambda 的函数指针。

假设此时我们需要向一个只接受函数指针的接口传递这样的回调函数,那么我们就不得不在全局范围内定义一个函数,并想办法让这个函数能够不经由函数参数列表访问到它所绑定的局部对象。

如果经由函数参数列表访问,那么这个函数所产生的函数指针类型将无法被接口接受;因为很显然,这个函数的参数列表会多出一个额外类型。

到了这里已经可以预见一个软件工程灾难了:为了一个简单的函数指针,就需要创建一个全局函数,还得想办法将局部对象的访问权暴露给全局。

所以这里我们需要一个简洁的解决方案。

1.将局部对象的作用域提升到全局

lambda 函数与常规函数一样,能够在函数体内未经捕获地访问全局对象,前提是在定义 lambda 时就已经看到了这些对象。

这里的全局对象包括:

  • 任何被 static 修饰的变量;
  • 在全局作用域内声明/定义的变量(包括 extern 引入)。

而一个不带捕获的 lambda 是可以被隐式转换为函数指针的。因此我们可以将捕获列表内的局部对象提升为 static,然后使用 lambda 包装一下。

int main()
{
  static std::vector<int> arr;
  auto fptr = +[]( int a ) { arr.push_back( a ); };

  foo( fptr ); // 传递函数指针
}

这很蠢,而且局部对象被提升为 static 毫无意义,甚至会破坏原有的 RAII 语义;更不用说部分情况下全局化一个局部对象的行为可能比获得一个函数指针更困难。

2.利用全局函数

我们可以反过来做:把一个带捕获的 lambda 表达式存放到一个函数内部作用域的 static 变量中,然后透过一个能够访问这个 static 变量的函数访问。

注意:因为在标准定义中,lambda 函数的赋值运算符全部都被标记为弃置,所以我们只能通过声明语句,从一个已存在的 lambda 对象上再构造一个 lambda 对象。

这时我们需要引入一点模板元编程技巧。

template<typename Lambda>
typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<Lambda>::value>::type
  make_fnptr( Lambda&& fn )
{
  static typename std::remove_reference<Lambda>::type fntor = std::forward<Lambda>( fn );
}

我们可以在内部返回一个不带捕获、且转为了函数指针的 lambda 函数;但我们很快就会发现:我们不知道返回的函数指针类型是什么,也就是说这个函数的返回值是未决的;这会直接触发编译失败。

我们无法推断返回类型的原因有很多:

  • 我们不知道这个 Lambda 的返回值和参数列表是什么,所以写不出能够描述指向二次包装后 lambda 的指针类型;
  • 函数参数列表需要在调用该函数时传入,但是可变模板参数列表会与函数参数中的模板参数产生语义冲突;
  • 即使我们获取了被包装的类型参数列表,我们也无法在函数体内就地展开并填充到二次包装的 lambda 中(这里需要使用模板类的模式匹配)。

第一条原因还能借由引入复杂的类型萃取器解决,第二条也可以引入额外的类型包装解决,但第三条原因直接宣判了这个方案的死刑。

3.引入一个包装器类

因为类的静态函数天然就可以被转换为函数指针,同时这个静态函数的可见范围是全局,所以我们不妨引入一个模板包装类,将返回的函数指针指向这个类的静态函数。

为了支持带有参数列表的 lambda,这个静态函数还需要被模板化。

模板函数在被实例化后(填入函数参数列表)就可以获取地址了,这里没有问题。

此外 C++ 还有一个比较有意思的语法点:返回类型都是 void 的函数,如下形式的调用是没问题的。

#include <iostream>

void foo() { std::cout << "HelloWorld"; }
void func()
{
  return foo();
}

int main()
{
  func();
}

综上所述,略去一些设计过程不表,我们就可以得到这样一个实现。

template<typename Fn>
struct LambdaWrapper {
private:
  static_assert( std::is_class<Fn>::value, "Only available to lambda" );
  static_assert( !std::is_empty<Fn>::value, "Only available to lambda with capture" );

  static const Fn* fntor;

  template<typename... Args>
  static typename std::result_of<Fn( Args... )>::type invoking( Args... args )
  //static std::invoke_result_t<Fn, Args...> invoking( Args... args )
  {
    return ( *fntor )( std::forward<Args>( args )... );
  }

public:
  template<typename... Args>
  decltype( &invoking<Args...> ) to_fnptr() noexcept
  {
    return &invoking<Args...>;
  }

  template<typename _FnTp, typename FnTp>
  friend typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<FnTp>::value,
                                 LambdaWrapper<FnTp>>::type
    make_fnptr( _FnTp&& fn ) noexcept;
};
template<typename Fn>
const Fn* LambdaWrapper<Fn>::fntor = nullptr;

template<typename _FnTp, typename FnTp = typename std::remove_reference<_FnTp>::type>
typename std::enable_if<std::is_class<FnTp>::value && !std::is_empty<FnTp>::value,
                        LambdaWrapper<FnTp>>::type
  make_fnptr( _FnTp&& fn ) noexcept
{
  static_assert(
    ( std::is_lvalue_reference<FnTp>::value && std::is_copy_constructible<FnTp>::value )
      || ( !std::is_lvalue_reference<FnTp>::value && std::is_move_constructible<FnTp>::value ),
    "Invalid type" );

  static FnTp fntor = std::forward<FnTp>( fn );
  if ( LambdaWrapper<FnTp>::fntor == nullptr )
    LambdaWrapper<FnTp>::fntor = std::addressof( fntor );
  return LambdaWrapper<FnTp>();
}

这个方案有个优点:由于标准中保证了每个 lambda 的类型唯一,所以对于所有相同的 lambda 函数对象,每次调用 make_fnptr 时所指向的内部变量都是相同的。

而且我们还利用了类的访问控制权限,保证了只有 lambda 的创建者才能访问到指向对应的 lambda 对象的函数指针。

但我不保证带引用捕获的 lambda 内部的引用生命周期问题。且这个方案会引入轻微的内存开销。

然后我们就可以这样使用:

int x = 10;
// 右值类型的 lambda 没问题
auto wrapper = make_fnptr( [&x]( int i ) { return x += i; } );

auto lambda = [&x]( int i ) { return x += i; };
// 左值类型也没有问题
auto wrapper2 = make_fnptr( lambda );

// 但是不支持不带捕获的 lambda 函数
// 这类函数可以直接转换为函数指针,所以类型转换是没必要的
// auto wrapper3 = make_fnptr( []() { std::cout << "Hello World!"; } );

// 调用 to_fnptr 方法、并提供函数的参数列表就能获得函数指针
auto ptr  = wrapper.to_fnptr<int>();
auto ptr2 = wrapper2.to_fnptr<int>();

函数的类型参数列表完全由 to_fnptr 的模板参数指定,你给什么它就返回什么。

但是与实际函数的参数列表不匹配的类型参数会导致编译错误。

int a = 2, b = 3;
double result = 4;

auto wrapper = make_fnptr( [&result]( int& a, int& b ) {
  std::swap( a, b );
  return ( a + b ) * result;
} ); // 参数列表是引用就传入引用
 auto ptr     = wrapper.to_fnptr<int&, int&>();

std::cout << "a = " << a << std::endl << "b = " << b << std::endl;
std::cout << "Return = " << ptr( a, b ) << std::endl;
std::cout << "a = " << a << std::endl << "b = " << b << std::endl;

测试程序见此

为什么这个包装器只支持带捕获的 lambda 函数对象?

因为其他函数类型都能轻松转换为函数指针。

4.添加一个类型容器

更进一步的,我们可以引入一个用于存储类型的模板;这个模板能够将一组类型在不同模板列表间相互传递。

template<typename...>
struct TypeList {};

// 可以这样用
using ArgList = TypeList<int, double, char*>;

这里的 TypeList 可以通过文本替换的方式更改为 std::tuple,效果是等价的。

然后我们直接要求使用者在创建一个 lambda 包装器时,必须显式给出对应 lambda 的函数参数列表;也就是说在 make_fnptr 的模板参数列表额外添加一个类型参数。

过程略,总之我们可以得到如下的实现。

template<typename...>
struct TypeList {};

template<typename, typename>
class LambdaWrapper;
template<typename Fn, template<typename...> class List, typename... Args>
class LambdaWrapper<Fn, List<Args...>> {
  static_assert( std::is_class<Fn>::value, "Only available to lambda" );
  static_assert( !std::is_empty<Fn>::value, "Only available to lambda with capture" );
  static_assert( std::is_same<List<Args...>, TypeList<Args...>>::value, "Only accepts TypeList types" );

  static const Fn* fntor;

  static typename std::result_of<Fn( Args... )>::type invoking( Args... args )
  //static std::invoke_result_t<Fn, Args...> invoking( Args... args )
  {
    return ( *fntor )( std::forward<Args>( args )... );
  }

  template<typename ParamList, typename FnTp>
  friend typename std::enable_if<
    std::is_class<typename std::remove_reference<FnTp>::type>::value
      && !std::is_empty<typename std::remove_reference<FnTp>::type>::value,
    decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type, ParamList>::invoking )>::type
    make_fnptr( FnTp&& fn ) noexcept;
};
template<typename Fn, template<typename...> class List, typename... Args>
const Fn* LambdaWrapper<Fn, List<Args...>>::fntor = nullptr;

template<typename ParamList = TypeList<>, typename FnTp>
typename std::enable_if<
  std::is_class<typename std::remove_reference<FnTp>::type>::value
    && !std::is_empty<typename std::remove_reference<FnTp>::type>::value,
  decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type, ParamList>::invoking )>::type
  make_fnptr( FnTp&& fn ) noexcept
{
  static_assert(
    ( std::is_lvalue_reference<FnTp>::value && std::is_copy_constructible<FnTp>::value )
      || ( !std::is_lvalue_reference<FnTp>::value && std::is_move_constructible<FnTp>::value ),
    "Invalid type" );

  using LambdaType  = typename std::remove_reference<FnTp>::type;
  using WrapperType = LambdaWrapper<LambdaType, ParamList>;

  static LambdaType fntor = std::forward<FnTp>( fn );
  if ( WrapperType::fntor == nullptr )
    WrapperType::fntor = std::addressof( fntor );
  return &WrapperType::invoking;
}

现在包装器类就成为了一个纯粹的内部实现,不会暴露任何方法。并且每次调用 make_fnptr 都只会立即产出一个函数指针,而不再会出现中间类对象。

int x = 10;
std::cout << "x = " << x << std::endl;

auto wrapper = make_fnptr<TypeList<int>>( [&x]( int i ) { return x += i; } );

auto lambda   = [&x]( int i ) { return x += i; };
auto wrapper2 = make_fnptr<TypeList<int>>( lambda );

// auto wrapper3 = make_fnptr( []() { std::cout << "Hello World!"; } );

std::cout << "x = " << x << std::endl;

std::cout << "x = " << x << std::endl;
std::cout << "delta = " << wrapper( 5 ) << std::endl;
std::cout << "x = " << x << std::endl;

std::cout << "delta = " << ( *wrapper2 )( 5 ) << std::endl;
std::cout << "x = " << x << std::endl;

测试程序见此

如果你愿意为 C++ 的所有函数类型写出一个类型萃取器的话,那么 make_fnptr 的函数参数列表需求其实也可以去除。

5.线程安全改造

在上面的代码中我们可以注意到:由于我们需要使用局部 static 变量的地址初始化另一个 static 变量,所以有且仅有这个初始化过程是线程不安全的。

对于函数内 static 变量而言,它的初始化构造在标准中被明确规定为线程安全,C++11 中有一个术语专门用于描述这种构造技巧:magic static

很显然,类的 static 成员只应该被初始化一次,所以我们可以考虑使用标准库提供的 std::call_once 函数初始化它。也就是像这样改造 make_fnptr 的实现:

using LambdaType  = typename std::remove_reference<FnTp>::type;
using WrapperType = LambdaWrapper<LambdaType, ParamList, InstanceTag>;

static std::once_flag seal;
static LambdaType fntor = std::forward<FnTp>( fn );
std::call_once( seal, []() { WrapperType::fntor = std::addressof( fntor ); } );
return &WrapperType::invoking;

lambda 函数本身就是一个仿函数对象;但与 lambda 不同,同种类型的仿函数对象所对应的函数并不相同。例如 std::function(int()) 既可以指 main,也可以指 std::rand

如果希望支持包装一些除了 lambda 之外的仿函数对象,如 std::function 等,那么我们还需要为这个封装器和包装函数提供一个模板参数标识,用于区分同种类型、但不同实例的仿函数对象。

现在我们可以得到这样的一个实现:

template<typename...>
struct TypeList {};

template<typename, typename, std::size_t = 0>
class LambdaWrapper;
template<typename Fn, template<typename...> class List, typename... Args, std::size_t Tag>
class LambdaWrapper<Fn, List<Args...>, Tag> {
  static_assert( std::is_class<Fn>::value, "Only available to lambda" );
  // 为了支持任意仿函数对象,这里不再约束类的大小
  static_assert( std::is_same<List<Args...>, TypeList<Args...>>::value,
                 "Only accepts TypeList types" );

  static const Fn* fntor;

public:
  static typename std::result_of<Fn( Args... )>::type invoking( Args... args )
  // static std::invoke_result_t<Fn, Args...> invoking( Args... args )
  {
    return ( *fntor )( std::forward<Args>( args )... );
  }

  template<typename ParamList, std::size_t InstanceTag, typename FnTp>
  friend
    typename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value,
                            decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type,
                                                     ParamList,
                                                     InstanceTag>::invoking )>::type
    make_fnptr( FnTp&& fn ) noexcept;
};
template<typename Fn, template<typename...> class List, typename... Args, std::size_t Tag>
const Fn* LambdaWrapper<Fn, List<Args...>, Tag>::fntor = nullptr;

template<typename ParamList = TypeList<>, std::size_t InstanceTag = 0, typename FnTp>
typename std::enable_if<std::is_class<typename std::remove_reference<FnTp>::type>::value,
                        decltype( &LambdaWrapper<typename std::remove_reference<FnTp>::type,
                                                 ParamList,
                                                 InstanceTag>::invoking )>::type
  make_fnptr( FnTp&& fn ) noexcept
{
  static_assert(
    ( std::is_lvalue_reference<FnTp>::value && std::is_copy_constructible<FnTp>::value )
      || ( !std::is_lvalue_reference<FnTp>::value && std::is_move_constructible<FnTp>::value ),
    "Invalid type" );

  using LambdaType  = typename std::remove_reference<FnTp>::type;
  using WrapperType = LambdaWrapper<LambdaType, ParamList, InstanceTag>;

  static std::once_flag seal;
  static LambdaType fntor = std::forward<FnTp>( fn );
  std::call_once( seal, []() { WrapperType::fntor = std::addressof( fntor ); } );
  return &WrapperType::invoking;
}

对于 lambda 和仿函数对象,我们可以有如下使用方法。

struct Functor {
  int operator()( int x, int y ) const noexcept { return x + y; }
};

int x = 10, y = 20;

auto wrapper = make_fnptr<TypeList<int>>( [&x]( int i ) { return x += i; } );
std::cout << "x = " << x << std::endl;
std::cout << "delta = " << wrapper( 5 ) << std::endl;
std::cout << "x = " << x << std::endl;

Functor fntor1, fntor2;

auto wrapper2 = make_fnptr<TypeList<int, int>, 0>( fntor1 );
auto wrapper3 = make_fnptr<TypeList<int, int>, 1>( fntor2 );
std::cout << "sum = " << wrapper2( x, y ) << std::endl;
std::cout << "sum = " << wrapper3( 114, 514 ) << std::endl;

测试程序见此

6.调整模板参数类型

既然我们已经在前文引入了一个容纳函数参数列表的类型容器,那么接下来其实可以更进一步,像 std::function 一样要求用户显式给出对应函数指针的函数类型。

注意:这里的函数类型是指形如 int(int, int)void(const std::string&)不完整类型,它们不能用于声明和定义变量,但是确实是一种实际存在的抽象类型。

而我们要返回的函数指针类型是一种指针类型,对应来说是这样的:int (*) (int, int)void (*) (const std::string&)

这种函数类型就像 std::tuple 一样,可以经由模板类的特化,被拆解为多个类型。如果我们能够通过拆解得到这个函数类型的返回值类型以及函数参数列表类型,那么就没有必要再引入一个类的静态成员,而是可以直接构造一个不带捕获的 lambda 函数,再经由类型转换拿到对应的函数指针。

实际上我们在这里将第二步中困扰我们的问题给解决了。

但由于这种函数类型需要经过模板的模式匹配后才能拆解得到返回值和参数列表,所以我们还是需要一个辅助类;不过这个辅助类已经不再具有“封装”的作用了,所以我们可以给它换个名字。

略过设计和实现过程不提,删减掉无关代码后,可以得到如下代码:

template<typename>
class FnPtrMaker;
template<typename Ret, typename... Params>
struct FnPtrMaker<Ret( Params... )> { // 特化匹配以拆解函数类型
  static_assert( std::is_function<Ret( Params... )>::value, "Only available to function type" );

  template<std::size_t InstanceTag = 0, typename Functor>
  static typename std::enable_if<std::is_class<typename std::decay<Functor>::type>::value,
                                 typename std::add_pointer<Ret( Params... )>::type>::type
    from( Functor&& fn ) noexcept
  {
    static_assert(
      ( std::is_lvalue_reference<Functor>::value && std::is_copy_constructible<Functor>::value )
        || ( !std::is_lvalue_reference<Functor>::value
             && std::is_move_constructible<Functor>::value ),
      "Functor must be copy or move constructible" );

    static typename std::decay<Functor>::type fntor = std::forward<Functor>( fn );
    return +[]( Params... args ) { return fntor( std::forward<Params>( args )... ); };
  }
};

template<typename FunctionType, std::size_t InstanceTag = 0, typename Functor>
decltype( FnPtrMaker<FunctionType>::template from<InstanceTag>( std::declval<Functor>() ) )
  make_fnptr( Functor&& fn ) noexcept
{
  return FnPtrMaker<FunctionType>::template from<InstanceTag>( std::forward<Functor>( fn ) );
}

观察代码可以发现:在模板函数 from 中,由于只有一个 magic static 语句以及一个获取函数地址的类型转换语句,故该函数已经做到了线程安全。

此时可以这样使用:

int x = 10;

auto wrapper = make_fnptr<int( int )>( [&x]( int i ) { return x += i; } );
std::cout << "x = " << x << std::endl;
std::cout << "delta = " << wrapper( 5 ) << std::endl;
std::cout << "x = " << x << std::endl;

你要是愿意的话,也可以绕开 make_fnptr 函数,直接通过类模板及其静态函数获得函数指针。

int x = 10;
auto wrapper = FnPtrMaker<int( int )>::from( [&x]( int i ) { return x += i; } );

测试程序见此

由于我们无法从 lambda 中直接提取函数类型,所以 make_fnptr 接收的函数类型与实际的仿函数对象不匹配时,错误信息会延迟到编译发生时爆出;而不是像 SFINAE 一样直接经由语言服务器在 IDE 前端报错。

也就是说函数类型不匹配会导致编译失败,但这种失败信息要到编译时才知道。

在代码实现中,辅助 lambda 的返回类型由编译器自动推断;所以使用时不能尝试从实际类型为 int() 的函数对象构造一个类型为 double() 的函数指针(也就是禁止了隐式的返回类型转变)。

这一行为可以通过显式指定辅助 lambda 的返回类型改变。

7.处理 noexcept 说明符

前文说过,lambda 函数实际上是一个重载了 operator() 运算符的类对象;显然这个被重载的运算符函数是一个成员函数(在 static operator() 提案进入标准前,即 C++23 之前,的确如此)。

那么问题来了:C++ 中成员函数的函数签名中,除去返回类型和入参列表,还包括了可选的 cv 修饰符、引用限定符及 noexcept 说明符;在之前的代码中,我们返回的函数指针类型里,这些属性被全部丢弃了。

这些限定符不只是有语义效果,它们还能辅助编译器更全面地优化代码。

在 C++17 之前的类型系统中,上述可选说明符都不会参与实际函数签名类型的构成,但是 noexcept 说明符会影响函数类型;而到了 C++17,noexcept 说明符被正式纳入函数签名类型。

值得庆幸的是,cv 修饰符和引用限定符只会出现在成员函数的函数签名中;如果尝试将一个不带捕获的 lambda 转换为函数指针,上述两种修饰符均会被抛弃。所以我们只需要处理 noexcept 说明符。

由于 C++17 的语法变动,这里出现了一个跨版本兼容问题:如果要兼容 C++11,我们就不能往函数类型里面填 noexcept。不过解决办法也很简单:我们单独处理每个函数参数实例的 noexcept 即可。

这描述起来可能会很抽象(虽然实际也是如此),但总之这是能做到的。

此外,因为我们不再限制被转换的函数类型必须是带捕获(含数据成员)的 lambda 类型,所以现有的代码会将一个能被隐式转为函数指针的 lambda 对象一并存放在局部 static 变量中;实际上这是多此一举的。所以我们要为 from 函数提供一个重载版本,专门针对可以被隐式转换为函数指针的空 lambda 类型。

这里要用到一些更复杂的模板元编程技巧,总之得到的代码如下:

#include <type_traits>
#include <utility>

template<typename>
class FnPtrMaker;
template<typename Ret, typename... Params>
class FnPtrMaker<Ret( Params... )> {
  static_assert( std::is_function<Ret( Params... )>::value, "Only available to function type" );

  template<typename Functor> // 辅助谓词模板,判断该函数对象的调用是否不会抛出异常
  struct is_nothrow_functor { // 实际上可以被替换为 C++17 的 std::is_nothrow_invocable
    static constexpr bool value = noexcept( std::declval<Functor>()( std::declval<Params>()... ) );
  };

  template<typename Functor>
  using FnPtrType = typename std::add_pointer<Ret( Params... ) noexcept(
    is_nothrow_functor<Functor>::value )>::type;

  template<typename Functor, typename = void>
  struct is_empty_lambda : std::false_type {};
  template<typename Functor>
  struct is_empty_lambda<
    Functor,
    typename std::enable_if<
      std::is_class<Functor>::value && std::is_empty<Functor>::value
      && std::is_same<decltype( +std::declval<Functor>() ), FnPtrType<Functor>>::value>::type>
    : std::true_type {};
  // 利用 decltype 检查表达式合法性,并且判断表达式的求值类型是否为我们期望的函数指针类型
  // 如果传入一个重载了会返回对应函数指针的一元 operator+ 空类对象,那么这里的判断会出错
  // 本质上是因为在判断空 lambda 时只能写出必要条件,而且我们(目前)找不到足够充分的约束条件

public:
  template<std::size_t InstanceTag, typename Functor>
  static constexpr // 类型转换发生在编译期,所以当然可以是 constexpr 的
    typename std::enable_if<is_empty_lambda<typename std::decay<Functor>::type>::value,
                            FnPtrType<typename std::decay<Functor>::type>>::type
    from( Functor&& fn ) noexcept
  { // 对于空的 lambda 类型,直接类型转换为函数指针
    return +fn;
  }
  template<std::size_t InstanceTag, typename Functor>
  static typename std::enable_if<std::is_class<typename std::decay<Functor>::type>::value
                                   && !is_empty_lambda<typename std::decay<Functor>::type>::value,
                                 FnPtrType<typename std::decay<Functor>::type>>::type
    from( Functor&& fn ) noexcept
  {
    static_assert(
      ( std::is_lvalue_reference<Functor>::value && std::is_copy_constructible<Functor>::value )
        || ( !std::is_lvalue_reference<Functor>::value
             && std::is_move_constructible<Functor>::value ),
      "Functor must be copy or move constructible" );

    static typename std::decay<Functor>::type fntor = std::forward<Functor>( fn );
    return +[]( Params... args ) noexcept( is_nothrow_functor<Functor>::value ) {
      return fntor( std::forward<Params>( args )... );
    };
  }
};

template<typename FunctionType, std::size_t InstanceTag = 0, typename Functor>
constexpr decltype( FnPtrMaker<FunctionType>::template from<InstanceTag>(
  std::declval<Functor>() ) )
  make_fnptr( Functor&& fn ) noexcept
{
  return FnPtrMaker<FunctionType>::template from<InstanceTag>( std::forward<Functor>( fn ) );
}

简单解释一下实现的核心原理:C++ 中的 noexcept 不仅可以作为一个函数说明符,它还是一个运算符,用于在编译期判断给定表达式的调用是否无异常抛出。

总而言之,使用方法没有改变,为了保证这段代码能跑,这里还是放一个测试程序

要注意:由于 C++ 类型系统中,异常说明符不同的函数指针之间不能互相赋值,所以在实际使用时请注意接口侧函数签名类型的异常规范,并适当调整传入的 lambda 本身的异常规范。

8.区分泛型 lambda

众所周知,C++14 后允许 lambda 被声明为泛型函数,而 C++20 则允许显式标注一个泛型 lambda的模板参数。

auto generic_lambda  = []( auto a, auto b ) { return a + b; }; // after C++14
auto template_lambda = []<typename T1, typename T2>( T1 a, T2 b ) { return a + b; }; // after C++20

很显然,一个被声明为泛型的 lambda 不可能被自动隐式转换为函数指针,因为此时该 lambda 的函数参数列表及返回值类型都是待定的。而且自 C++20 之后,标准对 lambda 所属的类类型的三个构造函数、两个赋值函数做了语义上的修改,这更精细化了能够区分 lambda 类型的条件。

C++20 的调整包括:允许不带捕获的 lambda 具有默认构造函数,复制、移动构造及赋值函数;而带捕获的 lambda 只允许存在被标注为 default 的复制及移动构造函数,其余函数全部被标注为 delete

因此此时我们可以围绕 lambda 类型的特点写出如下代码:

#include <type_traits>
#include <utility>

template<typename>
class FnPtrMaker;
template<typename Ret, typename... Params>
class FnPtrMaker<Ret( Params... )> {
  static_assert( std::is_function<Ret( Params... )>::value, "Only available to function type" );

  template<typename Functor>  // 辅助谓词模板,判断该函数对象的调用是否不会抛出异常
  struct is_nothrow_functor { // 实际上可以被替换为 C++17 的 std::is_nothrow_invocable
    static constexpr bool value = noexcept( std::declval<Functor>()( std::declval<Params>()... ) );
  };

  template<typename Functor>
  using FnPtrType =
    typename std::add_pointer<Ret( Params... ) noexcept( is_nothrow_functor<Functor>::value )>::type;

  template<typename Functor, typename = void>
  struct is_lambda : std::false_type {};
#if __cplusplus >= 202002L
  template<typename Functor>
  struct is_lambda<
    Functor,
    std::enable_if_t<std::is_class_v<Functor> && std::is_void_v<std::void_t<decltype( &Functor::operator() )>>
                     && std::is_empty_v<Functor> && std::is_default_constructible_v<Functor>
                     && std::is_copy_constructible_v<Functor> && std::is_move_constructible_v<Functor>
                     && std::is_copy_assignable_v<Functor> && std::is_move_assignable_v<Functor>
                     && std::is_void_v<std::void_t<decltype( +std::declval<Functor>() )>>>>
    : std::true_type {}; // For empty lambda
  template<typename Functor>
  struct is_lambda<
    Functor,
    std::enable_if_t<std::is_class_v<Functor> && std::is_void_v<std::void_t<decltype( &Functor::operator() )>>
                     && !std::is_empty_v<Functor> && !std::is_default_constructible_v<Functor>
                     && !std::is_copy_assignable_v<Functor> && !std::is_move_assignable_v<Functor>>>
    : std::true_type {}; // For lambda with captured params
#else
  template<typename Functor>
  struct is_lambda<
    Functor,
    typename std::enable_if<
      std::is_class<Functor>::value && std::is_void<decltype( &Functor::operator(), void() )>::value
      && !std::is_default_constructible<Functor>::value && !std::is_copy_assignable<Functor>::value
      && !std::is_move_assignable<Functor>::value>::type> : std::true_type {};
#endif

  template<typename Functor, typename = void>
  struct is_generic_lambda : std::false_type {};
#if __cplusplus >= 201402L
# if __cplusplus >= 202002L
  template<typename Functor>
  struct is_generic_lambda<
    Functor,
    std::enable_if_t<std::is_class_v<Functor>
                     && std::is_void_v<std::void_t<decltype( &Functor::template operator()<Params...> )>>
                     && std::is_empty_v<Functor> && std::is_default_constructible_v<Functor>
                     && std::is_copy_constructible_v<Functor> && std::is_move_constructible_v<Functor>
                     && std::is_copy_assignable_v<Functor> && std::is_move_assignable_v<Functor>>>
    : std::true_type {}; // For empty lambda
  template<typename Functor>
  struct is_generic_lambda<
    Functor,
    std::enable_if_t<std::is_class_v<Functor>
                     && std::is_void_v<std::void_t<decltype( &Functor::template operator()<Params...> )>>
                     && !std::is_empty_v<Functor> && !std::is_default_constructible_v<Functor>
                     && !std::is_copy_assignable_v<Functor> && !std::is_move_assignable_v<Functor>>>
    : std::true_type {}; // For lambda with captured params
# else
  template<typename Functor>
  struct is_generic_lambda<
    Functor,
    typename std::enable_if<
      std::is_class<Functor>::value
      && std::is_void<decltype( &Functor::template operator()<Params...>, void() )>::value
      && !std::is_default_constructible<Functor>::value && !std::is_copy_assignable<Functor>::value
      && !std::is_move_assignable<Functor>::value>::type> : std::true_type {};
# endif
#endif

public:
  template<std::size_t InstanceTag = 0, typename Functor>
  static constexpr // 类型转换发生在编译期,所以当然可以是 constexpr 的,如果使用的是
                   // C++20,这里也可以被标注为 consteval
    typename std::enable_if<is_lambda<typename std::decay<Functor>::type>::value
                              && std::is_empty<typename std::decay<Functor>::type>::value
                              && !is_generic_lambda<typename std::decay<Functor>::type>::value,
                            FnPtrType<typename std::decay<Functor>::type>>::type
    from( Functor&& fn ) noexcept
  { // 对于空的且非模板化的 lambda 类型,直接类型转换为函数指针
    return +fn;
  }
  template<std::size_t InstanceTag = 0, typename Functor>
  static typename std::enable_if<( is_lambda<typename std::decay<Functor>::type>::value
                                   && !std::is_empty<typename std::decay<Functor>::type>::value )
                                   || is_generic_lambda<typename std::decay<Functor>::type>::value,
                                 FnPtrType<typename std::decay<Functor>::type>>::type
    from( Functor&& fn ) noexcept
  {
    static_assert(
      ( std::is_lvalue_reference<Functor>::value && std::is_copy_constructible<Functor>::value )
        || ( !std::is_lvalue_reference<Functor>::value && std::is_move_constructible<Functor>::value ),
      "The Functor type must be copy or move constructible" );

    static typename std::decay<Functor>::type fntor = std::forward<Functor>( fn );
    return +[]( Params... args ) noexcept( is_nothrow_functor<Functor>::value ) {
      return fntor( std::forward<Params>( args )... );
    };
  }
};

template<typename FunctionType, std::size_t InstanceTag = 0, typename Functor>
constexpr decltype( FnPtrMaker<FunctionType>::template from<InstanceTag>( std::declval<Functor>() ) )
  make_fnptr( Functor&& fn ) noexcept
{
  return FnPtrMaker<FunctionType>::template from<InstanceTag>( std::forward<Functor>( fn ) );
}

使用方式并没有改变,只是更改了一些依语法标准而定的优化细节。

9.Reference

感谢 bilibili@Ayano_Aishi 在视频 BV1Hm421j7qc 下方的评论区中提供了本文代码的原始版本以及本文的灵感来源。

C++ lambda函数是一种匿名函数,它可以在需要函数对象的地方使用。捕获列表是lambda函数的一部分,用于指定lambda函数中可以访问的外部变量。 捕获列表可以包含以下几种形式的捕获方式: 1. 值捕获(value capture):通过将外部变量复制到lambda函数中来捕获它们。使用方式是在捕获列表中使用变量名,例如:[x, y]。 2. 引用捕获(reference capture):通过引用外部变量来捕获它们。使用方式是在捕获列表中使用变量名前加上&符号,例如:[&x, &y]。 3. 隐式捕获(implicit capture):可以根据上下文自动捕获外部变量。使用方式是在捕获列表中使用=表示值捕获,使用&表示引用捕获,例如:[=]、[&]。 4. 初始化捕获(init capture):可以在捕获列表中使用初始化语句来捕获外部变量。使用方式是在捕获列表中使用变量名和初始化语句,例如:[x = 10, y = std::move(z)]。 捕获列表的作用是将外部变量引入lambda函数的作用域中,使得lambda函数可以访问这些变量。捕获方式的选择取决于你对变量的使用需求,例如是否需要修改变量的值、是否需要访问变量的最新值等。 需要注意的是,被捕获的变量在lambda函数中是只读的,除非使用mutable关键字来声明lambda函数为可变的,这样就可以修改被捕获的变量的值了。 希望以上解释对你有帮助!如果你还有其他问题,请继续提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值