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)指的是程序在运行时,所出现的不可预测、不符合语言规范、且未定义其结果的行为。在内存方面的使用中,一些常见的未定义行为包括:
- 访问未初始化的内存:当我们对未初始化的内存进行读写操作时,其结果是未定义的。例如,以下代码中的指针p未初始化,对其进行解引用操作是未定义行为:
int *p;
std::cout<<*p<<std::endl; //未定义行为
- 访问越界的内存:C++中,数组越界访问是未定义行为。当我们访问数组或指针指向的内存范围之外时,程序行为是未定义的。例如,以下代码中,当我们访问数组a的第6个元素时就已经越界:
int a[5] = {1,2,3,4,5};
std::cout<<a[5]<<std::endl; //未定义行为
- 使用已经释放的内存:对已经释放的内存进行读写操作也是未定义行为。例如,以下代码中,当我们调用delete释放内存后,再去读写其指向的内存是未定义行为
int *p = new int;
delete p;
*p = 10; //未定义行为
- 多次释放同一块内存:对同一块内存多次使用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的时候,可以持续这样对比着来做。