从视觉SLAM方案中理解C++的语法条款(二)

 我的上一篇文章:从视觉SLAM方案中理解C++的语法条款(一)-CSDN博客

可能有助于理解这篇文章。

本文大量使用栈区堆区的知识,相关知识有较大进步空间的读者可以参考以下文章:

(简介版)C++ 学习(九)内存分区(代码区、全局区、栈区、堆区)_c++运行时 堆,栈,代码,全局区存储地址-CSDN博客

(深入版) 什么是代码区、常量区、静态区(全局区)、堆区、栈区?-CSDN博客

读者如果在阅读时有其他困难,请打在评论区,我会在文章中补上相关内容,这也是我进一步学习的动力。 

目录

🌈条款11:在operator=中处理自我赋值

🌈条款12:复制对象时勿忘其每一个部分

🥝条款13:以对象管理资源

🌈条款14:在资源管理类中小心coping行为

🌈条款15:在资源管理类中提供对初始资源的访问

🌈条款16:成功使用new和delete时要采取相同形式

🌈条款17:以独立语句将newed对象置入智能指针


💫前言:

       本文以开源ORB_SLAM2为例,分析Scott Meyers大佬的Effective C++,同时还用到了视觉slam十四讲高博的开源代码。使用这些素材其实是因为我最近也在学习SLAM,发现自己对SLAM代码中的许多语法不明所以。希望这个系列能给大家带来收获,我们一起进步!

💫资料链接:

  • 链接: https://pan.baidu.com/s/16eN1vPq3Et1LEpXWlwColQ?pwd=hzfn 提取码: hzfn 复制这段内容后打开百度网盘手机App,操作更方便哦
  • 内含:ORB_SLAM2代码,kitti数据集 2个G, Effective C++ PDF, 视觉SLAM十四讲从理论到实践  第2版.pdf 

条款11-17可以说是多C++11中的智能指针和多线程编程原理的深入探讨,但是请注意本书是2011年以前很就就出版的,所以智能指针和多线程编程很可能就是照着本书的原理设计的。所以想要彻解slam方案中用到的智能指针和多线程编程的原理,条款11-17是值得一读的。

考虑到slam中会吞吐大量的图像数据,和从图像中提取出的信息,内存泄漏风险很高,这些条款中提到的管理资源的思想,是很有价值的。


🌈条款11:在operator=中处理自我赋值

为了避免纸上谈兵,下面搬来书中的两个类作为例子:

class Bitmap {};  // 我知道这是图片格式名,但不用在意这个名字
class Widget {
    // 这里的Widget不是指qt里的Widget,不用在意类名
    private:
    Bitmap* pb;  // Pointer of Bitmap 指向一个从堆区heap分配而得的对象 
    void swap(Widget* rhs);  // 有自我赋值安全又有异常安全的等号重载方案2
};

有自我赋值安全但没有异常安全的等号重载(异常安全是什么在代码示例中有解释):

/// @brief 有自我赋值安全但没有异常安全的等号重载
Widget& Widget::operator=(const Widget& rhs)
{
    if (this == &rhs) return *this;  // 证同测试identity test,防止自我赋值

    delete pb;  // 停止使用当前的bitmap
    pb = new Bitmap(*rhs.pb);  // 使用rhs'bitmap的副本
    /*不具备异常安全性:
    一旦内存不足或者Bitmap的copy构造函数抛出异常,
    pb将指向一块被删除的Bitmap,这样的指针是有害的,
    既不能安全地删除pb,也不能安全地读取pb
    */
    return *this;
}

有自我赋值安全又有异常安全的等号重载方案1:

/// @brief 有自我赋值安全又有异常安全的等号重载方案1
Widget& Widget::operator=(const Widget& rhs)
{
    Bitmap* pOrig = pb;  // 记住原先的pb
    pb = new Bitmap(*rhs.pb);  // 使用rhs'bitmap的副本
    delete pOrig;  // 停止使用原先的bitmap
    return *this;
}

有自我赋值安全又有异常安全的等号重载方案2:

void Widget::swap(Widget& rhs)  // 有自我赋值安全又有异常安全的等号重载方案2
{
    std::swap(this->pb, rhs.pb);
    // 如果Widget类管理了不止一个资源,这里可以继续添加swap
}

