可变参数模板和折叠表达式的工程示例

可变参数模板和折叠表达式的工程示例

 次点击
36 分钟阅读

多传感器融合算法往往都需要一个时间同步算法,时间同步算法的输入一般是多个带时间戳的传感器数据。

使用C++不久的人,往往会给这多个传感器的类分别创建实例,在处理的时候再根据传感器数量进行遍历。这样写没有问题,就是不够优雅。

学习可变参数模板和折叠表达式需要先对C++中的Template有一定了解。模板在编译期实例化,意味着编译器会为不同的模板参数类型生成对应的代码。这种机制被称为"静态多态",区别于虚函数的"动态多态"。

可变参数模板

template<typename... SensorTypes>
class ApproximateTimeSynchronizer {
	static_assert(sizeof...(SensorTypes) >= 1, "At least one sensor stream required");
	static_assert(sizeof...(SensorTypes) <= 9, "Supports up to nine sensor streams");
    //...
}

这段代码中,template<typename... SensorTypes> class ApproximateTimeSynchronizer 声明了一个名为 ApproximateTimeSynchronizer 的类模板。

  • template<typename... SensorTypes>:可变参数模板的声明

    • typename...:表示后面的 SensorTypes是一个模板参数包,可以接收任意数量的类型参数
    • SensorTypes:参数包的名称,用于指代这一组类型参数。在这个场景中,可以代表不同的传感器数据,比如 cv::Mat类型的图像,XYZI格式的点云。
    • ...是C++11引入的可变参数模板语法,允许模板接受零个或多个任意类型的参数。结合 SensorTypes参数包,可以实现多个传感器类型的传入。
  • sizeof...方法是用于获取模板参数包中元素数量的运算符。

typename 关键字

typename 关键字

  1. 明确指定模板参数是一个类型,而非其他实体,如变量,函数。
  2. 指明依赖于模板参数的名称是类型,否则编译器可能认为它是变量或者函数。

总的来说,typename的两种用法都是在告诉你,后面是个类型名typename关键字的使用,给出一个例子。

template<typename... SensorTypes>
class ApproximateTimeSynchronizer {
	static_assert(sizeof...(SensorTypes) >= 1, "At least one sensor stream required");
	static_assert(sizeof...(SensorTypes) <= 9, "Supports up to nine sensor streams");

public:
	using TupleType = std::tuple<SensorTypes...>;
	using OptionalTuple = std::optional<TupleType>;

	OptionalTuple poll();

    //...
}

template<typename... SensorTypes>
typename ApproximateTimeSynchronizer<SensorTypes...>::OptionalTuple
ApproximateTimeSynchronizer<SensorTypes...>::poll() {
	process();
	if (ready_.empty()) {
		return std::nullopt;
	}
	TupleType result = std::move(ready_.front());
	ready_.pop_front();
	return result;
}
  1. template<typename... SensorTypes>的用法是明确告诉编译器:SensorTypes 是一个类型参数包。这种用法与 template<class... SensorTypes> 等价,两者都用于声明类型参数包,但是 template 语义更清晰。
  2. typename ApproximateTimeSynchronizer<SensorTypes...>::OptionalTuple的用法是显式声明该名称是一个(返回值)类型,一般用于在模板中使用依赖模板参数的嵌套类型时。一般格式为 typename 模板类<模板参数>::嵌套类型,示例代码中 ApproximateTimeSynchronizer<SensorTypes...> 是一个依赖于模板参数 SensorTypes... 的类,OptionalTuple 是这个类内部的嵌套类型using OptionalTuple = std::optional<TupleType>)。由于 OptionalTuple 的具体类型依赖于 SensorTypes...,编译器无法在编译早期确定它是类型还是其他成员,因此必须用 typename 明确标记它是一个类型。

再介绍一下依赖名称和非依赖名称。

  • 依赖名称:名称的含义依赖于模板参数。TupleType 类型依赖于模板参数 SensorTypes...,因为它是模板类 ApproximateTimeSynchronizer 的内部类型。
  • 非依赖名称:名称的含义不依赖于模板参数。比如是全局类型或当前作用域中已知的类型。

嵌套模板

template<typename... SensorTypes>
template<std::size_t Index, typename SensorT>
void ApproximateTimeSynchronizer<SensorTypes...>::add(SensorT&& data) {
	static_assert(Index < kSensorCount, "Sensor index out of range");
	using Expected = SensorTypeAt<Index>;
	static_assert(std::is_same_v<std::decay_t<SensorT>, Expected>, "Sensor type mismatch");

	const auto timestamp = sync_utils::getTimestamp(static_cast<const Expected&>(data));
       if (!handleTimeJump<Index>(timestamp)) {
	       return;
       }
    //...
}

