Rust实战(5):Rust 如何防御 C++ vector 容器的迭代器边界判断问题

本文通过对比C++和Rust的例子,探讨了C++中迭代器未定义行为可能导致的问题,以及Rust如何通过所有权、生命周期等机制来避免这类问题,强调了Rust在内存安全性方面的优势。
摘要由CSDN通过智能技术生成

C++ 例子代码

构造一个C++迭代器带来的隐藏BUG的例子代码。头文件如下func2.h:

#ifndef TEST_H
#define TEST_H

#include <vector>
#include <algorithm>
#include <cmath>

struct Boo {
    double position;
    double c0;
    double c1;
    double c2;
    double c3;
    double c4;
    double c5;
};


struct Container {
    std::vector<Boo> boo_list;
   

    void dump(){
        printf("[");
        for(const Boo& p: boo_list){
            printf("%f,",p.position);
        }
        printf("]\n");
    }
};

class Algo {
public:
    Algo();
    void find(Container &container, double value);
};

#endif // TEST_H

对应的实现代码构造如下:

#include "func2.h"

Algo::Algo(){}

void Algo::find(Container &container,double value) {
    const std::vector<Boo> &boo_list = container.boo_list;

    auto cmp = [](const double &value, const Boo &element) { 
        bool ret = value <= element.position; 
        printf("[%d] value:%f, element.position:%f\n", ret, value, element.position);
        return ret;
    };

    auto boo_index_it = std::upper_bound(boo_list.begin(),boo_list.end(), value, cmp);
    auto boo = *boo_index_it;
    printf("value:%f, boo: %f\n", value, boo.position);
}

现在,编写一个测试代码:

#include <random>
#include <string>
#include <iostream>

#include "func2.h"

int main(){
    // init data
    Algo algo;
    Container container;
    for(int i=0;i<10;i++){
        container.boo_list.push_back(Boo{
            double(i),
            double(i),
            double(i),
            double(i),
            double(i),
            double(i),
            double(i)
        });
    }
    container.dump();

    // case2.1 normal
    algo.find(container,3.0);

    // case2.2 bug
    algo.find(container,20.0);

    // case2.3 bug
    container.boo_list.clear();
    algo.find(container,3.0);

    return 0;
}

上述代码是构造出来的,问题出在函数 find 函数里,这个函数里通过 std::upper_bound 方法获取Container成员变量 boo_list 里第一个大于目标值 value的元素。这里暂时忽略find函数返回值是void的问题,应该设计更好的返回值。直接看find函数末尾的实现部分的问题。

但是 std::upper_bound 返回的迭代器,如果没找到就返回 boo_list.end()。但是对 boo_list.end()解引用是一个C++的未定义行为,undefined behavior, 也就是UB。如果实际的系统运行的时候,程序没有崩溃,但是继续往下走,错误的数据导致什么都可能发生。

Rust 版本的实现

尝试用Rust实现下,可以更好地理解在 Rust 语言里这类问题是如何自然被抑制或者消失的。

同上一节一样,程序目录结构如下:

├── Cargo.toml
├── main
│   ├── Cargo.toml
│   └── src
│       └── main.rs
├── some
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs

some/src/下增加一个文件boo_algo.rs,代码如下:

use std::cmp::Ordering;

pub struct Boo {
    pub position: f32,
    pub c0: f32,
    pub c1: f32,
    pub c2: f32,
    pub c3: f32,
    pub c4: f32,
    pub c5: f32,
}

pub struct Container {
    boo_list: Vec<Boo>,
}

impl Container {
    pub fn new(boo_list: Vec<Boo>) -> Self {
        Self {
            boo_list
        }
    }

    pub fn dump(&self) {
        print!("[");
        for p in &self.boo_list {
            print!("{},", p.position);
        }
        print!("]\n");
    }

    pub fn clear(&mut self) {
        self.boo_list.clear();
    }
}

pub struct Algo {}

impl Algo {
    pub fn new() -> Self {
        Self {}
    }

    pub fn find<'a>(
        &self,
        container: &'a Container,
        search_value: f32,
    ) -> Option<&'a Boo> {
        let insert_index = match container.boo_list.binary_search_by(|element| {
            if element.position == search_value {
                Ordering::Equal
            } else if element.position < search_value {
                Ordering::Less
            } else {
                Ordering::Greater
            }
        }) {
            Ok(index) => index + 1,
            Err(index) => index,
        };
        container.boo_list.get(insert_index)
    }
}

其中关键比对的代码实现是find的实现代码:

pub fn find<'a>(
        &self,
        container: &'a Container,
        search_value: f32,
    ) -> Option<&'a Boo> {
        let insert_index = match container.boo_list.binary_search_by(|element| {
            if element.position == search_value {
                Ordering::Equal
            } else if element.position < search_value {
                Ordering::Less
            } else {
                Ordering::Greater
            }
        }) {
            Ok(index) => index + 1,
            Err(index) => index,
        };
        container.boo_list.get(insert_index)
    }

这里Rust一上来就就会需要掌握生命周期标记的语法,关于生命周期的语法参考 https://vector.blog.csdn.net/article/details/119341262 里的一些解释。

备注:Rust函数最后一个语句不加分号,它的返回值就是函数的返回值。不需要显式 return。

