目录

C++20:泛型lambda表达式

系列 - C++ Features

最近在工作中,遇到一个需求,要读取某个硬件加速器的一系列计数寄存器,以确定网络通信的状态。这一系列寄存器有类似的定义:

union reg {
    uint64_t u;
    struct reg_s {
        uint64_t stat : 48; //!< statistics
        uint64_t      : 16; //!< reserved
    }s;
};

同时,芯片厂商提供的驱动库中提供了一个同名但全大写的API,用于确定寄存器的地址:

uint64_t REG(const uint64_t abId); // ab: acclerating block

由于寄存器的地址空间不受kernel管理,因此还提供了一个通用的,读取寄存器的API:

uint64_t read_reg64(const uint64_t regAddr);

一般情况下,针对单个寄存器的读取操作,可能最符合直觉的写法应该就是这样了:

void dumpAbStatRegs(const uint64_t abId)
{
    auto r = reg({.u = 0});
    r.u = read_reg64(REG(abId));
    // ...
}

但是,这一系列寄存器有十几二十几个,难道我要把这段代码复制N次?这也太不符合我的风格了,于是自然而然想到了封装。读取寄存器的操作遵循一个非常固定的模式,在这个模式中,只有寄存器类型reg和寄存器寻址APIREG会发生变化。那么,能使用lambda来完成这件事情吗?

泛型lambda是C++20引入的一个新特性,一个基本的泛型lambda的声明如下:

auto lambda = [captures] <template_params> (params) -> trailing_type
{
    // body
};

和普通的lambda表达式相比,泛型lambda只是在捕获列表和形参列表之间插入了一个模板形参列表。该模板形参列表除了没有使用template关键字之外,与普通形式的模板形参没有区别,也可以接受concept作为了类型约束。上述的声明等价于:

struct anonymous
{
    template<template_params>
    auto operator() (params) -> trailing_types
    {
        // body
    }
} lambda;

了解到这个特性以后,我立刻使用泛型lambda写了第一版实现(含一个类型约束):

template <typename Reg>
concept StatReg = requires (Reg r)
{
    std::is_union_v<Reg>;                       // Reg should be union
    r.u;                                        // there should be a member named as `u` in Reg
    std::is_same_v<decltype(r.u), uint64_t>;    // type of `r.u` should be u64
    r.s.stat;                                   // there should be a structure member, and
                                                    // 1. it is named as `s`
                                                    // 2. there should be a member named as `stat` in `s`
};


// in function dumpAbStatRegs()
auto readStatReg = [=]<StatReg R, std::function<uint64_t(const uint64_t)> RF>()
{
    auto r = R({.u = 0});
    r.u = read_reg64(RF(abId)); // abId is captured
    return r.s.stat;            // return this field for logging
}

auto r1 = readStatReg<reg1, REG1>(); // ERROR!

显然,我打算把寄存器类型和寻址API直接通过模板形参进行传递。但是,这个实现有两个比较麻烦的问题需要解决。

先考虑一个比较简单的泛型lambda:

auto foo = []<typename T>(const T& t)
{
    return t;
}

auto rt = foo(1u); // T would be deduced as unsigned integer, u8, u16, u32 or others

这个例子过于简单,但是反映了C++模板一个重要的特性,模板参数推导。得益于这个特性,我们在使用泛型API时,不一定要显式声明入参的类型,从而简化代码。

但是readStatReg并没有入参,因此调用它时必须显式声明模板参数。这时候另一个问题暴露出来了,考虑到泛型lambda的等价形式,readStatReg的类型并非一个模板,而它的成员函数operator()才是模板!因此,对它的调用应该写作:

auto r1 = readStatReg.template operator()<reg1, REG1>();

可以看到,第一版实现中调用完全搞错了模板实例化的位置!不过说真的,与其这样使用泛型lambda,还不如单独实现一个独立的函数模板,起码可读性要好一些……

这个问题与泛型lambda无关,但是值得一提。即std::function并非constexpr对象,因为它可以处理多种不同的可调用对象,在传入带捕获的lambda时,它的构造过程不可避免地要动态分配内存。

基于以上原因,模板形参列表中的非类型参数RF的声明是非法的,即便我的本意是想传入一个简单的全局函数。解决方法也很简单,直接使用函数指针类型替代std::function即可。

  1. cppreference - lambda表达式
  2. Stack Overflow - How to provide template arguments to a lambda with a call operator template?