示例代码给出了一个嵌套模板。

外层 template<typename... SensorTypes> 是类模板 ApproximateTimeSynchronizer 的参数。

内层 template<std::size_t Index, typename SensorT> 是成员函数 add 自己的模板参数。

在调用时, Index 无法被编译器自动推导,必须通过 <> 显式指定;SensorT 可以被自动推导。

synchronizer.add<0>(std::move(image));

这里需要引入类型参数非类型参数的概念。在C++模板中,非类型参数指不是类型的模板参数。简单来说,类型参数(如 typename T)出现在模板参数列表中;非类型参数(也可以说是变量形参)在模板中代表一个具体的值。模板的参数并不一定是类型,也可以是一个变量。

对下面这个简单的模板函数来说,调用时也需要显式指定非类型参数 N,并传递类型参数 value

template<int N, typename T>
void func(T value) { ... }
func<5>(3.14);  // <5> 显式指定 N=5,(3.14) 传递实参,T 被推导为 double

折叠表达式

template<typename... SensorTypes>
template<typename Fn>
void ApproximateTimeSynchronizer<SensorTypes...>::applyIndices(Fn&& fn) {
	applyIndicesImpl(std::forward<Fn>(fn), std::make_index_sequence<kSensorCount>{});
}
template<typename... SensorTypes>
template<typename Fn, std::size_t... Is>
void ApproximateTimeSynchronizer<SensorTypes...>::applyIndicesImpl(Fn&& fn, std::index_sequence<Is...>) {
	(fn(std::integral_constant<std::size_t, Is>{}), ...);
}
template<typename... SensorTypes>
void ApproximateTimeSynchronizer<SensorTypes...>::recoverAll() {
	applyIndices([&](auto index_tag) {
		constexpr std::size_t idx = decltype(index_tag)::value;
		auto& queue = std::get<idx>(queues_);
		auto& past = std::get<idx>(past_);
		while (!past.empty()) {
			auto value = std::move(past.back());
			past.pop_back();
			queue.push_front(std::move(value));
		}
	});
}

这个示例代码给出了3个函数,调用栈为 recoverAll ->applyIndices -> applyIndicesImpl。这几个函数的目的是为了遍历和处理和传感器数量对应的一组数据结构。

applyIndices函数:启动索引遍历。

  1. 模板外层 template<typename... SensorTypes>是类模板参数,内层 template<typename Fn>applyIndices自身的模板参数,Fn是可调用对象类型,如函数、Lambda 。
  2. 函数参数 Fn&& fn转发引用,结合 std::forward<Fn>(fn)实现完美转发。完美转发能够保持传入的 fn左值/右值的特性,避免不必要拷贝。
  3. std::make_index_sequence<kSensorCount>{}是C++14引入的模板,可以在编译期生成类型为 std::index_sequence<0, 1, ..., kSensorCount-1>的实例,也就是在编译期就可以确定索引序列。
  4. 最终将转发后的 fn和生成的索引序列传入 applyIndicesImpl函数,由其完成具体的索引展开。

applyIndicesImpl函数:展开索引并调用回调。

  1. 模板参数: FnapplyIndices,表示可调用对象类型。std::size_t... Is非类型参数包(Is为Index Sequence的缩写),由传入的 std::index_sequence<Is...>推导。

  2. 函数参数:FnapplyIndicesstd::index_sequence<Is...>用于在编译期推导出 Is...参数包。

  3. 折叠表达式:(fn(std::integral_constant<std::size_t, Is>{}), ...);通过使用C++17新特性中的折叠表达式,将参数包展开为多个函数调用。对 Is...中的每个索引 Is,生成对应的 fn(std::integral_constant<std::size_t, Is>{})。折叠表达式能够确保当当 Is...0,1,2时,按下面三个调度按顺序执行。

    (fn(std::integral_constant<std::size_t, Is>{}), ...)
    //等效于
    fn(std::integral_constant<std::size_t, 0>{});
    fn(std::integral_constant<std::size_t, 1>{});
    fn(std::integral_constant<std::size_t, 2>{});
    
  4. std::integral_constant 专门用于在编译时存储整数常量。它有一个 value 成员,可以访问存储的值。在这里的作用是将编译期索引 Is包装为一个类型,后续可通过 decltype获取其类型,进而提取 value

介绍 recoverAll之前,先回顾一下Lambda表达式,基本形式为:

[捕获列表](参数) { 函数体 }

在代码中:

  • [&]:捕获列表,表示捕获所有外部变量的引用,这样Lambda才可以修改 queues_past_
  • (auto index_tag):参数,auto 让编译器自动推断类型
  • { ... }:函数体,包含实际要执行的代码