Rust这里等价于C++的std::uppper_bound方法是Vec::binary_search_by方法。这个方法返回的是一个Option<&T>,因此传递上来,find方法也就自然地返回Option<&'a Boo>。我们看看这样带来的好处。

测试用例代码,在lib.rs里添加:

mod boo_algo;

pub use boo_algo::*;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_iterator() {
        let mut boo_list = Vec::new();
        for i in 0..10 {
            let d = i as f32;
            boo_list.push(Boo {
                position: d,
                c0: d,
                c1: d,
                c2: d,
                c3: d,
                c4: d,
                c5: d,
            })
        }

        let mut container = Container::new(boo_list, 100);
        let algo = Algo::new();

        let ret = algo.find(&container, 3.0);
        assert!(ret.is_some() && ret.unwrap().position == 4.0);

        let ret = algo.find(&container, 20.0);
        assert!(ret.is_none());

        container.clear();
        let ret = algo.find(&container, 0.0);
        assert!(ret.is_none());
    }
}

可以看到,如果没找到,返回的ret是一个Option。代码写的时候如果要获取里面的值,就需要调用ret.unwrap()方法,而如果是一个None,调用unwrap的时候就会Panic,因此程序员需要用 match 语法去处理:

match ret {
    Some(element)=>println!("{}",element.position),
    None=>println!("not found")
}

或者用is_some(), is_none()方法判断下。对比一下,C++里也应该在对迭代器做解引用之前判断下迭代器是否等于end()迭代器。例如:

auto boo_index_it = std::upper_bound(boo_list.begin(),boo_list.end(), value, cmp);
if(boo_index_it!=boo_list.end()){
    auto boo = *boo_index_it;
    printf("value:%f, boo: %f\n", value, boo.position);
}else{
    printf("value:%f, not found\n", value);
}

差异在于,当C++代码里不判断的时候,它是一个未定义行为,程序可能不崩溃,这个就很尴尬了,有时候可能造成无法估量的损失。程序应该在发生内存错误的时候,立刻崩溃退出,而不是隐藏问题继续运行。Rust的类型系统在这方面就做的比较好,这也是为什么Rust是一个更安全的语言的原因。

什么是C++的未定义行为,以内存方面的使用举例

在C++中,未定义行为(Undefined Behavior,简称UB)指的是程序在运行时,所出现的不可预测、不符合语言规范、且未定义其结果的行为。在内存方面的使用中,一些常见的未定义行为包括:

  1. 访问未初始化的内存:当我们对未初始化的内存进行读写操作时,其结果是未定义的。例如,以下代码中的指针p未初始化,对其进行解引用操作是未定义行为:
int *p;
std::cout<<*p<<std::endl; //未定义行为
  1. 访问越界的内存:C++中,数组越界访问是未定义行为。当我们访问数组或指针指向的内存范围之外时,程序行为是未定义的。例如,以下代码中,当我们访问数组a的第6个元素时就已经越界:
int a[5] = {1,2,3,4,5};
std::cout<<a[5]<<std::endl; //未定义行为
  1. 使用已经释放的内存:对已经释放的内存进行读写操作也是未定义行为。例如,以下代码中,当我们调用delete释放内存后,再去读写其指向的内存是未定义行为
int *p = new int;
delete p;
*p = 10; //未定义行为
  1. 多次释放同一块内存:对同一块内存多次使用delete释放也是未定义行为。例如,以下代码中,重复调用delete释放内存是未定义行为
int *p = new int;
delete p;
delete p; //未定义行为

在实际编写程序时,我们应该避免这些未定义行为,以保证程序的正确性和稳定性。

Rust如何解决C++的常见内存未定义行为

Rust在设计上就避免了C++的一些常见内存未定义行为,以提供更安全、更可靠的内存管理。具体而言,Rust对内存的使用和管理进行了以下改进:

所有权(Ownership)和借用(Borrowing):Rust引入了所有权和借用的概念,通过编译器在编译时检查所有权和借用的规则,避免了C++中的一些内存管理问题,如使用已释放的内存、使用空指针等。在Rust中,每个值都有一个所有者(Owner),当所有者超出作用域时,该值会自动被释放。

生命周期(Lifetime):Rust利用生命周期对值和借用的关系进行了更严格的控制。生命周期表示一个值在内存中的有效范围,编译器通过检查生命周期的有效性来避免访问已经被释放的内存。

模式匹配和变量绑定:Rust通过模式匹配和变量绑定等机制,避免了一些C++中的内存管理问题,如空指针等。例如,Rust中的Option类型用于处理可能为空的值,编译器在编译时会检查Option是否为空,避免了C++中的空指针问题。

借用检查器(Borrow checker):Rust的借用检查器是一个编译时的工具,用于检查借用的规则是否被遵守。通过借用检查器,Rust可以保证在编译时就避免了一些内存管理问题,如空指针、数据竞争等。

总之,Rust通过引入所有权和借用、生命周期、模式匹配和变量绑定、借用检查器等机制,避免了C++中的一些常见内存未定义行为,使得Rust更加安全、更可靠。

小结

分析C++和Rust解决问题的方式,即使继续使用C++,依然可以带来系统分析和诊断问题上的帮助,遇到badcase的时候,可以持续这样对比着来做。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值