more work on the Rust c++ comparison

This commit is contained in:
Pagwin 2025-05-22 14:28:43 -04:00
parent 4bcfdeb435
commit 07ef4651c2

View file

@ -23,12 +23,14 @@ TODO: make these links
- Hello World
- Variables
- Copying and Moving
- Basic Types
- Basic/Primitive Types
- Common Stdlib Types
- Loops
- Functions
- Making Your Own Types
- Type Aliasing
- Compile time computation
- Pattern Matching
- Generics
- Modularity
- Dependency Management and Build Systems
@ -152,6 +154,9 @@ std::vector<std::unique_ptr<int>> second = std::move(first);
second = std::move(first)
```
Doing making a type move only manually involves deleting the lvalue reference constructor and assignment operators and overriding the rvalue reference constructor and assignment operators.
For those in need of more info looking into the rule of 5 will help.
### Rust
In Rust we have the opposite phenomenon, a value will be moved if you don't take action to prevent it.
@ -185,54 +190,237 @@ let second = first.clone();
let third = first;
```
## Basic Types
## Basic/Primitive Types
Nothing interesting, here's a table providing info on equivalent types.
While C++ literals and Rust literals and how they differ are mildly interesting, I haven't given them a sections.
At the type level though there's nothing interesting, so here's a table providing info on equivalent types.
If a C++ type has `std::` at the front it isn't a primitive.
Note: If a C++ type has `std::` at the front it isn't a primitive type.
Note2: if you see something like `asdf(1,2,3,4)` that just means I don't want to make 4 rows in the table for `asdf1`, `asdf2`, `asdf3` and `asdf4`.
The 3 dots serve the same purpose for `unsigned` because words are long.
Note3: Sometimes I will a single capital letter, like `T` as a placeholder for templates/generics.
|C++|Rust|
|-|-|
|`bool`|`bool`|
|`char`|no equivalent|
|`char`|equivalent is C++ implementation dependant|
|`signed char`|`i8`|
|`unsigned char`|`u8`|
|`short`|no equivalent `i16` is the closest|
|`int`|no equivalent `i32` is the closest|
|`long`|no equivalent `i32` is the closest|
|`long long`|no equivalent `i64` is the closest|
|`unsigned ...`|`u...`|
|`unsigned ...`|`u(8,16,32,64)`|
|`std::int(8,16,32,64)_t` |`i(8,16,32,64)`|
|`std::uint(8,16,32,64)_t`|`u(8,16,32,64)`|
|no equivalent barring compiler extensions|`i128`|
|no equivalent barring compiler extensions|`u128`|
|`float`|`f32`|
|`double`|`f64`|
|`long double`|no equivalent|
|`&T`|`&mut T`|
|`&const T` or `const &T`|`&T`|
|`*T`|`*mut T`|
|`*const T` or `const *T`|`*const T`|
|`std::float16_t` (C++23)|`f16` (nightly at time of writing)|
|`std::float128_t` (C++23)|`f128` (nightly at time of writing)|
|`std::size_t`|`usize`|
|`std::ptrdiff_t` (not touching `ssize_t` thanks)|`isize`|
|`T[N]` or `std::array<T,N>`|`[T;N]`|
|no equivalent `std::uint32_t` is the closest|`char`|
|no equivalent `std::string_view` is the closest|`str`|
|no equivalent `std::span<T>` is the closest|`[T]`|
|`void`|no equivalent `()` is the closest|
|no equivalent `void`, `std::monostate` and `std::tuple<>` are the closest|`()`|
|no equivalent `[[noreturn]] void` is the closest | `!` (nightly at time of writing) |
## Common Stdlib Types
More equivalent types, this time from the stdlib, here's another table.
Note: (in prelude) just means you don't need the `std::blah` or a `use` to use the type.
Note2: Sometimes I will a single capital letter, like `T` as a placeholder for templates/generics.
|C++|Rust|
|-|-|
|`std::vector<T>`|`std::vec::Vec<T>` (in prelude)|
|`std::span<T>`| `&[T]`|
|`std::string`|`std::string::String` (in prelude) or `std::ffi::OSString`|
|`std::string_view`|`&str`|
|`std::optional<T>`|`std::option::Option<T>` (in prelude)|
|`std::expected<T,E>`|`std::result::Result<T,E>` (in prelude)|
|`std::unique_ptr<T>`|`alloc::boxed::Box<T>` (in prelude)|
|`std::shared_ptr<T>`|`alloc::rc::Rc<T>`|
|`std::weak_ptr<T>`|`alloc::rc::Weak<T>`|
## Loops
TODO
### Commentary
All of this is within some function body.
### C++
### `while` loops
### Rust
```cpp
// C++
while (some_condition) {
//jump to next loop
continue;
// exit loop early
break;
}
```
```rs
//Rust
while some_condition {
continue;
break;
}
```
### `for` loops
Rust does not have a direct equivalent to C++ for loops of the form.
```cpp
for(auto v = some_initial_value; condition; update()){
// loop body
}
```
However it does have an equivalent to loops of the form
```cpp
for(auto v:some_iterator){
// loop body
}
```
namely
```rs
for v in some_iterator {
// loop body
}
```
Move by default and pattern matching can lead to potentially confusing loops however.
While on the subject of iterators though both languages have higher level constructs for working on iterators.
There are a lot of these however and they are frequently named similarly so instead of making an incomplete table instead I'd like to write about how iterators differ between the 2 languages.
In C++ an iterator is an object which has overloads which allow it to be treated as a pointer.
What this means in practice is that until the introduction of the ranges library in C++20 (which I haven't been able to use without getting a compiler error yet) that passing in an iterator to some function generally meant passing in the start and end point iterators.
In Rust things work differently, instead all iterators must fit a particular shape (see #Generics(TODO: hyperlink) for what that means) where they have a `next` method which returns an `Option` (see (#Common Stdlib Types) (TODO: hyperlink) for more info).
This means that
1. The iterator can be passed as 1 value
2. The iterator can be only partially evaluated
For comparison lets compare usage of C++ `std::transform` from the algorithms library to Rust's `map` method.
```cpp
std::vector<int> bucket{};
std::transform(some_iterator.begin(), some_iterator.end(), std::back_inserter(bucket), [](auto _){return 3;});
```
Notice the need to have an explicit location to output things onto when we do the transform rather than when we need the values.
```rs
let bucket:Vec<i32> = some_iterator
.map(|_|{return 3;})
.collect();
```
This Rust version arguably does more than what is needed.
If we had left things at just the usage of `map` it would functionally be a no-op, we wouldn't need somewhere to store our transformed data because we didn't make it yet.
However to keep the 2 examples equivalent `collect` was used to gather things up into a `Vec`.
C++20 ranges (with some C++23 additions) in principle allow for C++ code which is similar to this Rust code to be written like follows
```cpp
namespace views = std::ranges::views;
std::vector<int> bucket = some_iterator
| views::transform([](auto _){return 3})
| std::ranges::to<std::vector>();
```
However I suffer from severe skill issue when using ranges so I haven't been able to write something like this off the cuff without running into a compiler error wall so I can't vouch for it.
## Functions
TODO
### Commentary
Basic functions in Rust and C++ are by and large very similar to each other in terms of semantics, they differ mostly in terms of syntax.
### C++
A basic example with the 2 notable C++ syntax variants and the 2 ways of doing it in Rust is below.
### Rust
```cpp
using i32 = std::int32_t;
i32 add(i32 a, i32 b) {
return a+b;
}
auto add(i32 a, i32 b) -> i32 {
return a+b;
}
```
```rs
fn add(a:i32, b:i32) -> i32 {
return a+b;
}
fn add(a:i32, b:i32) -> i32 {
a+b
}
```
The difference in C++'s case is how the function is declared (basically syntax sugar) while in Rust's case the difference is due to there being multiple ways of deciding what value a function returns.
For Rust you can have a function return it's value via `return` however you can also have a function return it's value via having the last expression in the function body evaluate to the return type.
In addition to basic functions we also have closures/anonymous functions though.
The following demonstrates them without captures, capture by reference and capture by move.
```c++
using i32 = std::int32_t;
std::unique_ptr<i32> x = std::make_unique(3);
auto add = [](i32 a, i32 b){
return a+b;
};
auto add_ref_x = [&x](i32 a, i32 b){
return a+b+*x;
};
auto add_move_x = [x = std::move(x)](i32 a, i32 b){
return a+b+*x;
};
```
```rs
let x = Box::new(3);
let add = |a:i32, b:i32| {
return a+b;
};
let add_ref_x = |a:i32, b:i32| {
return a+b+*x;
};
let add_mov_x = move |a:i32, b:i32| {
return a+b+*x;
};
```
Again there isn't a meaningful substance difference though in C++'s case moving and referencing local values was a bit more tedious than it was in Rust which can be nice in some cases.
In both cases the language just goes and makes an anonymous type for us which has the needed struct/class members and function call operator then constructs a value of that type with values from the surrounding function.
Well kinda, I'd link to a blog post going into detail on how things differ on a type level but I can't find it atm, sorry.
## Making Your Own Types
TODO
### Commentary
### C++