Skip to content

Instantly share code, notes, and snippets.

@jharmer95
Last active June 10, 2024 23:25
Show Gist options
  • Save jharmer95/ba0dde7b47eb70b38362edd905ca1806 to your computer and use it in GitHub Desktop.
Save jharmer95/ba0dde7b47eb70b38362edd905ca1806 to your computer and use it in GitHub Desktop.
Translating Rust keywords, concepts, and idioms to C++

Rust to C++

Language Only

Keywords

as

as has a few different contexts in Rust:

// (1) Use (i.e. import) alias
use long_crate_name::module_name as short_name;

// (2) Type alias
use crate_name::module_name::TypeName as TN;

// (3) Cast
let x = 22 as f32;

// (1) Namespace alias
namespace short_name = long_namespace::sub_namespace;

// (2) Type alias
using TypeAlias = mylib::TypeOG;

// OR (typedef)
typedef mylib::TypeOG TypeAlias;

// (3) Cast
const auto x = static_cast<float>(22);

// OR (functional-style)
const auto x = float{22};

// OR (old, C-style)
const auto x = (float) 22;

break

// (1) "Normal" break out of parent loop
let vals = vec![0, 1, 2, 22];

for i in vals {
    if i > 5 {
        break;
    }
}

// (2) Break out of named loop
let vals2 = vec![vec![0, 1, 2], vec![4, 5, 6]];

`my_loop: for i in vals2 {
    for j in i {
        if j > 5 {
            break `my_loop;
        }
    }
}

// (1)
const std::vector<int> vals { 0, 1, 2, 22 };

for (const auto i : vals) {
    if (i > 5) {
        break;
    }
}

// (2) NO TRUE EQUIVALENT!!!
const std::vector<std::vector<int>> vals2{ { 0, 1, 2 }, { 4, 5, 6 } };

// Would have to implement by either introducing a control variable...
bool break_my_loop = false;

for (auto it = vals2.cbegin(); !break_my_loop && it != vals2.cend(); ++it) {
    for (const auto j : *it) {
        if (j > 5) {
            break_my_loop = true;
            break;
        }
    }
}

// ... or by using `goto`:
for (const auto& i : vals2) {
    for (const auto j : i) {
        if (j > 5) {
            goto my_loop_end;
        }
    }
}

my_loop_end:
    // Code after loop

const

GOTCHA!: The const keyword exist in both C++ and Rust, but const in Rust is actually similar to constexpr in C++:

// (1) Compile-time variable
const TUNING_PARAM: f64 = 2.1654;

// (2) Compile-time-executable function
const fn add_two(num: i32) -> i32 {
    num + 2
}

// (1)
constexpr double TUNING_PARAM = 2.1654;

// (2)
constexpr int add_two(int num) {
    return num + 2;
}

Another use of const in Rust is to explicitly indicate that the target of a pointer is immutable (this is required by Rust).

let x: i32 = 12;
let p_x = &x as *const i32;

const int x = 12;
const int* p_x = &x;

NOTE: const in Rust is not the same as consteval in C++; there is currently no concept of compile-time only execution in Rust.

continue

continue is similar to break in that it can be a 1:1 replacement, but has a named loop version as well:

// (1) "Normal" use of continue:
for i in 0..10 {
    if i % 2 == 0 {
        continue;
    }
}

// (2) Continue named loop:
'my_loop: for i in 0..10 {
    for j in 5..10 {
        if j % 3 == 0{
            continue 'my_loop;
        }
    }
}

// (1)
for (int i = 0; i < 10; ++i) {
    if (i % 2 == 0) {
        continue;
    }
}

// (2) NO TRUE EQUIVALENT!!!

// Would have to implement by introducing a control variable...
bool continue_my_loop = false;

for (int i = 0; i < 10; ++i) {
    for (int j = 5; j < 10; ++j) {
        if (j % 3 == 0) {
            continue_my_loop = true;
            break;
        }
    }

    if (continue_my_loop) {
        continue_my_loop = false;
        continue;
    }
}

// ... or by using `goto`:
for (int i = 0; i < 10; ++i) {
my_loop_begin:
    for (int j = 5; j < 10; ++j) {
        if (j % 3 == 0) {
            ++i;
            goto my_loop_begin;
        }
    }
}

crate

A Rust crate can be similar to a C++ module, however a C++ binary can be built from multiple modules, while each binary in Rust is one crate.

else/if

