从内存安全角度审视 C++、Zig 和 Rust

一般来说,C++ 让程序员可以自由地做任何他们想做的事情。Circle C++ 提供了一个令人信服的解决方案,可以增强 C++ 的内存安全性,并为 C++ 提供出色的附加功能,这些功能可以轻松(最重要的是,可以逐步适应现有的 C++ 代码库)。

Rust 提供了出色的默认值和严格的内存安全性。它确实是一种出色的编程语言,但它的学习曲线也很难,因为借用检查器等概念对于 C++ 老手来说可能是一个陌生的概念。

Zig 提供了平衡、合理的内存安全性(使用分配器的不干预方法),总体而言,与 C++ 或 Rust 相比,它是一种更简单的语言。它还提供了与 C/C++ 代码库的出色集成,因此可以很容易地将其添加到现有代码库中。

如何比较内存安全性
根据 Sean 的工作,内存安全可以分为五类:

  1. 生命周期安全
  2. 类型安全 — 空类型
  3. 类型安全——联合多样性
  4. 线程安全
  5. 运行时安全

为了本文的目的,我将仅研究生命周期安全、类型安全(null 和 union)和运行时安全。

生命周期安全
 Circle C++ 代码:

feature on safety 
# include  <cstdio> 

int  main () safe { 
  int ^a; // 未初始化的借用
  { 
    int b = 10 ;
// b 处于活动状态
    a = ^b;
// 尽管 a 似乎是从 b 可变借用,但是在当前作用域内它未被使用,因此 a 未初始化。
   } 
  int c = *a; 
  unsafe printf (
"%d\n" , c) ; 
  *a = 11 ; 
  unsafe printf (
"%d\n" , *a) ; 
}

  • feature on safety: 这是 Circle C++ 编译器的一项功能,我们在其中添加了 Circle C++ 编译器的特定功能。
  • main 函数的安全限定符safe。 这让我们可以为特定的函数指定安全性。 我认为这是在现有大型 C++ 代码库中引入严格安全性的绝佳方法。 这将允许在现有项目中逐步添加严格的安全标准,而无需进行重大的完全重写,因为重写将导致编译器错误

Zig代码:

const std = @import("std");

pub fn main() !void {
    var a: *i32 = undefined;
    {
        var b: i32 = 10;
        a = &b;
        std.debug.print(
"address of b is: {s}\n", .{&b});
    }
    std.debug.print(
"a is pointing to: {s}\n", .{a});
    const c: i32 = a.*;
    std.debug.print(
"{d}\n", .{c});
    a.* = 11;
    std.debug.print(
"{d}\n", .{a.*});
}

结果表明代码中明显存在使用后释放漏洞,因为即使“b”超出范围后,“a”仍持有内存地址“b”。为了缓解这一问题,Zig 通常建议采取以下措施

  1. 不要使用指针作为堆栈分配的变量
  2. 使用allocator分配器在堆上分配内存

修改后:
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
    defer {
        _ = gpa.deinit();
    }
    const allocator = gpa.allocator();

    var a: *i32 = undefined;
    {
        var b = try allocator.create(i32);
        b.* = 10;
        a = b;
        std.debug.print(
"address of b is: {s}\n", .{&b});
    }
    std.debug.print(
"a is pointing to: {s}\n", .{a});
    const c: i32 = a.*;
    std.debug.print(
"{d}\n", .{c});
    a.* = 11;
    std.debug.print(
"{d}\n", .{a.*});
}


Rust代码:

fn main() {
    let a:i32;
    let a_ptr = &a;
}

上述代码无法编译。这是因为let c = *a_ptr。在 Rust 中,取消引用原始指针被视为不安全的行为,程序员应该unsafe在必要时将该代码隔离到一个块中。与 C++ 或 Zig 相比,Rust 使这些基本场景本质上更安全。

更多点击这里

总结
本文并非对各个语言的内存管理模型进行全面介绍。我想介绍的是每种语言中内存安全工作原理的公平和基本观点。

C++ 语言哲学允许你实现最广泛的行为,并且确实拥有所有花哨的功能。程序员可以自由选择自己的风格,并且必须根据自己的意愿实现某些功能,这可能会提供最佳的内存安全模型或最差的内存安全模型,具体取决于程序员和项目。

Zig 在 C++ 的自由度和 Rust 僵硬的内存管理模型之间实现了平衡,因此具有相当强的内存安全性,并且编写代码非常有趣。学习这门语言也相对简单,学习曲线更平缓,提供直观的功能,同时轻轻地推动编写内存安全的代码。

Rust 确实提供了强大的内存安全保证,因此编译器非常严格,控制力很强。除了基础知识之外,当你进入生命周期和异步编程时,事情就会变得非常困难,语言也会成为一个难以攻克的难题。