/// @brief 有自我赋值安全又有异常安全的等号重载方案2
Widget& Widget::operator=(const Widget& rhs)
{
    // copy-and-swap 拷贝再交换
    Widget temp(rhs);  // 拷贝
    swap(temp);  // 交换
    return *this;
}

这里大量使用引用和指针(),就不得不提一下引用和指针的差异。同样储存值的地址,

引用的,值的解析是自动的、隐式的。上面代码中,体现在void swap函数中,rhs左边没有星号就解析其值,以及右边是句点。

指针的,值的解析是手动的、显式的。体现在指针变量的左边有星号才解析其值。(以及上面代码中,在void swap函数中,this右边是箭头。)

或者说,相较于引用,指针对于“是地址还是值的问题”更为较真。


🌈条款12:复制对象时勿忘其每一个部分

先准备一下示范的类:

// 高博的代码里,类内成员习惯以下划线做后缀,作为区分,构造函数的形参没有
class Base {
public:
    Base(const Base& rhs)
public:
    double baseMember_;
};
class Derived: public Base {
public:
    double member1_;
    double member2_;
    double member3_;
};

 然后是我们的copy构造函数:

Base::Base(const Base& rhs)
    : baseMember_(rhs.baseMember_)
    {
        /*可以在这里运行写入日志信息的代码*/
    }

Derived::Derived(const Derived& rhs)
    : Base(rhs),  // 初始化子类的时候别忘了父类成员的初始化
      member1_(rhs.member1_),
      member2_(rhs.member2_),
      member3_(rhs.member3_),
    {
        /*可以在这里运行写入日志信息的代码*/
    }

🥝条款13:以对象管理资源

资源取得时机便是初始化时机 Resource Acquisition Is Initialization (RAII,记住这个RAII,接下来会考)

在对象析构的时候释放它所管理的堆区资源,利用析构函数确保资源被释放,如果释放动作抛出异常导致在析构中出错,见条款08

为了实现上述目标,书中推荐使用智能指针std::shared_ptr与std::weak_ptr管理堆区资源

在测试智能指针之前,我们先做一点准备:

// 1-设计工厂函数,返回基类指针,指向派生类的动态分配对象
#include <memory>
#include <iostream>

using namespace std;

class TimeKeeper {  // 这是一个抽象的钟
public:
    TimeKeeper();
    TimeKeeper(TimeKeeper& tkr);
    virtual ~TimeKeeper();  // 为了实现工厂函数,需要写一个virtual的析构函数,见条款07
    static void getCount();
    static int _count;
};
int TimeKeeper::_count = 0;

TimeKeeper::TimeKeeper(){
    ++_count;
}

TimeKeeper::TimeKeeper(TimeKeeper& tkr){
    ++_count;
}

// 析构函数必须类外实现,否则undefined reference to `vtable for TimeKeeper'
TimeKeeper::~TimeKeeper(){
    --_count;
}

void TimeKeeper::getCount(){
    cout << _count << endl;
}

class AtomicClock: public TimeKeeper{

};
class WaterClock: public TimeKeeper{

};
class WristClock: public TimeKeeper{

};

这里介绍一下工厂函数:

// 工厂函数:如果只想知道时间而不关心如何计算等细节
TimeKeeper* getTimeKeeper(){
    TimeKeeper* ptkr = new AtomicClock();
    return ptkr;
}

大家可能有疑问,既然只使用基类的功能,那么直接生成一个基类对象不就完了吗,为什么还要搞一个工厂函数?那是因为派生类可能重写了基类的虚函数,基类对象用不了改写过的函数。

下面的测试验证了我的观点

准备阶段:

class TimeKeeper {
public:
    TimeKeeper();
    TimeKeeper(TimeKeeper& tkr);
    virtual ~TimeKeeper();

    virtual void getMaterial(){
        cout << "wood" << endl;
    }

};

// 重写getMaterial
class AtomicClock: public TimeKeeper{
public:
    virtual void getMaterial() override {
        cout << "iron" << endl;
    }
};
class WaterClock: public TimeKeeper {};
class WristClock: public TimeKeeper {};

