一、IMU运动学
1、测量值:
常用六轴IMU是由陀螺仪(Gyroscope)和加速度计(Acclerator)两部分组成。
- 陀螺仪测量:角速度。
- 加速度计:加速度。
安装要尽量保证IMU的安装位置在车辆中心。避免由IMU与载体系不重合引来的问题。
2、噪声模型
IMU噪声由两部分组成:
- 测量噪声(Measurement Noise)。
- 零偏(Bias)。
- 哪怕车辆静止:加速度和角速度测量值均值也不为零,而是带有一定的偏移。
- 偏移量是由IMU内部的机电测量装置导致的。
- 建立数学模型,认为零偏也是系统的状态量。
测量值:数学模型为:理论值 + 零偏 + 噪声。
- 噪声:是一个高斯过程,协方差随着时间变得越来越大,IMU本身的测量值也随着采样时间变长而变得更加不准确。因此采样频率越高的IMU,其精度也会相对较高。
- 角速度就是角度的导数,加速度又是速度的导数。所以 IMU 的测量噪声,也可以解释为角度的随机游走和速度的随机游走。因此请不要看到随机游走这四个字,只想到零偏部分,而应该从整体层面看待问题。
- 零偏:随机游走/布朗运动/维纳过程。而零偏部分由布朗运动描述,呈现随机游走状态。表现在实际当中,则可以认为一个 IMU 的零偏会从某个初始值开始,随机地向附近做不规律的运动。运动的幅度越大,就称它的零偏越不稳定。所以质量好的IMU,零偏应该保持在初始值附近不动。
- 随机游走实际上就是导数为高斯过程的随机过程,
3、思维教育
请读者注意,这里的高斯过程和布朗运动过程,都是IMU 测量数据的数学模型。数学模型并不一定是和真实世界完全对应的。有时,数学模型是对真实世界的一种简化,便于后续的算法计算。读者应当理解这种思想。后面我们对许多系统进行线性近似,保留各种一阶项,也是基于这种简化的思想。真实 IMU 的测量噪声和零偏受到非常多因素的影响,例如载体的震动、温度,IMU自身的受力、标定与安装误差,等等。把它们建模为两个随机过程,更多是为了方便状态估计算法的计算,不是完美、精确的建模方式。这种先简化、再补偿的思想,在现实中十分常见。
我想起一句名言:为啥研究各种线性假设:因为非线性的大家数学水平都达不到。所以都会把问题转为线性近似。直白点说:一阶泰勒公式展开。局部有效。不断逼近。
总结一句:差不多得了。
4、离散时间噪声模型:
这个离散的是咱们代码最感兴趣的。其中时间间隔对应代码中dt。
5、查看IMU手册:
噪声和零偏参考值。
二、IMU航迹推算
通过IMU的测量值,估算推断系统的运动。
只有IMU 时如何推断系统的运动状态。我们发现这样做是可行的,但只有IMU 的系统需要对 IMU 读数进行二次积分,其测量误差与零偏的存在会导致状态变量很快地漂移。
1、PVQ状态方程
增量形式:
这个公式,特别是公式3.15,编程要用的。
2、累计运动
三、IMU递推代码实验
1、IMU代码
首先看IMU这个类/结构体:在common/imu.h中。在common中,说明后面的IMU都是这个结构。
//
// Created by xiang on 2021/7/19.
//
#ifndef MAPPING_IMU_H
#define MAPPING_IMU_H
#include <memory>
#include "common/eigen_types.h"
namespace sad {
/// IMU 读数
struct IMU {
IMU() = default;
IMU(double t, const Vec3d& gyro, const Vec3d& acce) : timestamp_(t), gyro_(gyro), acce_(acce) {}
double timestamp_ = 0.0;
Vec3d gyro_ = Vec3d::Zero(); // gyroscope 陀螺仪
Vec3d acce_ = Vec3d::Zero(); // accelerometer 加速度计
};
} // namespace sad
using IMUPtr = std::shared_ptr<sad::IMU>;
#endif // MAPPING_IMU_H
属性:分别是:
- 时间戳timestamp_
- 陀螺仪数值gyro_
- 加速度计数值acce_
然后这三个属性的构造函数。以及基于这个类别的智能指针。
2、导航状态NavState
在看书中代码之前,还需要看一个类:NavStated状态变量这个泛型类。也在common中,nav_state.h
//
// Created by xiang on 2021/7/19.
//
#ifndef SAD_NAV_STATE_H
#define SAD_NAV_STATE_H
#include <sophus/so3.hpp>
#include "common/eigen_types.h"
namespace sad {
/**
* 导航用的状态变量
* @tparam T 标量类型
*
* 这是个封装好的类,部分程序使用本结构体,也有一部分程序使用散装的pvq,都是可以的
*/
template <typename T>
struct NavState {
using Vec3 = Eigen::Matrix<T, 3, 1>;
using SO3 = Sophus::SO3<T>;
NavState() = default;
// from time, R, p, v, bg, ba
explicit NavState(double time, const SO3& R = SO3(), const Vec3& t = Vec3::Zero(), const Vec3& v = Vec3::Zero(),
const Vec3& bg = Vec3::Zero(), const Vec3& ba = Vec3::Zero())
: timestamp_(time), R_(R), p_(t), v_(v), bg_(bg), ba_(ba) {}
// from pose and vel
NavState(double time, const SE3& pose, const Vec3& vel = Vec3::Zero())
: timestamp_(time), R_(pose.so3()), p_(pose.translation()), v_(vel) {}
/// 转换到Sophus
Sophus::SE3<T> GetSE3() const { return SE3(R_, p_); }
friend std::ostream& operator<<(std::ostream& os, const NavState<T>& s) {
os << "p: " << s.p_.transpose() << ", v: " << s.v_.transpose()
<< ", q: " << s.R_.unit_quaternion().coeffs().transpose() << ", bg: " << s.bg_.transpose()
<< ", ba: " << s.ba_.transpose();
return os;
}
double timestamp_ = 0; // 时间
SO3 R_; // 旋转
Vec3 p_ = Vec3::Zero(); // 平移
Vec3 v_ = Vec3::Zero(); // 速度
Vec3 bg_ = Vec3::Zero(); // gyro 零偏
Vec3 ba_ = Vec3::Zero(); // acce 零偏
};
using NavStated = NavState<double>;
using NavStatef = NavState<float>;
} // namespace sad
#endif
先看属性:
- 时间戳timestamp_
- 描述姿态的旋转矩阵李群形式SO3 R_
- 位移:p_
- 速度:v_
- gyro 零偏:bg_
- acce 零偏:ba_
针对这些属性:有个含参构造函数:
- 一个显示声明形式,属性分别赋值。
- 另一个基于李群SE3形式的pose,来获取R_(pose.so3())和p_(pose.translation())。
提供了一个SE3导航状态泛型只读查询函数GetSE3();
为了打印方便,友元函数重载了<<运算符。
针对这个泛型类:声明了float和double两种简写方式。
3、IMU递推代码
IMU递推积分类。功能执行类。
slam_in_autonomous_driving-master/src/ch3/imu_integration.h
//
// Created by xiang on 2021/11/5.
//
#ifndef SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H
#define SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H
#include "common/eigen_types.h"
#include "common/imu.h"
#include "common/nav_state.h"
namespace sad {
/**
* 本程序演示单纯靠IMU的积分
*/
class IMUIntegration {
public:
IMUIntegration(const Vec3d& gravity, const Vec3d& init_bg, const Vec3d& init_ba)
: gravity_(gravity), bg_(init_bg), ba_(init_ba) {}
// 增加imu读数
void AddIMU(const IMU& imu) {
double dt = imu.timestamp_ - timestamp_;
if (dt > 0 && dt < 0.1) // 假设IMU时间间隔在0至0.1以内
{
// (公式3.15b)(公式3.15c)(公式3.15a)
p_ = p_ + v_ * dt + 0.5 * gravity_ * dt * dt + 0.5 * (R_ * (imu.acce_ - ba_)) * dt * dt;
v_ = v_ + R_ * (imu.acce_ - ba_) * dt + gravity_ * dt;
R_ = R_ * Sophus::SO3d::exp((imu.gyro_ - bg_) * dt);
} // 需要用到imu的陀螺仪和加速度计数值:imu.gyro_和imu.acce_;需要用重力g:gravity_。
// 需要用到imu参数ba_和bg_
// 更新时间 his
timestamp_ = imu.timestamp_;
}
/// 组成NavState 读取状态
NavStated GetNavState() const { return NavStated(timestamp_, R_, p_, v_, bg_, ba_); }
/// 读取状态分量
SO3 GetR() const { return R_; }
Vec3d GetV() const { return v_; }
Vec3d GetP() const { return p_; }
private:
// 累计量
SO3 R_; // Rotation 姿态朝向
Vec3d v_ = Vec3d::Zero(); // Velocity 速度
Vec3d p_ = Vec3d::Zero(); // Pos 位置
double timestamp_ = 0.0;
// 零偏,由外部设定
Vec3d bg_ = Vec3d::Zero(); // gyroscope bias 陀螺仪零偏
Vec3d ba_ = Vec3d::Zero(); // accelerometer bias 加速度计零偏
Vec3d gravity_ = Vec3d(0, 0, -9.8); // 重力
};
} // namespace sad
#endif // SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H
还是先看属性:
- 累积量:姿态朝向R_
- 累积量:速度v_
- 累积量:位置p_
- 时间戳:这个时间戳
- 这个时间戳:起始为零,
- 主要是为了记录上一帧IMU时间,
- 与下一帧计算时间间隔dt。
- IMU外参:Vec3d bg_:gyroscope bias 陀螺仪零偏
- IMU外参:Vec3d ba_:accelerometer bias 加速度计零偏
- IMU外参:Vec3d gravity_ :重力
使用IMU积分,必须要获取外参,才可以推导后面的状态。构造函数为外参属性赋值。
其他属性为状态量,需要累计或者输出的,赋值给导航状态实例。这个实例实际上是通过状态查询函数GetNavState()构造的,并输出的。并提供了另外三个只读查询函数GetR(),GetP(),GetV()。
最重要的功能执行:在void AddIMU(const IMU& imu)中,传入一个imu状态读数(测量值)。然后累计,累计的公式参照(3.15)。
// 增加imu读数
void AddIMU(const IMU& imu) {
double dt = imu.timestamp_ - timestamp_;
if (dt > 0 && dt < 0.1) // 假设IMU时间间隔在0至0.1以内
{
// (公式3.15b)(公式3.15c)(公式3.15a)
p_ = p_ + v_ * dt + 0.5 * gravity_ * dt * dt + 0.5 * (R_ * (imu.acce_ - ba_)) * dt * dt;
v_ = v_ + R_ * (imu.acce_ - ba_) * dt + gravity_ * dt;
R_ = R_ * Sophus::SO3d::exp((imu.gyro_ - bg_) * dt);
} // 需要用到imu的陀螺仪和加速度计数值:imu.gyro_和imu.acce_;需要用重力g:gravity_。
// 需要用到imu参数ba_和bg_
// 更新时间 his
timestamp_ = imu.timestamp_;
}
4、读写文件IO
有了IMU递推累计的功能执行类,测试时src/ch3/run_imu_integration.cc,还需要补充一些知识。
在研究代码中,发现这个读写文件用了一些语法糖。读写中bind和function的拓展学习。以及lambda的学习。同时也涉及ROSBag的读写操作。主要在common/io_utils.h和common/io_utils.cc两个文件中。在学习这两个文件之前,补充以下知识。
4.1 函数作为一种类型
这部分为补充阅读:主要参照《C++ Primer 第5版》第344页第10章《泛型算法》第10.3节《定制操作》和第533页第14章《操作重载与类型转换》第14.8.1节《lambda是函数对象》。
谓词:
- 向算法函数传递函数。这个函数作为一个变量形式传入的。
- 谓词有一元谓词和二元谓词(参数个数不同),输入的参数主要是待比较或处理的序列元素。
- 如果我们需要一个一元谓词,同时还有额外的筛选条件,这种情况下具名的一元谓词无法满足条件。
- 比如案例中介绍的find_if的算法:前两个参数接受一对迭代器,表示一个范围。第三个参数是一个一元谓词。比如我们需要筛选具有某个长度的单词,这个长度也是个变量,此时无法传入到一元谓词中,因为一元谓词只有一个输入的元素参数。这个长度参数用lambda的捕获列表来传入。
- lambda表达式比具名谓词用途更加广泛:也可以说:不仅仅用于谓词用法。
可调用对象:
对于一个对象或一个表达式,如果可以对其使用调用运算符(),则称它为可调用对象。
可调用对象包含以下五种:
- 函数
- 函数指针
- 重载了函数调用运算符的类
- lambda表达式
- bind创建的对象
4.2 lambda表达式
lambda表达式
一个lambda表达式表示一个可调用的代码单元:可以理解为一个未命名的内联函数。
显著标志是捕获列表和函数体。其他如尾置返回类型可选。
捕获列表:lambda所在函数中定义的局部变量的列表。
slam_in_autonomous_driving-master/src/ch3/run_imu_integration.cc
/// 记录结果
// 定义几个lambda类型的【可调用对象】,可以作为【函数调用】的形式来使用。
// 用个 变量名 来接这部分 可调用的代码单元。可以理解为就是建立了一个内联函数。
// C++ primer 14.8.1节(507页)和10.3.3节(349页)介绍了这种类型。
auto save_result = [](std::ofstream& fout, double timestamp, const Sophus::SO3d& R, const Vec3d& v,
const Vec3d& p) {
auto save_vec3 = [](std::ofstream& fout, const Vec3d& v) { fout << v[0] << " " << v[1] << " " << v[2] << " "; };
auto save_quat = [](std::ofstream& fout, const Quatd& q) {
fout << q.w() << " " << q.x() << " " << q.y() << " " << q.z() << " ";
};
fout << std::setprecision(18) << timestamp << " " << std::setprecision(9);
save_vec3(fout, p);
save_quat(fout, R.unit_quaternion());
save_vec3(fout, v);
fout << std::endl;
};
当定义一个lambad时,编译器生成一个与lambda对应的新的(未命名的)类类型。
当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
如果lambda的捕获列表为空,通常使用函数来代替它。上面的三个全部可以用函数替代。
4.3 bind参数绑定
属于拓展部分:
解决谓词或者其他函数对象参数补充的问题。还可以使用bind函数与占位符配合。
可以将bind函数看作一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
语法形式:
auto newCallable = bind(oldCallable, arg_list);
下面这个函数无法用到find_if函数中。
bool check_size(const string &s, string::size_type sz)
{
return s.size() >= sz;
}
我们使用bind和占位符
auto check6 = bind(check_size, _1, sz);
占位符:
- 为_n形式。占用第n个参数位置,表明预留参数位置。
- 预留多少个,取决于oldCallable对象函数的参数个数。
- 然后拓展或者说绑定的参数,通过后面的参数传入。
- 导致的后果是:只需要输入占位符预留的参数即可。完成了参数个数的控制。
- 同时这个占位符还可以完成参数位置的控制。
这个check6,只接受一个参数。接收的第一个参数,放在预留的参数位置上。这样就可以用到find_if函数中了。
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
std::bind的思想其实是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。
4.4 function
这部分为补充阅读:主要参照《C++ Primer 第5版》第511页第14章《重载运算与类型转换》第14.8.3节《可调用对象与function》。
可调用对象包含以下五种:
- 函数
- 函数指针
- 重载了函数调用运算符的类
- lambda表达式
- bind创建的对象
可调用的对象也有类型。
- 每个lambda都有它自己唯一的(未命名)类类型。
- 函数及函数指针的类型则与由其返回值类型和实参类型决定。
调用形式指明了调用返回的类型以及传递给调用的实参类型。
一种调用形式对应一个函数类型。比如:int(int, int)
function是一个模板。定义了一种函数类型,指明了调用形式。
/// 定义回调函数
using IMUProcessFuncType = std::function<void(const IMU &)>;
using OdomProcessFuncType = std::function<void(const Odom &)>;
using GNSSProcessFuncType = std::function<void(const GNSS &)>;
TxtIO类负责读取txt,至于如何处理txt的内容,则支持以回调函数的形式传入。
/**
* 读取本书提供的数据文本文件,并调用回调函数
* 数据文本文件主要提供IMU/Odom/GNSS读数
*/
class TxtIO {
public:
TxtIO(const std::string &file_path) : fin(file_path) {}
/// 定义回调函数
using IMUProcessFuncType = std::function<void(const IMU &)>;
using OdomProcessFuncType = std::function<void(const Odom &)>;
using GNSSProcessFuncType = std::function<void(const GNSS &)>;
TxtIO &SetIMUProcessFunc(IMUProcessFuncType imu_proc) {
imu_proc_ = std::move(imu_proc);
return *this;
}
TxtIO &SetOdomProcessFunc(OdomProcessFuncType odom_proc) {
odom_proc_ = std::move(odom_proc);
return *this;
}
TxtIO &SetGNSSProcessFunc(GNSSProcessFuncType gnss_proc) {
gnss_proc_ = std::move(gnss_proc);
return *this;
}
// 遍历文件内容,调用回调函数
void Go();
private:
std::ifstream fin;
IMUProcessFuncType imu_proc_;
OdomProcessFuncType odom_proc_;
GNSSProcessFuncType gnss_proc_;
};
这三个函数类型分别处理IMU数据、Odom数据、和GNSS数据。这三种类型的实例在这个TxtIO类中以属性字段的形式存在imu_proc_、odom_proc_、gnss_proc_。
这个类中,同时提供了三个函数字段属性的赋值函数SetIMUProcessFunc()。
这里的回调类型定义只是指明了传入回调的函数的调用形式,并没有把真正的函数操作传入,真正要执行的函数操作通过实际调用时,通过lambda的形式传入了,这部分代码在src/ch3/run_imu_integration.cc中。具体执行的函数任务。
std::ofstream fout("./data/ch3/state.txt");
io.SetIMUProcessFunc([&imu_integ, &save_result, &fout, &ui](const sad::IMU& imu) {
imu_integ.AddIMU(imu);
save_result(fout, imu.timestamp_, imu_integ.GetR(), imu_integ.GetV(), imu_integ.GetP());
if (ui) {
ui->UpdateNavState(imu_integ.GetNavState());
usleep(1e2);
}
}).Go();
在IMU递推中, 将imu传入之后,然后执行递推中的状态保存。
直到执行Go()时,才执行读取读取数据。然后才将读取的数据传递给函数字段成员。执行操作。
src/common/io_utils.cc
void TxtIO::Go() {
if (!fin) {
LOG(ERROR) << "未能找到文件";
return;
}
while (!fin.eof()) {
std::string line;
std::getline(fin, line);
if (line.empty()) {
continue;
}
if (line[0] == '#') {
// 以#开头的是注释
continue;
}
// load data from line
std::stringstream ss;
ss << line;
std::string data_type;
ss >> data_type;
if (data_type == "IMU" && imu_proc_) {
double time, gx, gy, gz, ax, ay, az;
ss >> time >> gx >> gy >> gz >> ax >> ay >> az;
// imu_proc_(IMU(time, Vec3d(gx, gy, gz) * math::kDEG2RAD, Vec3d(ax, ay, az)));
imu_proc_(IMU(time, Vec3d(gx, gy, gz), Vec3d(ax, ay, az)));
} else if (data_type == "ODOM" && odom_proc_) {
double time, wl, wr;
ss >> time >> wl >> wr;
odom_proc_(Odom(time, wl, wr));
} else if (data_type == "GNSS" && gnss_proc_) {
double time, lat, lon, alt, heading;
bool heading_valid;
ss >> time >> lat >> lon >> alt >> heading >> heading_valid;
gnss_proc_(GNSS(time, 4, Vec3d(lat, lon, alt), heading, heading_valid));
}
}
LOG(INFO) << "done.";
}
其他参考文献:C++中std::function传递深度解析:值传递与引用传递的底层原理
std::function
是一个函数包装器,它可以存储、复制和调用任何可调用的目标——函数、lambda 表达式、绑定表达式,甚至是其他函数对象。它提供了一种将函数或具有相同签名的其他可调用对象存储在统一的类型中的方式,从而增加了代码的灵活性和通用性。在多线程编程中,我们经常需要将任务(函数或函数对象)传递给线程来异步执行。在这种情况下,理解std::function
如何被传递(通过值传递或引用传递)以及这两种传递方式的影响,就显得尤为关键。
在函数字段成员赋值时,用了std::move()形式。
TxtIO &SetIMUProcessFunc(IMUProcessFuncType imu_proc) {
imu_proc_ = std::move(imu_proc);
return *this;
}
我们补充一些std::move()的资料。 右值引用的事。
4.5 std::move()
这部分为补充阅读:主要参照《C++ Primer 第5版》第610页第16章《泛型算法》第16.2.6节《理解std::move》。
不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。
move本质上可以接受任何类型的实参,它是一个函数模板。
读到这里,不补充左值引用和右值引用不行了。我自己都不懂了。
1)内存管理
参照《C++ Primer 第5版》第470页第13章《拷贝控制》第13.6节《对象移动》
在重新分配内存的过程中移动而不是拷贝元素
C++11新标准的一个最主要的特性是可以移动而非拷贝对象的能力。很多情况下都会发生对象拷贝,在其中某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
2)背景:
vector类将其元素保存在连续内存中。为了获得可接受的性能,vector预先分配足够的内存来保存可能需要的更多元素(参见书9.4节,第317页)。vector的每个添加元素的成员函数会检查是否有空间容纳更多的元素。如果有,成员函数会在下一个可用位置构造一个对象。如果没有可用空间,vector就会重新分配空间:它获得新的空间,将已有元素移动到新空间中,释放旧空间,并添加新元素。
当我们拷贝或赋值时,必须分配独立的内存,并从原vector对象拷贝元素至新对象。特别是我们在执行reallocate时,需要把所有的元素重新搬移到新的内存空间中去,并销毁原来的内存空间。如果是StrVec的对象,需要挨个把每个元素中的string删掉。拷贝这些指向性元素数据时,拷贝销毁都是多余的。重新分配内存空间时,如果我们能避免分配和释放string的额外开销,StrVec的性能会好得多。
3)对象移动
- 在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。
- 使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝,但可以移动。
在C++11新标准之后,容器可以保存不可拷贝的类型,只要它们能被移动即可。
4)右值引用
为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。
- 所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。
- 右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。
- 我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
一般而言:一个左值表达式表示的是一个对象的身份(容器、名称、空间),而一个右值表达式表示的是对象的值(内容、容器内容物)。
参照《C++ Primer 第5版》第121页第4章《表达式》第4.1.1节《左值和右值》
简单归纳:
- 当一个对象被用作右值的时候,用的是对象的值(内容);
- 当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
一个重要原则:
- 在需要右值的地方可以用左值来代替,
- 但是不能把右值当成左值(也就是位置)使用。
- 当一个左值被当成右值使用时,实际使用的是他的内容(值)。
左值可以当右值用,但是右值不可以当左值用。
例如:
int a = 10;
int b = a;
10 = a; //不可以。
一些案例情况:
- 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
- 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。
牢记这个取地址符。
取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
参照《C++ Primer 第5版》第121页第13章《对象移动》第13.6.1节《右值引用》
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。对于常规左值引用,我们不能将其绑定到 要求转换的表达式、 字面常量或是 返回右值的表达式。
右值引用有着完全相反的绑定特性:我们可以将一个 右值引用绑定到这类表达式上, 但不能将一个右值引用直接绑定到一个左值上。
int i = 42;
int &r = i; //正确:上引用i
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; //错误:i*42是一个右值
const int &r3 = i * 42; //正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; //正确:将rr2绑定到乘法结果上
- 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
5)左值持久;右值短暂
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知
- 所引用的对象将要被销毁
- 该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。
6)变量是左值
变量表达式都是左值:不能将一个右值引用绑定到一个右值引用类型的变量上。
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是左值!
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
7)标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。move函数使用了我们将在16.2.6节(第610页)中描述的机制来返回给定对象的右值引用。
int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是左值!
int &&rr3 = std::move(rr1); // ok
move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,
- 调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
- 在调用 move 之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
我的理解是:被move之后,这个对象只剩个名字(空壳公司),这个名字牌子可以装在别的内存空间上,也就是被赋予新的内容(值),或者可以把这个牌子(名字)扔了。愿意有人(数据)用就用,不用就扔了。
src/common/io_utils.h
//
// Created by xiang on 2021/7/20.
//
#ifndef SLAM_IN_AUTO_DRIVING_IO_UTILS_H
#define SLAM_IN_AUTO_DRIVING_IO_UTILS_H
#include <rosbag/bag.h>
#include <rosbag/view.h>
#include <sensor_msgs/LaserScan.h>
#include <fstream>
#include <functional> // for std::function
#include <utility> // for std::move()
#include "common/dataset_type.h"
#include "common/global_flags.h"
#include "common/gnss.h"
#include "common/imu.h"
#include "common/lidar_utils.h"
#include "common/math_utils.h"
#include "common/message_def.h"
#include "common/odom.h"
#include "livox_ros_driver/CustomMsg.h"
#include "tools/pointcloud_convert/velodyne_convertor.h"
#include "ch3/utm_convert.h"
namespace sad {
/**
* 读取本书提供的数据文本文件,并调用回调函数
* 数据文本文件主要提供IMU/Odom/GNSS读数
*/
class TxtIO {
public:
TxtIO(const std::string &file_path) : fin(file_path) {}
/// 定义回调函数
using IMUProcessFuncType = std::function<void(const IMU &)>;
using OdomProcessFuncType = std::function<void(const Odom &)>;
using GNSSProcessFuncType = std::function<void(const GNSS &)>;
TxtIO &SetIMUProcessFunc(IMUProcessFuncType imu_proc) {
imu_proc_ = std::move(imu_proc);
return *this;
}
TxtIO &SetOdomProcessFunc(OdomProcessFuncType odom_proc) {
odom_proc_ = std::move(odom_proc);
return *this;
}
TxtIO &SetGNSSProcessFunc(GNSSProcessFuncType gnss_proc) {
gnss_proc_ = std::move(gnss_proc);
return *this;
}
// 遍历文件内容,调用回调函数
void Go();
private:
std::ifstream fin;
IMUProcessFuncType imu_proc_;
OdomProcessFuncType odom_proc_;
GNSSProcessFuncType gnss_proc_;
};
我特别留意了一下这个三个属性值:为啥要用std::move()的形式来赋值。
前面提到过一句:
不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。
为啥要用右值引用?因为考虑的是移动而非拷贝。
上面也提到过,我们考虑移动而非拷贝的情况第二种:
使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝,但可以移动。
按照我个人的理解:这个TxtIO类中,因为使用了IO缓冲,所以这个函数对象不能被拷贝,只能被移动。此外,针对IMU的处理程序,个人感觉应该单独启动一个线程来处理。总结两点:
- 在这个绑定的函数对象中,因为要调用文件读写IO缓冲,所以这种不能拷贝的对象,只能用移动的方式。
- 完事这个imu的处理方式,后期应该单独启动一个线程,所以也应该独立出来。避免值传递拷贝导致的资源冲突。
被move操作之后,就变成了前面提到的那种情况:既避免了资源争夺,又避免了无法拷贝。
- 调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
- 在调用 move 之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
认知有限,如果有同学能给解释的更清楚,求您留言告知我一声。