高翔【自动驾驶与机器人中的SLAM技术】学习笔记(九)imu运动学;lambda表达式;bind;function;std::move()

news2024/9/21 14:42:06

一、IMU运动学

1、测量值:

常用六轴IMU是由陀螺仪(Gyroscope)加速度计(Acclerator)两部分组成。

  • 陀螺仪测量:角速度。
  • 加速度计:加速度。

安装要尽量保证IMU的安装位置在车辆中心。避免由IMU与载体系不重合引来的问题。

2、噪声模型

IMU噪声由两部分组成:

  • 测量噪声(Measurement Noise)。
  • 零偏(Bias)。
    • 哪怕车辆静止:加速度和角速度测量值均值也不为零,而是带有一定的偏移。
    • 偏移量是由IMU内部的机电测量装置导致的。
    • 建立数学模型,认为零偏也是系统的状态量。

 

测量值:数学模型为:理论值 + 零偏 + 噪声。 

  • 噪声:是一个高斯过程,协方差随着时间变得越来越大,IMU本身的测量值也随着采样时间变长而变得更加不准确。因此采样频率越高的IMU,其精度也会相对较高。
    • 角速度就是角度的导数,加速度又是速度的导数。所以 IMU 的测量噪声,也可以解释为角度的随机游走速度的随机游走。因此请不要看到随机游走这四个字,只想到零偏部分,而应该从整体层面看待问题。
  • 零偏随机游走/布朗运动/维纳过程。而零偏部分由布朗运动描述,呈现随机游走状态。表现在实际当中,则可以认为一个 IMU 的零偏会从某个初始值开始,随机地向附近做不规律的运动。运动的幅度越大,就称它的零偏越不稳定。所以质量好的IMU,零偏应该保持在初始值附近不动。
    • 随机游走实际上就是导数为高斯过程的随机过程,

3、思维教育

请读者注意,这里的高斯过程和布朗运动过程,都是IMU 测量数据的数学模型数学模型并不一定是和真实世界完全对应的。有时,数学模型是对真实世界的一种简化,便于后续的算法计算。读者应当理解这种思想。后面我们对许多系统进行线性近似保留各种一阶项,也是基于这种简化的思想。真实 IMU 的测量噪声和零偏受到非常多因素的影响,例如载体的震动、温度,IMU自身的受力、标定与安装误差,等等。把它们建模为两个随机过程,更多是为了方便状态估计算法的计算,不是完美、精确的建模方式。这种先简化、再补偿的思想,在现实中十分常见。

我想起一句名言:为啥研究各种线性假设:因为非线性的大家数学水平都达不到。所以都会把问题转为线性近似。直白点说:一阶泰勒公式展开。局部有效。不断逼近。

总结一句:差不多得了。

4、离散时间噪声模型:

这个离散的是咱们代码最感兴趣的。其中时间间隔\Delta t对应代码中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,还需要补充一些知识。

在研究代码中,发现这个读写文件用了一些语法糖。读写中bindfunction的拓展学习。以及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 之后,我们不能对移后源对象的值做任何假设。

我们可以销毁一个移后源对象,也可以赋予它新值但不能使用一个移后源对象的值

认知有限,如果有同学能给解释的更清楚,求您留言告知我一声。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2085393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SOA-BP海鸥优化BP神经网络实现数据预测Python实现

BP神经网络是一种多层前馈神经网络&#xff0c;它通过反向传播算法来训练网络中的权重和偏置&#xff0c;以最小化预测误差。然而&#xff0c;BP神经网络的性能很大程度上依赖于其初始参数的选择&#xff0c;这可能导致训练过程陷入局部最优解。海鸥优化算法因其探索和开发能力…