// 工厂函数:如果只想知道时间而不关心如何计算等细节S
TimeKeeper* getTimeKeeper(){
    TimeKeeper* ptkr = new AtomicClock();
    return ptkr;
}

 测试结果与结论:

int main()
{
    AtomicClock ac;
    ac.getMaterial();  // 打印iron
    ac.TimeKeeper::getMaterial();  // 打印wood

    getTimeKeeper()->getMaterial();  // 打印iron 
    getTimeKeeper()->TimeKeeper::getMaterial(); // 打印wood 
    // 由此可知:派生类的基类部分既可以使用派生类重载过的函数,也可以使用基类原来的,但是基类对象就只有基类的了
}

接下来直接上智能指针的用法:

智能指针初始化: std::shared_ptr<T> Ptr(new T  或者   指向它的指针);  

当最后一个指向new T堆区资源的智能指针析构,也就是没人用它了,该堆区资源将被释放

// 如果要初始化一个null shared ptr即一个空的智能指针
std::shared_ptr<T> ptr(static_cast<T>(0)[,自定义删除器]); 

// 或者这样,高博就是这么做的
typedef std::shared_ptr<T> Ptr;

Ptr ptr = nullptr;

 实战一下:

// 2-使用智能指针,释放堆区内存
// shared_ptr不能指向数组
void f(){
    std::shared_ptr<TimeKeeper> pTkr1(getTimeKeeper());
    cout <<TimeKeeper::_count << endl;  // 打印1
    std::shared_ptr<TimeKeeper> pTkr2(getTimeKeeper());  // 指向同一个资源
    cout << TimeKeeper::_count << endl;  // 打印2
    // f()调用结束后,局部变量pTkr销毁,AtomicClock堆区内存释放
}

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    for(int i: arr){
        f();
        cout << "<" << i << ">: " << TimeKeeper::_count << endl;
    }
    getTimeKeeper()->getCount();  // 打印1
    getTimeKeeper()->getCount();  // 打印2
    getTimeKeeper()->getCount();  // 打印3
}

智能指针在slam方案中的使用,不能说很多,只能说铺天盖地,这也就是为什么条款13是用有营养的猕猴桃开头的

// 后端类
class Backend {
    typedef std::shared_ptr<Backend> Ptr;
};
// 相机类
class Camera {
    typedef std::shared_ptr<Camera> Ptr;
};
// 配置类
class Config {
    static std::shared_ptr<Config> config_;
};
// 数据集类
class Dataset {
    typedef std::shared_ptr<Dataset> Ptr;
};
// 特征类
struct Feature {
    typedef std::shared_ptr<Feature> Ptr;v
    std::weak_ptr<Frame> frame_;         // 持有该feature的frame
    std::weak_ptr<MapPoint> map_point_;  // 关联地图点
};
// 帧类
struct Frame {
    typedef std::shared_ptr<Frame> Ptr;
    // extracted features in left image
    std::vector<std::shared_ptr<Feature>> features_left_;
    // corresponding features in right image, set to nullptr if no corresponding
    std::vector<std::shared_ptr<Feature>> features_right_;
};
// 地图类
class Map {
    typedef std::shared_ptr<Map> Ptr;
};
// 路标类
struct MapPoint {
    typedef std::shared_ptr<MapPoint> Ptr;
    std::list<std::weak_ptr<Feature>> observations_;
};
// 可视化类
class Viewer {
    typedef std::shared_ptr<Viewer> Ptr;
};
// VO对外接口类
class VisualOdometry {
    typedef std::shared_ptr<VisualOdometry> Ptr;
};
    

例如这个Frame对象,在Frame的成员函数Frame::Ptr Frame::CreateFrame()中,从堆区诞生并被编号,一诞生就被装进智能指针,这个智能指针先是在Dataset类的

Frame::Ptr Frame::CreateFrame() {
    static long factory_id = 0;
    Frame::Ptr new_frame(new Frame);
    new_frame->id_ = factory_id++;
    return new_frame;
}

Frame::Ptr Dataset::NextFrame()中装填opencv处理过的图片,然后进入

