diff --git a/posts/rust-cpp-comparison.md b/posts/rust-cpp-comparison.md index dd0cc7e..35fc5d9 100644 --- a/posts/rust-cpp-comparison.md +++ b/posts/rust-cpp-comparison.md @@ -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> 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]`| |no equivalent `std::uint32_t` is the closest|`char`| |no equivalent `std::string_view` is the closest|`str`| +|no equivalent `std::span` 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`|`std::vec::Vec` (in prelude)| +|`std::span`| `&[T]`| +|`std::string`|`std::string::String` (in prelude) or `std::ffi::OSString`| +|`std::string_view`|`&str`| +|`std::optional`|`std::option::Option` (in prelude)| +|`std::expected`|`std::result::Result` (in prelude)| +|`std::unique_ptr`|`alloc::boxed::Box` (in prelude)| +|`std::shared_ptr`|`alloc::rc::Rc`| +|`std::weak_ptr`|`alloc::rc::Weak`| + ## 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 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 = 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 bucket = some_iterator + | views::transform([](auto _){return 3}) + | std::ranges::to(); +``` + +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 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++