基于vue框架的残疾人就业帮扶平台97c5w(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,企业,招聘信息,类型,求职信息,投递信息,邀请信息,通知信息,帮扶政策,申请信息,意见反馈 开题报告内容 基于Vue框架的残疾人就业帮扶平台开题报告 一、选题背景与意义 随着社会的文明进步和经济的快速发展&#xff0c;残疾人群体…

flannel,etcd,docker

bridge容器 听有容器连接到桥就可以使用外网&#xff0c;使用nat让容器可以访问外网使用ipas指令查看桥&#xff0c;所有容器连接到此桥&#xff0c;ip地址都是172.17.0.0/16网段&#xff0c;桥是启动docker服务后出现&#xff0c;在centos使用bridge-utils安装 跨主机的容器…

第一次使用PyCharm写C++(失败)

前言&#xff1a; 由于我已经非常习惯使用PyCharm远程连接服务器了&#xff0c;我认为非常方便&#xff0c;所以希望C也能直接用Pycharm。于是尝试在PyCharm上部署C环境。 但是&#xff0c;我失败了。如果您知道问题所在&#xff0c;欢迎给我留言。我认为Pycharm并没有编译C/C…

Windows电脑微信可以登录发消息,但是网页打不开的解决方法:刷新DNS缓存

遇到的问题 今天实验室的电脑突然网页打不开&#xff0c;baidu上不了&#xff0c;chrome浏览器也上不了。但是ping baidu.com能够ping通&#xff0c;github pull也可以&#xff0c;网易云可以听歌。也就是说网络是通的&#xff0c;但是浏览器无法上网。 解决方法 我是通过 W…

直播商城APP开发指南:基于多商户商城系统源码的实现

对于开发者而言&#xff0c;构建一个功能完备、性能优越的直播商城APP已经成为当前技术领域的一个重要方向。本文将以多商户商城系统源码为基础&#xff0c;深入探讨如何高效开发一个直播商城APP。 一、多商户商城系统的核心概念 多商户商城系统是一种支持多个商家在同一平台…

深度解读SGM41511电源管理芯片I2C通讯协议REG09寄存器解释

REG09 是 SGM41511 的第十个寄存器&#xff0c;地址为 0x09。这是一个只读&#xff08;R&#xff09;寄存器&#xff0c;用于报告各种故障状态。上电复位值&#xff08;PORV&#xff09;为 xxxxxxxx&#xff0c;表示上电时的初始状态是不确定的。这个寄存器提供了充电器当前故障…

【Python机器学习】NLP词频背后的含义——从词频到主题得分

目录 TF-IDF向量及词形归并 主题向量 一个思想实验 一个主题评分算法 一个LDA分类器 LDiA TF-IDF向量&#xff08;词项频率—逆文档频率向量&#xff09;可以帮助我们估算词在文本块中的重要度&#xff0c;我们使用TF-IDF向量和矩阵可以表明每个词对于文档集合中的一小段…

【hot100篇-python刷题记录】【跳跃游戏】

R6-贪心算法 符合贪心的原因是&#xff1a; 我们要走到最后可以每次都选择尽可能远的来走&#xff0c;其次&#xff0c;能走到该步意味着该步以前都能到达。因此&#xff0c;局部最优解可以代表全局最优解。 class Solution:def canJump(self, nums: List[int]) -> bool:#最…

全志/RK安卓屏一体机:智能家居中控屏,支持鸿蒙国产化

智能家居中控屏 智能家居中控屏功能 智能中控屏作为全屋智能解决方案中的重要组成部分&#xff0c;融合智能开关面板、智能音箱、万能遥控、可视对讲、智能网关等设备&#xff0c;用一块屏承担起联动控制、人机交互、信息显示、个性化服务等功能。 智能中控屏是智能家居控制管…

cesium 轨迹线

在智慧城市项目中&#xff0c;轨迹线一般用来表现城市道路的流动效果。和cesium动态线篇效果类似&#xff0c;只是这里是通过设置高亮占比&#xff0c;而不是通过传入一张图片。 1. 自定义TrialFlowMaterialProperty类 1.1. 自定义 TrialFlowMaterialProperty 类 /** Descri…

MES管理系统助力印刷企业实现智能化工艺流程

在印刷这一古老而充满活力的行业中&#xff0c;科技的浪潮正以前所未有的速度重塑着每一个生产环节。随着制造业数字化转型的深入&#xff0c;引入MES管理系统&#xff0c;为印刷企业带来了从原材料入库到成品出库的全流程智能化变革&#xff0c;不仅提升了生产效率&#xff0c…

基于SpringBoot+Vue+MySQL的网上商城系统

系统背景 随着社会的快速发展&#xff0c;计算机的影响是全面且深入的。人们生活水平的不断提高&#xff0c;日常生活中人们对网上商城购物系统方面的要求也在不断提高&#xff0c;购物的人数更是不断增加&#xff0c;使得网上商城购物系统的开发成为必需而且紧迫的事情。网上商…

无人机图传通信模组,抗干扰、稳定传输,8公里图传模组原理

在蔚蓝的天空下&#xff0c;无人机如同自由的精灵&#xff0c;穿梭于云间&#xff0c;为我们捕捉那些令人心动的瞬间。而在这背后&#xff0c;有一项技术正悄然改变着航拍的世界&#xff0c;那就是无人机图传通信模组。今天&#xff0c;就让我们一起揭开它的神秘面纱&#xff0…

在蓝桥云课ROS中快速搭建Arduino开发环境

普通方式 一步步慢悠悠的搭建和讲解需要5-6分钟&#xff1a; 如何在蓝桥云课ROS中搭建Arduino开发环境 视频时间&#xff1a;6分40秒 高效方式 如何高效率在蓝桥云课ROS中搭建Arduino开发环境 视频时间&#xff1a;1分45秒 配置和上传程序到开发板 上传程序又称为下载程序h…

匠心服务·智启新程丨2025华清远见新品发布会在北京隆重举行

2024年8月23日&#xff0c;华清远见教育科技集团的“匠心服务智启新程”2025新品发布会在北京隆重举行。云集多位行业专家学者、知名企业代表&#xff0c;聚焦市场新动向&#xff0c;站在行业技术最前沿&#xff0c;以多元化视角深入解读当前行业面临的新机遇新挑战&#xff0c…

信创环境下怎么做好信创防泄露?

为实现信创环境下的数据防泄露和“一机两用”标准落地&#xff0c;依靠十几年的沙盒技术积累&#xff0c;研发出了支持统信UOS/麒麟等信创OS的沙箱&#xff0c;配合零信任SDP网关&#xff0c;提高数据安全&#xff0c;实现“一机两用”安全解决方案。 信创防泄漏的需求 信创环…

从每 N 行找出需要数据拼成一行

Excel某表格不规范&#xff0c;每两行6列对应规范表格的一行3列&#xff0c;分别是&#xff1a;第1行第1列或第2行第1列&#xff08;两者重复&#xff0c;取其一即可&#xff09;、第2行第2列、第1行第3列。 ABC1John DoeCompany A2John Doejohn.doeexample.com3Jane SmithCom…

盘点国内外好用的12款文件加密软件|2024年好用的加密软件有哪些

在当今信息化时代&#xff0c;企业和个人都面临着数据泄露的风险。为了保护敏感信息&#xff0c;文件加密软件已经成为不可或缺的工具。本文将盘点国内外好用的12款文件加密软件&#xff0c;并提供其在2024年的使用推荐&#xff0c;帮助用户更好地保护数据安全。 1. 安秉加密软…

【html+css 绚丽Loading】 000024 八方流转杖

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…