Frame::Ptr Dataset::NextFrame() {
    boost::format fmt("%s/image_%d/%06d.png");
    cv::Mat image_left, image_right;
    // read images
    image_left =
        cv::imread((fmt % dataset_path_ % 0 % current_image_index_).str(),
                   cv::IMREAD_GRAYSCALE);
    image_right =
        cv::imread((fmt % dataset_path_ % 1 % current_image_index_).str(),
                   cv::IMREAD_GRAYSCALE);

    if (image_left.data == nullptr || image_right.data == nullptr) {
        LOG(WARNING) << "cannot find images at index " << current_image_index_;
        return nullptr;
    }

    cv::Mat image_left_resized, image_right_resized;
    cv::resize(image_left, image_left_resized, cv::Size(), 0.5, 0.5,
               cv::INTER_NEAREST);
    cv::resize(image_right, image_right_resized, cv::Size(), 0.5, 0.5,
               cv::INTER_NEAREST);

    auto new_frame = Frame::CreateFrame();
    new_frame->left_img_ = image_left_resized;
    new_frame->right_img_ = image_right_resized;
    current_image_index_++;
    return new_frame;
}

bool VisualOdometry::Step()并在其中被bool Frontend::AddFrame(type)当作参数,参数采用值传递pass-by-value,用于前端加载下一帧,用于前端的ORB特征提取

bool VisualOdometry::Step() {
    Frame::Ptr new_frame = dataset_->NextFrame();
    if (new_frame == nullptr) return false;

    auto t1 = std::chrono::steady_clock::now();
    bool success = frontend_->AddFrame(new_frame);
    auto t2 = std::chrono::steady_clock::now();
    auto time_used =
        std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
    LOG(INFO) << "VO cost time: " << time_used.count() << " seconds.";
    return success;
}

这份Frame堆区资源,一直(有各个函数中)作为局部变量的智能指针指着,函数运行结束,指针析构,但是每次都有另一个指针接收它,薪火相传。(换句话说,不同的栈区指针指向同一份堆区资源。这使得各个函数得以引用这份资源,而又不会复制资源,造成内存的浪费。想必这份资源是很费内存的,比如一帧图像)

bool Frontend::AddFrame(myslam::Frame::Ptr frame) {
    current_frame_ = frame;

    switch (status_) {
        case FrontendStatus::INITING:
            StereoInit();
            break;
        case FrontendStatus::TRACKING_GOOD:
        case FrontendStatus::TRACKING_BAD:
            Track();
            break;
        case FrontendStatus::LOST:
            Reset();
            break;
    }

    last_frame_ = current_frame_;
    return true;
}

直到bool VisualOdometry::Step()和bool Frontend::AddFrame(type), 终于没有后继的指针了,于是随着最后一个智能指针析构,这份Frame堆区资源释放,走完了它短暂而不凡的一生。(堂吉柯德附体)


🌈条款14:在资源管理类中小心coping行为

(如果要禁止对象copy行为:见条款06)

class Uncopyable {
private:
    Uncopyable(Uncopyable &);
    Uncopyable& operator=(Uncopyable &);
};  // 然后继承这个类,就能禁止复制行为了

#include <memory>  // for std::shared_ptr
#include <thread>
#include <mutex>

void lock(std::mutex* pm) {
    pm->lock();
}

void unlock(std::mutex* pm) {
    pm->unlock();
}

class Lock {
public:
    explicit Lock(std::mutex* pm)    // 以某个Mutex初始化shared_ptr
        : mutexPtr(pm, unlock)      // 并以unlock函数为删除器,这样就不用担心忘记给互斥器解锁了
        {
            lock(mutexPtr.get());  // get函数见条款15
        }
private:
    std::shared_ptr<std::mutex> mutexPtr;
};

当某个资源可能被多个线程访问时,要加互斥器,就是你这个线程访问该资源的时候,排斥其他的线程访问该资源。

事实上slam方案中早已使用c++11中的std::unique_lock类来作为我们上面写的Lock类,因为它的private下面的内容不是智能指针:

直接上用法:

std::unique_lock<std::mutex> lck(mutex对象);  

                        or

std::lock_guard<std::mutex> lck(mutex对象);  


进一步了解C++11多线程编程的用法,请移步:https://www.cnblogs.com/zizbee/p/13520823.html