recoverAll 函数:是具体的业务逻辑,用于恢复队列数据。调用 applyIndices函数并传入Lambda表达式。applyIndices的参数是一个泛型 Lambda(C++14 特性,参数用 auto声明,可接受任意类型)。

constexpr std::size_t idx = decltype(index_tag)::value;

decltype(index_tag):获取 index_tag编译时类型。

::value:获取 index_tag 中存储的编译时常量值

constexpr:表示 idx 是编译时常量。

看懂这行代码的功能需要结合前面 applyIndicesImpl的函数逻辑。

// applyIndices 的调用
applyIndices([&](auto index_tag) {
    // Lambda 体
});

applyIndices 接收一个 Lambda 表达式作为参数,这个 Lambda 有一个参数 auto index_tag

// applyIndices 调用 applyIndicesImpl
applyIndicesImpl(std::forward<Fn>(fn), std::make_index_sequence<kSensorCount>{});

applyIndicesImpl 接收两个参数:

  • Fn&& fn:就是传入的 Lambda
  • std::index_sequence<Is...>:编译时生成的索引序列
// applyIndicesImpl 折叠表达式
(fn(std::integral_constant<std::size_t, Is>{}), ...);

折叠表达式会将 fn 调用 kSensorCount 次,每次调用 fn 时,传入的是 std::integral_constant<std::size_t, Is>{},其中 Is 是编译时索引。

每次 applyIndicesImpl调用 fn时,实际是在执行:

// 对于 Is = 0
fn(std::integral_constant<std::size_t, 0>{});
// 对于 Is = 1
fn(std::integral_constant<std::size_t, 1>{});
// 对于 Is = 2
fn(std::integral_constant<std::size_t, 2>{});

到我们的Lambda中,index_tag的类型就变成了:

  • 第一次调用:std::integral_constant<std::size_t, 0>
  • 第二次调用:std::integral_constant<std::size_t, 1>
  • 第三次调用:std::integral_constant<std::size_t, 2>

总结一下这里的折叠表达式和lambda表达式都做了什么:

  1. 根据 sizeof...(SensorTypes)编译时确定了 kSensorCount的值。如 kSensorCount = 3
  2. applyIndices 调用 applyIndicesImpl,编译器在编译时根据 kSensorCount生成 std::index_sequence<0,1,2>
  3. applyIndicesImpl 的折叠表达式在编译时展开,将 Is... 替换为 0,1,2,生成如下代码:
fn(std::integral_constant<std::size_t, 0>{});
fn(std::integral_constant<std::size_t, 1>{});
fn(std::integral_constant<std::size_t, 2>{});
  1. lambda表达式会被调用3次:
  • 第一次调用:index_tag 类型 = std::integral_constant<std::size_t, 0>
    • decltype(index_tag)::value = 0
  • 第二次调用:index_tag 类型 = std::integral_constant<std::size_t, 1>
    • decltype(index_tag)::value = 1
  • 第三次调用:index_tag 类型 = std::integral_constant<std::size_t, 2>
    • decltype(index_tag)::value = 2

举一个具体的例子,编译器会将 applyIndices调用转换为:

// 编译器生成的代码(不是你写的,是编译器自动生成的)
{
    // 第一次调用
    auto index_tag = std::integral_constant<std::size_t, 0>{};
    constexpr std::size_t idx = decltype(index_tag)::value; // idx = 0
    auto& queue = std::get<0>(queues_);
    auto& past = std::get<0>(past_);
    while (!past.empty()) {
        auto value = std::move(past.back());
        past.pop_back();
        queue.push_front(std::move(value));
    }
  
    // 第二次调用
    auto index_tag = std::integral_constant<std::size_t, 1>{};
    constexpr std::size_t idx = decltype(index_tag)::value; // idx = 1
    auto& queue = std::get<1>(queues_);
    auto& past = std::get<1>(past_);
    while (!past.empty()) {
        auto value = std::move(past.back());
        past.pop_back();
        queue.push_front(std::move(value));
    }
  
    // 第三次调用
    auto index_tag = std::integral_constant<std::size_t, 2>{};
    constexpr std::size_t idx = decltype(index_tag)::value; // idx = 2
    auto& queue = std::get<2>(queues_);
    auto& past = std::get<2>(past_);
    while (!past.empty()) {
        auto value = std::move(past.back());
        past.pop_back();
        queue.push_front(std::move(value));
    }
}

总结

所以,挺优雅的,是吧。

优雅.webp

© 本文著作权归作者所有,未经许可不得转载使用。