if and else are the same in C++ (Rust does not require parentheses and does require braces, however). else can also, like in Rust, be combined to make else if.

Rust also has let if for conditional assignment and if let for pattern matching.

// (1) Plain `if`/`else if`/`else`:
if x > 5 {
    println!("Greater than 5");
} else if x == 5 {
    println!("Equal to 5");
} else {
    println!("Less than 5");
}

// (2) `let` + `if`/`else`:
let y = if x > 5 {
    2
} else {
    3
}

// (3) `if`/`else` + `let`:
let i: Option<i32> = some_func();

if let Some(i) = x {
    println!("i has value: {}", x.unwrap());
} else {
    println!("i is None");
}

// (1)
if (x > 5) {
    puts("Greater than 5");
} else if (x == 5) {
    puts("Equal to 5");
} else {
    puts("Less than 5");
}

// (2)

// Could be a ternary...
int y = x > 5 ? 2 : 3;

// ... or for more complex cases: an immediately-invoked lambda
int y = [x] {
    if (x > 5) {
        return 2;
    }

    return 3;
}();

// (3)
// C++ does not have pattern matching in the language!

// Could do something like:
const std::optional<int> i = some_func();

i.has_value() ? printf("i has value: %d\n", i.value()) : puts("i is nullopt");

// But it is not the same

enum

GOTCHA!: An enum in Rust may sometimes appear like an enum in C++, but a Rust enum is much more powerful. It's also worth noting that a "plain" Rust enum is close to a C++ enum class, not a raw enum.

// (1) "Plain" `enum`:
enum MachineState {
    Busy,
    Idle,
    Off,
    Error,
}

let state: MachineState = get_state();

match state {
    Busy => println!("Machine is busy"),
    Idle => println!("Machine is idling"),
    Off => println!("Machine is off"),
    Error => println!("Machine encountered an error"),
}

// (2) Advanced `enum`:
enum Character {
    Number(i32),
    Letter(char),
    Whitespace,
}

let c: Character = get_character();

match c {
    Number(num) => println!("Got number: {}", num),
    Letter(l) => println!("Got letter: {}", l),
    Whitespace => println!("Got whitespace"),
}

// (1)
enum class MachineState {
    Busy,
    Idle,
    Off,
    Error,
};

const MachineState state = get_state();

switch (state) {
    using enum MachineState;

    case Busy:
        std::cout << "Machine is busy\n";
        break;

    case Idle:
        std::cout << "Machine is idling\n";
        break;

    case Off:
        std::cout << "Machine is off\n";
        break;

    case Error:
        std::cout << "Machine encountered an error\n";
        break;
}

// (2)
// NO TRUE EQUIVALENT!!!

// Could be implemented as (unsafe) `union`...
struct Empty {};

union Character {
    int Number;
    char Letter;
    Empty Whitespace;
};

// ... but we have no way to know which type is held...

// ... or as a safe `variant`...
using Character = std::variant<int, char, std::monostate>;

const Character c = get_character();

if (c.holds_alternative<int>()) {
    std::cout << "Got number: " << std::get<int>(c) << '\n';
} else if (c.holds_alternative<char>()) {
    std::cout << "Got letter: " << std::get<char>(c) << '\n';
} else {
    std::cout << "Got whitespace\n";
}

// ... but again, C++ does not have pattern matching so it is limited

extern

While, extern technically has two uses in Rust, as of the 2018 edition of Rust, the extern crate functionality is almost never used.

// (1) Single function
extern "C" pub fn sum(n1: i32, n2: i32) -> i32;

// (2) Whole block
extern "C" {
    pub fn print_hello();
    pub static mut NUM: i32;
}

// (1)
extern "C" int sum(int n1, int n2);

// (2)
extern "C" {
    void print_hello();
    int NUM;
}

false/true

Boolean values true and false are exactly the same in Rust as C++.

let is_clean = true;
let is_dirty = false;

const bool is_clean = true;
const bool is_dirty = false;

fn

Rust has a separate keyword to define a function, where as C++ does not.

// (1) Declaring a function:
fn repeat_str(s: &str, n: i32) -> String;

// (2) Passing a function as a paremeter:
fn call_func(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg)
}

// (1)

// "Normal" syntax:
std::string repeat_str(std::string_view s, int n);

// Trailing return is closer to Rust:
auto repeat_str(std::string_view s, int n) -> std::string;

// (2)

// Function reference syntax (does not support lambdas)
int call_func(int(&f)(int), int arg) {
    return f(arg);
}