不知到你有没有意识到,我刚刚的说法其实是错误的,unique_lock类不能替代我们写的Lock类,因为它们被设计出来的目的不同,它没有在类的内部使用智能指针来储存原始资源(见条款15)。

(实际上我也还没有看懂Scott Meyers的Lock类究竟是干什么的,希望在我想出答案前能有大佬解疑)

例如高博的stereo slam方案中的代码:

struct Frame {
   public:
    typedef std::shared_ptr<Frame> Ptr;
    SE3 pose_;                       // Tcw 形式Pose
    std::mutex pose_mutex_;          // Pose数据锁

   public:  // data members

    // set and get pose, thread safe
    SE3 Pose() {  
        // lck对象实例化,互斥锁自动lock。实例化即上锁
        std::unique_lock<std::mutex> lck(pose_mutex_);  // 这里的unique_lock可以用lock_guard代替
        return pose_;  // 多个线程抢着访问pose_
    }  // 栈区对象lck析构,互斥锁自动unlock

    void SetPose(const SE3 &pose) {
        std::unique_lock<std::mutex> lck(pose_mutex_);
        pose_ = pose;  // 多个线程抢着修改pose_
    }
    
    // 下面是拓展内容,也挺关键,有兴趣可以自行阅读
    /// 工厂构建模式,分配id 
    static std::shared_ptr<Frame> CreateFrame();  // 静态成员函数的局部变量并不会在该函数运行结束后被释放
};  // 部分代码,不全

当然我对多线程编程这块还很薄弱,建议大家这方面参考其他大佬的博客,不足之处希望大家多多指正。 


🌈条款15:在资源管理类中提供对初始资源的访问

你成功地把各种资源都装到了像shared_ptr这样的资源管理类RAII class中,但是,其他人写了些这样的函数,它们直接以资源为参数而不是装了资源的智能指针,当然这是很常见

现在你要做的就是通过显式转换或隐式转换,把一个RAII class对象转换为其所内含之原始资源

  • 显式转换函数:get是shared_ptr的成员函数,返回原始资源的副本(推荐)
  • 隐式转换函数:operator 内涵原始资源的类名() const {return 那个内含原始资源;}

 下面是两种转换方式的实现:

// 原始资源
class FontHandle {};
// RAII类
class Font {
friend void releaseFont(FontHandle fh);
public:
    explicit Font(FontHandle fh)  // 构造函数是explicit函数,拒绝隐式转换
        : f(fh)
        {}
    ~Font() {releaseFont(f);}

    FontHandle get() const {return f;}      // 显式转换函数:到处都是get,但是安全
    operator FontHandle() const {return f;}  // 隐式转换函数:没get也能提取原始数据了
    // 缺点:允许这样的写法存在:Font f1(getFont()); FontHandle f2 = f1;
    // 这当然是客户写错了,但是f1隐式地转换为FontHandle对象,写错了还不会报错
private:
    FontHandle f;
};

// 用于Font内涵原始资源初始化,Font的初始话当然也可以直接用static FontHandle对象完成
FontHandle getFont(){
    static FontHandle fh;
    return fh;
}  

void releaseFont(FontHandle fh){
    delete &fh;
} //释放资源

用这个函数来测试:

void changeFont(FontHandle f);

测试结果:两种转换方式均通过:

void test()
{
    Font f(getFont());
    changeFont(f.get());  // 显式转换
    changeFont(f);       // 隐式转换
}

 说白了隐式转换就图一个省事好看,大家还是老老实实地用智能指针的get做显式转换吧,除非你不用智能指针另起炉灶。

🌈条款16:成功使用new和delete时要采取相同形式

你觉得下面的有错吗:

string* pusr = new string[100];  // 在堆区创建一个对象数组,并用一个指针指向它的开头

delete pusr;  // 手动释放指针指向的内存

没错!一百个string对象它只删除了第一个。所以同样是堆区的:

删除单一对象:delete 单一对象;

删除数组对象:delete [ ] 数组对象;

 总之,new的适合有[ ],delete的时候也要有[ ],这就是“采取相同形式”。

🌈条款17:以独立语句将newed对象置入智能指针

用法就是:

std::shared_ptr<T> Ptr(new T);  // 可以参考更具体的条款13

然后使用Ptr

  • 46
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值