c++20中显式的lambda模板参数列表


问题介绍

想临时写个lambda函数,但lambda函数需要是模板化的,而且模板化的参数既不是输入参数也不是输出参数,只是中间过程需要用到的一个类型。这时候需要用到“c++20中的显式模板参数列表”。

先简单写一个结构体,具有类型擦除的特性。

struct Value {
    int _type = 1;         // 0 for char, 1 for int
    void *_data = nullptr; // the length of _data array is 10.
};

Value类型内部通过一个_type来枚举 _data 存储的数据类型。 这里用0表示char数组,1表示int数组。其他值可以留作他用。

_data 表示 _type类型的数据,为了简化问题,这里是长度为10的数组,也就是要么是char [10]要么是int [10]

后续我们会介绍四个函数, 一个初始化init函数,初始化Value的_data数组。三个用于数组批量加上一个整数的函数add1add2add3

一个“普通”的lambda:init

为了初始化Value类型的_data, 我们希望用T* init(T d)返回一个长度为10的、初始值都为d的指针, 可以写成如下lambda:

auto init = [](auto d) {
    auto x = new decltype(d)[10];
    for (int i = 0; i < 10; ++i) {
        x[i] = d;
    }
    return x;
};

ok,没啥问题,这是个用auto模板化的lambda。

初始化的整个代码如下:

// ...

int main() {
    auto init = [](auto d) {
        auto x = new decltype(d)[10];
        for (int i = 0; i < 10; ++i) {
            x[i] = d;
        }
        return x;
    };

    Value v{._type = 0, ._data = nullptr};

    if (v._type == 0) {
        v._data = (void *)init(42);
    } else if (v._type == 1) {
        v._data = (void *)init('a');
    } else {
        std::abort();
    }
    // ...
}

一个不普通的的lambda和对应的模板函数

现在我们希望通过一个add函数,给Value的每个元素加上一个标量。 那么可以将”加“的逻辑抽象出来,不然每个_type用if判断一下,就要复制一遍代码。

先来用add1表示都加上1,这是一个普通的模板函数。

template <typename T> void add1(Value data10) {
    T *x = (T *)data10._data;
    for (int i = 0; i < 10; ++i) {
        x[i] += 1;
        std::cout << x[i] << ' ';
    }
    std::cout << '\n';
}

前边的模板参数T只是针对函数内部某处逻辑上用到的。既不是函数的返回参数,也不是输入参数。

普通模板函数没有太大问题。但是这函数可能只会集中在_type是数组的if大法里,这也是我遇到的问题,需要判断这个类型擦除的数据是不是数组,如果是数组则执行add的逻辑,所以希望这个add函数能够离if大法比较近,一眼就能看完lambda的逻辑,而不是跳来跳去。

但是写lambda会遇到问题,这时不太能用auto来表示模板参数。T既不是入参也不是返回参数。 通过询问万能的群友,我们可以用一个固定输入为nullptr或者别的什么的参数,通过输入参数表示这个中间用到的模板参数T。 代码如下:

auto add2 = [](const Value &data10, auto *_null) {
    auto *x = (decltype(_null))data10._data;
    for (int i = 0; i < 10; ++i) {
        x[i] += 2;
        std::cout << x[i] << ' ';
    }
    std::cout << '\n';
};

最后一个_null表示一个指针,但是不会在运行时用到他的值,只是在编译期间传递入参的类型信息。 函数体第一行auto *x = (decltype(_null))data10._data;用到了这个参数。

比如调用时

if (v._type == 1) {
    add1<char>(v);
    add2(v, (char *)nullptr);
}

(char *)nullptr来指示需要特化的lambda模板。

最后一个lambda

总感觉这个问题还有naiive的方案。搜了一下auto表示lambda的模板参数竟然是c++14的东西,而不是20的,所以c++20对lambda更新了什么?

然后找到了How to provide template arguments to a lambda with a call operator template

就有了如下代码,c++20的lambda的[]()之间可以写<>了。

auto add3 = []<typename T>(const Value &data10) {
    auto *x = (T *)data10._data;
    for (int i = 0; i < 10; ++i) {
        x[i] += 3;
        std::cout << x[i] << ' ';
    }
    std::cout << '\n';
};

调用时需要写的比较长,也好理解,lambda其实是匿名函数对象,是个对象,不是狭义上的函数。

if (v._type == 0) {
    add1<int>(v);
    add3.template operator()<int>(v);
}

完整代码

https://godbolt.org/z/f5zYbzj35

上边是完整的一个代码,可以修改第27行,

Value v{._type = 1, ._data = nullptr};

改为

Value v{._type = 1, ._data = nullptr};

可以得到int数组的结果。

参考内容

上边这些内容能解决我的问题了,lambda其他的内容也没有深入研究,列一下参考的链接吧

  1. How to provide template arguments to a lambda with a call operator template
  2. Lambda expressions (since C++11)
  3. c++20 The Complete guide 第11章lambda表达式