// Function object syntax (supports lambdas, but may allocate)
int call_func(const std::function<int(int)>& f, int arg) {
    return f(arg);
}

// Generic template syntax (support lambdas, no allocation, no explicit constraints)
template<typename F>
int call_func(F f, int arg) {
    return f(arg);
}

// C++20 Generic syntax (support lambdas, no allocation, no explicit constraints)
int call_func(auto f, int arg) {
    return f(arg);
}

// C++20 Constrained `requires` syntax (support lambdas, no allocation, explicit constraints)
template<typename F>
    requires requires (F f, int n) { { f(n) } -> std::same_as<int>; }
int call_func(F f, int arg) {
    return f(arg);
}

// C++20 Concept syntax (support lambdas, no allocation, explicit constraints)
template<typename F>
concept FnInt = requires (F f, int n) {
    { f(n) } -> std::same_as<int>;
};

int call_func(FnInt auto f, int arg) {
    return f(arg);
}

for

Unlike C++, Rust has multiple uses for the keyword for:

  • A loop
  • Indicating the implementation of a trait for a type
  • Higher-ranked trait bounds

NOTE: The last two will be covered under traits.

Rust's for loop is similar to C++, but mostly to it's "range-based" for.

// (1) Integer range
for i in 0..10 {
    println!("{}", i);
}

// (2) Iterator range
let v = vec![1, 2, 3];

for i in v {
    println!("{}", i);
}

// (1)

// Classic for loop
for (int i = 0; i < 10; ++i) {
    std::cout << i << '\n';
}

// C++20 Ranges for loop
for (const auto i : std::views::iota(0, 10)) {
    std::cout << i << '\n';
}

// (2)
const std::vector<int> v { 1, 2, 3 };

for (const auto i : v) {
    std::cout << i << '\n';
}

impl

Rust does not have classes or OOP like that found in C++. Therefore, an impl block is somewhat unique to the way Rust attaches functions to data structures.

// (1) Non-generic type
struct Person {
    age: i32,
    name: String,
}

impl Person {
    pub fn new(age: i32, name: &str) -> Self {
        Self {age, name: String::from(name)}
    }

    pub fn say_hello(&self) {
        println!("Hello, my name is {}!", self.name);
    }
}

// (2) Generic type
struct Pair<T> {
    first: T,
    second: T,
}

impl<T> Pair<T> {
    pub fn new(first: T, second: T) -> Self {
        Self {first, second}
    }

    pub fn say_hello(&self) {
        println!("Default hello!");
    }
}

// Partial specialization
impl Pair<bool> {
    pub fn say_hello(&self) {
        println!("Hello from bool!");
    }

    // Extension via partial specialization
    pub fn both_true(&self) -> bool {
        self.first && self.second
    }
}

// (1)
// .hpp file
class Person {
    int age;
    std::string name;

public:
    // Inline definition
    Person(int age, std::string_view name)
        : age(age), name(std::string{name}) {
    }

    // Declaration only
    void say_hello() const&;
};

// .cpp file
void Person::say_hello() const& {
    std::cout << "Hello, my name is " << name << "!\n";
}

// (2)
template<typename T>
class Pair {
    T first;
    T second;

public:
    Pair(T first, T second)
        : first(first), second(second) {
    }

    void say_hello() const& {
        std::cout << "Default hello!\n";
    }

    // Extension via partial specialization can't normally be done in C++ (would have to specialize the whole class)
    // Can use SFINAE instead
    template<typename U = T, typename = std::enable_if_t<std::is_same_v<U, bool>>>
    bool both_true() const& {
        return first && second;
    }
};

// Partial specialization of say_hello()
template<>
void Pair<bool>::say_hello() const& {
    std::cout << "Hello from bool!\n";
}

impl can also be used to implement a trait for a type, that will be shown later.

in

in looks into a set of iterators (or a range) in a for loop, see for for more information. The equivalent in C++ would be :.

let vec = vec![1, 2, 3];

for n in vec {
    println!("{}", n);
}

const std::vector<int> vec {1, 2, 3};

for (const auto n : vec) {
    std::cout << n << '\n';
}

let

While the concept of indicating an initialization with a keyword like let exists in many languages, C and C++ are not included in that list. It is important to note that the concept of const-ness in C/C++ is the default in Rust:

// (1) Compiler-inferred type
let x = 24;

// (2) Explicit type
let f: f64 = 2.2;

// (3) Structured binding of tuple
let (a, b) = ("alpha", "beta");

// (4) Structured binding of struct
struct Example {
    a: f32,
    b: u64,
}

let ex = Example { a: 2.23, b: 442 };
let Example { a: f, b: _ } = ex; // f == 2.23

// (1)
const auto x = 24;

// (2)
const double f = 2.2;

// (3)
const auto [a, b] = std::tuple { "alpha", "beta" };

// (4)
struct Example {
    float a;
    uint64_t b;
};

// C++20 designated initializer syntax
const Example ex{ .a = 2.23, .b = 442 };

const auto [f, _] = ex;

loop

C++ has no built-in equivalent of loop, but it is pretty trivially equal to while(true) in C/C++.

It could be given as:

#define loop while(true)

Of course, in Rust, all loops can be named, where C and C++ must result to goto labels or other control methods.

Additionally, Rust can use loop as an expresssion for assignment:

let mut i = 1;

let something = loop {
    i *= 2;

    if i > 10 {
        break i;
    }
}; // something == i == 16

int i = 1;

const auto something = [&i] {
    while(true) {
        i *= 2;

        if (i > 10) {
            return i;
        }
    }
}();

match

C++ has no pattern matching like Rust, the closest you can get is with a switch statement over integral and enum types.

let x: i32 = get_int();

match x {
    0 => println!("x is 0"),
    1 => println!("x is 1"),
    2 | 3 => println!("x is 2 or 3"),
    _ => println!("x is something else"),
}

const int x = get_int();

switch (x) {
    case 0:
        std::cout << "x is 0\n";
        break;

    case 1:
        std::cout << "x is 1\n";
        break;

    case 2:
    case 3:
        std::cout << "x is 2 or 3\n";
        break;

    default:
        std::cout << "x is something else\n";
        break;
}

mod

GOTCHA!: A module (mod) in Rust should not be mistaken for a module in C++. A C++ module is closer to a crate in Rust (though a C++ binary may have multiple modules), where a mod in Rust is more similar to a namespace in C++, though C++ namespaces do not inherently have public/private access notation.

// Crate "my_lib"
pub mod math {
    pub fn sum(n1: i32, n2: i32) -> i32 {
        n1 + n2
    }

    fn secret_func() {
    }
}

pub fn welcome() {
    internals::print_str("Welcome!");
}

mod internals {
    fn print_str(s: &str) {
        println!(s);
    }
}
// Crate "my_exe"
use my_lib;

fn main() {
    my_lib::math::sum(2, 6); // OK, math and sub are public
    my_lib::math::secret_func(); // ERROR! secret_func is not public

    my_lib::welcome(); // OK, welcome is pub
    my_lib::internals::print_str("Test"); // ERROR! internals and print_str are not public
}

// Module "my_lib"
export module my_lib;

// Need to fwd-declare private functions if used before private fragment
namespace internals {
    void print_str(std::string_view s);
}

namespace math {
    export int sum(int n1, int n2) {
        return n1 + n2;
    }

    void secret_func();
}

export void welcome() {
    internals::print_str("Welcome!");
}

module :private;

namespace internals {
    void print_str(std::string_view s) {
        std::cout << s << '\n';
    }
}
// Executable "my_exe"
import my_lib;

int main()
{
    // Notice you don't provide the "crate" name in C++
    const int x = math::sum(2, 6); // OK
    math::secret_func(); // ERROR! Not exported

    welcome(); // OK
    internals::print_str("Test"); // ERROR! Not exported
}

move

mut

Unlike Rust, in C++ variables, pointers, references and parameters are mutable by default (exception: lambda captures by-value require the mutable keyword). This means that the equivalent to mut in C++ is not making something explicitly immutable with const/constexpr.

// (1) Variable
let mut x = 12;

// (2) Reference
let mut r_x = &x;

// (3) Parameter
fn my_func(p1: &mut i32, p2: i32);

// (4) Immutable pointer to mutable data
let p_x = &mut x as *mut i32;

// (5) Mutable pointer to immutable data
let mut p_x2 = &x as *const i32;

// (1)
auto x = 12;

// (2)
auto& r_x = x;

// (3)
void my_func(int& p1, int p2);

// (4)
int* const p_x = &x;

// (5)
const int* p_x2 = &x;

pub

ref

return

return is the exact same in C++ as it is in Rust.

fn some_func(n: i32) -> i32 {
    if n > 120 {
        // Returns early
        return 120;
    }

    n + 1
}

int some_func(int n) {
    if (n > 120) {
        return 120;
    }

    // Note that C++ requires the return keyword, it cannot implicitly return an expression
    return n + 1;
}

self/Self

static

struct

super

trait

type

unsafe

There is not equivalent of unsafe in C++. All C++ code could be considered "unsafe".

It is possible to indicate "safe" vs "unsafe" code using something like namespaces (and making unsafe namespaces hidden to external code), but there is absolutely NO guarantee!

use

The C++ keyword using is most similar to the use keyword in Rust, though as of C++20, module keywords such as import also fulfil some of the same use cases.

// (1) Import name from another crate/module
use other_crate::mod1::MyType;

// (2) Import multiple names from another crate/module
use other_crate::mod1::{MyType, MyError};

// (3) Import all names from another crate/module
use other_crate::mod1::*;

// (4) Import and alias
use other_crate::mod1::MyType as Person;

// (5) Import variants from an enum
enum MyEnum {
    A,
    B,
}

use MyEnum::*;

// (6) Re-exporting items
mod mod2 {
    pub use mod1::MyType;
}

// (1) From another namespace
using mod1::MyType;

// (2)
// NO TRUE EQUIVALENT, have to alias each individually
using mod1::MyType;
using mod1::MyError;

// (3) NOT RECOMMENDED
using namespace mod1;

// With modules
import mod1;

// (4)
using Person = mod1::MyType;

// (5) Requires C++20
enum MyEnum {
    A,
    B,
};

using enum MyEnum;

where

In Rust, where specifies the constraints for generic parameters. Prior to C++20, C++ could do this but relied on complex metaprogramming tricks like SFINAE. As of C++20, concepts and requires provide C++ with some of the utility of Rust's where.

// (1) Constraining a generic type based on traits
fn print_thing<T>(thing: &T)
where
    T: Display,
{
    println!("{thing}");
}

// (2) Constraining with multiple traits
fn print_sum<T>(thing1: &T, thing2: &T)
where
    T: Add + Display,
{
    let tmp = thing1 + thing2;
    println!("{tmp}");
}

// (3) Constraining with lifetimes
fn ex_func<'a, 'b>(str1: &'a str, str2: &'b str) -> &'a str
where
    'b: 'a, // lifetime b outlives lifetime a
{
}

// (1)

//template<typename T>
//concept Display = ...;

// C++20 Requires clause
template<typename T>
    requires Display<T>
void print_thing(const T& thing) {
    std::cout << thing << '\n';
}

// C++20 Constrained Template syntax
template<Display T>
void print_thing(const T& thing) {
    std::cout << thing << '\n';
}

// C++20 Constrained auto syntax
void print_thing(const Display auto& thing) {
    std::cout << thing << '\n';
}

// Pre-C++20 SFINAE
template<typename T>
struct is_display;

// is_display implementation...

template<typename T>
inline constexpr bool is_display_v = is_display<T>::value;

template<typename T, typename std::enable_if_t<is_display_v<T>>* = nullptr>
void print_thing(const T& thing) {
    std::cout << thing << '\n';
}

// (2)
template<typename T>
    requires Add<T> && Display<T>
void print_sum(const T& thing1, const T& thing2) {
    const auto tmp = thing1 + thing2;
    std::cout << tmp << '\n';
}

// (3) NO EQUIVALENT

Note: requires clauses and expressions in C++ can be very powerful as they are checked at compile-time, allowing for more advanced and flexible capabilities compared to Rust.

Example:

template<typename T>
concept Indexable = requires(T a, size_t n)
{
    {a[n]} -> std::same_as<T::value_type>;
    {a.at(n)} -> std::same_as<std::optional<T::value_type>>;
};

while

Like the other loops in Rust, the while loop in C++ is essentially equivalent with the exceptions being named loops (see break for more) and while let pattern matching.

// (1) Simple while loop
while !check_done() {
    println!("Waiting...");
}

// (2) while let
fn get_input() -> Option<String>;

while let Some(mesg) = get_input() {
    println!(mesg);
}

// (1)
while (!check_done()) {
    std::cout << "Waiting...\n";
}

// (2) No direct equivalent!
std::optional<std::string> get_input();

for (auto mesg = get_input(); mesg.has_value(); mesg = get_input()) {
    std::cout << mesg.value() << '\n';
}

async / await

dyn

union

core

std

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment