All C++ keywords explained! – Part 1

Keywords from A to C

In this entry I will take a moment to enumarate all the keywords avaliable in C++ (yes, all of them) and give you some examples about how to use all of them and how useful they are in some situations. The article will be splitted in three parts to facilitate the search to the user. You can check the part 2 and part 3.

A

B

C


A

  • alignas: Specifies the alignment in memory for a class, struct, non bit field members and variables. If the alignment specified is smaller than the biggest size element, the program is ill-formed. It is equivalent to use alignas(alignof(type-id)). See: cppreference/alignas

C++
struct alignas(256) S {};

int main() {
    S s;
    std::cout << alignof(s); // Prints "256".
}

  • and: Performs a logic and operation. It is equivalent to &&. See: cppreference/and
C++
if (true and true) {
        std::cout << "It is true";
}

  • and_eq: Performs a bitwise and operation with the value itself. It is equivalent to &=. See: cppreference/end_eq
C++
auto b1 = 0xb11110000;
auto b2 = 0xb00101111;

std::cout << std::hex << b1 << '\n'; // b11110000
b1 and_eq b2;
std::cout << std::hex << b1 << '\n'; // b00100000

  • asm: Inline assembly code. This declaration is conditionally-supported and implementation defined, meaning that it may not be present and, even when provided by the implementation, it does not have a fixed meaning. See: cppreference/asm
C++
// This functions...
int f() {
    int x = 10;
    return x;
}

// ... Is equivalent to this assembly inline code.
asm(R"(
.globl func
    .type func, @function
    f:
    .cfi_startproc
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $10, -4(%rbp)
        movl    -4(%rbp), %eax
        popq    %rbp
        ret
    .cfi_endproc
)");

  • auto: This keyword has different uses but it can be summerize in the affirmation that it deduce at compile time the type of our declarations. The risk in this keyword is to assume default values in C++. For this, string literals come to help. See: cppreference/auto

B

C++
auto b1 = 0xb11110000;
auto b2 = 0xb00101111;

auto b3 = b1 bitand b2;
std::cout << std::hex << b3 << '\n'; // b00100000

C++
auto b1 = 0xb00110000;
auto b2 = 0xb00101111;

auto b3 = b1 bitor b2;
std::cout << std::hex << b3 << '\n'; // b00111111

  • bool: Capable of holding one of the two values: true or false. The value of sizeof(bool) is implementation defined and might differ from 1. See: cppreference/bool
C++
bool boolean = true;
bool boolean2 = false;

std::cout << sizeof(boolean); // 1

C++
int main() {
    int i = 2;
    switch (i) {
        case 1: std::cout << "1";   //<---- maybe warning: fall through
        case 2: std::cout << "2";
            break;
    }
 
    std::cout << '\n';
 
    for (int j = 0; j < 2; j++) {
        if (j == 1) break;
    }
}

C

  • case: Used only inside a switch statement to define different paths depending on the input value. See: cppreference/case
C++
switch (1) {
    case 1:
        std::cout << '1'; // prints "1"
        break;       // and exits the switch
    case 2:
        std::cout << '2';
        break;
}

C++
try {
    std::string("abc").substr(10); // throws std::length_error
} catch (const std::exception& e) { // reference to the base of a polymorphic object
    std::cout << e.what(); // information from length_error printed
}

C++
char c = 'h';
std::cout << sizeof(c); // 1

  • char8_t: Char type with explicit definition with at least 8 bits (1 byte) to represent UTF-8. See: cppreference/char8_t
C++
char8_t c = 'h';
std::cout << sizeof(c); // 1

  • char16_t: Char type with explicit definition with at least 16 bits (2 bytes) to represent UTF-16. See: cppreference/char16_t
C++
char16_t c = 'h';
std::cout << sizeof(c); // 2

  • char32_t: Char type with explicit definition with at least 32 bits (4 bytes) to represent UTF-32. See: cppreference/char32_t
C++
char32_t c = 'h';
std::cout << sizeof(c); // 4

  • class: It can be use to declare a class, an enumeration scoped, as a forward declaration or as a template type parameter. See: cppreference/class
C++
class Foo;  // forward declaration of a class
 
class Bar { // definition of a class
  public:
    Bar(int i) : m_i(i) {}
  private:
    int m_i;
};
 
template <class T> // template argument
void qux() {
    T t;
}
 
enum class Pub { // scoped enum, since C++11
    b, d, p, q
};
 
int main()
{
    Bar Bar(1);
    class Bar Bar2(2); // elaborated type
}

  • compl: Performs a bitwise complement operation. It is equivalent to ~. See: cppreference/compl
C++
auto b1 = 0xb00001111;
auto b2 = compl b1;
std::cout << std::hex << compl b2; // b11110000

  • concept: The way to define constraints to later be applied in templates. In the code snippet below we define a concept that requieres type T to be a floating point in order to success. They are always used along “requieres” See: cppreference/concept
C++
template<typename T>
concept floating = std::is_floating_point<T>::value;

template<typename T>
requires floating<T> 
void foo(T t)  {
    std::cout << "is floating point";
};

int main () {
    foo(10.5);
    foo(10); // Compile error, this integer does not fullfill the floating condition
}

  • const: Specifies that a variable is immutable. Can be used on pointers even if the underlying value is not immutable. It can also bee used on return types and functions. See: cppreference/const
C++
const x = 10;
x = 20; // Compile time error: assignment of read-only variable 'x'

  • consteval: Specifies that a function is an immediate function, that is, every call to the function must produce a compile-time constant. It can be used in lambdas, functions and if statements See: cppreference/consteval
C++
consteval int sqr(int n) {
  return n*n;
}
constexpr int r = sqr(100); // OK
 
int x = 100;
int r2 = sqr(x);  // Error: Call does not produce a constant

  • constexpr: It can be used in functions, lambdas and if statements. The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. See: cppreference/constexpr
C++
constexpr int foo() {
    return 42;
}
constexpr int x = foo(); // x = 42, directly at compile time

  • constinit: If a variable is marked as constinit, it has to be initializated at compile time with static or thread_local life duration. When de declared variable is a reference, constinit is equivalent to constexpr. When the declared variable is an object, constexpr mandates that the object must have static initialization and constant destruction and makes the object const-qualified, however, constinit does not mandate constant destruction and const-qualification See: cppreference/constinit
C++
const char *g() { return "dynamic initialization"; }
constexpr const char *f(bool p) { return p ? "constant initializer" : g(); }
 
constinit const char *c = f(true); // OK
constinit const char *d = f(false); // error: variable 'd' does not have a constant initializer

  • const_cast: Add or removes a const restriction on a variable. Removing const in a variable is undefined behaviour if you modify the value. It recieves a reference to the variable and returns a pointer to a new casted variable. See: cppreference/const_cast
C++
// Remove const
const int x = 10;
int* y = const_cast<int*>(&x);
*y = 20;
std::cout << *y;
// Add const
int j = 3; 
const int* pj = const_cast<const int*>(&j);
*pj = 10; // error: assignment of read-only location '* pj'

C++
for (int i = 0; i < 10; i++) {
    if (i != 5) continue;
    std::cout << i << " ";       //this statement is skipped each time i!=5
}

  • co_await: Suspends a coroutine and returns control to the caller. The expression next to the co_await keyword is converted into an awaitable. It performs an awaitable.operator co_await() if possible, if not, operator co_await(static_cast<Awaitable&&>(awaitable)) for a non-member overload. The awaiter.await_ready() is called, if it returns false then the coroutine is suspended and awaiter.await_suspend(handle) is called. We have different scenarios depending what awaiter.await_suspend(handle) returns:
    • Returns void: Then the action is returnet inmediatly to the caller.
    • Returns true: Returns control to the caller/resumer of the current coroutine.
    • Returns false: Resumes the current coroutine.
    • Returns a coroutine handle for some other coroutine: That handle is resumed (by a call to handle.resume())

Finally, the awaiter.await_resume() is called.

See: cppreference/co_await

C++
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>

struct awaitable {
    std::jthread* p_out;

    bool await_ready() {
        std::cout << "awaitable::await_ready()\n"; // Print 4.
        return false;
    }

    void await_suspend(std::coroutine_handle<> h) {
        std::cout << "awaitable::await_suspend()\n"; // Print 5.
        std::jthread& out = *p_out;
        out = std::jthread([h] { h.resume(); });
        std::cout << "New thread ID: " << out.get_id() << '\n'; // Print 6.
    }

    void await_resume() { 
        std::cout << "awaitable::await_resume()\n"; // Print 7. 
    }
};

struct task {
    struct promise_type {
        task get_return_object() {
            std::cout << "task::promise_type::get_return_object()\n"; // Print 1.
            return {};
        }
        std::suspend_never initial_suspend() { 
            std::cout << "task::promise_type::initial_suspend()\n"; // Print 2.
            return {};
        } 
        void return_void() {
            std::cout << "task::promise_type::return_void()\n"; // Print 9.
        }
        std::suspend_never final_suspend() noexcept { 
            std::cout << "task::promise_type::final_suspend()\n";  // Print 10.
            return {}; 
        }
        void unhandled_exception() {
            std::cout << "task::promise_type::unhandled_exception()\n"; 
        }
    };
};

awaitable switch_to_new_thread(std::jthread& out) {
    return awaitable{&out};
}

task resuming_on_new_thread(std::jthread& out) {
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; // Print 3.
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; // Print 8.
}

int main() {
    std::jthread out;
    resuming_on_new_thread(out);
}

// Output
task::promise_type::get_return_object()
task::promise_type::initial_suspend()
Coroutine started on thread: 139977331607360
awaitable::await_ready()
awaitable::await_suspend()
New thread ID: 139977331603200
awaitable::await_resume()
Coroutine resumed on thread: 139977331603200
task::promise_type::return_void()
task::promise_type::final_suspend()

  • co_return: We have a coroutine object that contains and manipulates the promise object. When a coroutine begins its execution it performs the following steps:
    • Allocates the coroutine.
    • Copies all function parameters to the coroutine state.
    • Call the constructor for the promise with all the parameters.
    • Calls promise.get_return_object() and store the result in a local variable.
    • Calls promise.initial_suspend()
    • Then when a couroutine reaches co_return it performs the following: Only one of the functions can be defined.
      • Calls promise.return_void()
      • Calls promise.return_value(expr)
    • Finally it calls the promise.final_suspend()

See: cppreference/co_return

C++
struct promise;
struct coroutine : std::coroutine_handle<promise> {
    using promise_type = struct promise;
};

struct promise {
    coroutine get_return_object() {
        std::cout << "promise::get_return_object()\n";
        return {coroutine::from_promise(*this)}; 
    }
    std::suspend_always initial_suspend() noexcept { 
        std::cout << "promise::initial_suspend()\n";
        return {}; 
    }
    std::suspend_always final_suspend() noexcept { 
        std::cout << "promise::final_suspend()\n";
        return {}; 
    }
    auto return_value(auto x) {
        std::cout << "promise::return_value(" << x << ")\n";
        return x;
    }
    void unhandled_exception() {}
};

int main() {
    coroutine h = [](int i) -> coroutine { // make i a coroutine parameter
    co_return i;
  }(42);
 
  h.resume();
  h.destroy();
}

// Output
promise::get_return_object()
promise::initial_suspend()
promise::return_value(42)
promise::final_suspend()

  • co_yield: Yield-expression returns a value to the caller and suspends the current coroutine. It’s equivalent to co_await promise.yield_value(expr) See: cppreference/co_yield
C++
#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>

template <typename T>
struct Generator {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;

    struct promise_type {  // required
        T value_;
        std::exception_ptr exception_;

        Generator get_return_object() {
            std::cout << "Generator::promise_type::get_return_object()\n";
            return Generator(handle_type::from_promise(*this));
        }
        std::suspend_always initial_suspend() { 
            std::cout << "Generator::promise_type::initial_suspend()\n";
            return {}; 
        }
        std::suspend_always final_suspend() noexcept { 
            std::cout << "Generator::promise_type::final_suspend()\n";
            return {}; 
        }
        void unhandled_exception() {
            exception_ = std::current_exception();
        }

        template <std::convertible_to<T> From>  // C++20 concept
        std::suspend_always yield_value(From&& from) {
            value_ = std::forward<From>(from);  // caching the result in promise
            std::cout << "Generator::promise_type::yield_value(" << value_ << ")\n";
            return {};
        }
        void return_void() {
            std::cout << "Generator::promise_type::return_void()\n";
        }
    };

    handle_type h_;

    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool() {
        fill();  // The only way to reliably find out whether or not we finished
                 // coroutine, whether or not there is going to be a next value
                 // generated (co_yield) in coroutine via C++ getter (operator
                 // () below) is to execute/resume coroutine until the next
                 // co_yield point (or let it fall off end). Then we store/cache
                 // result in promise to allow getter (operator() below to grab
                 // it without executing coroutine).
        return !h_.done();
    }
    T operator()() {
        fill();
        full_ = false;  // we are going to move out previously cached
                        // result to make promise empty again
        return std::move(h_.promise().value_);
    }

   private:
    bool full_ = false;

    void fill() {
        if (!full_) {
            h_();
            if (h_.promise().exception_)
                std::rethrow_exception(h_.promise().exception_);
            // propagate coroutine exception in called context

            full_ = true;
        }
    }
};

Generator<uint64_t> caller_count() {
    std::cout << "Called first time\n";

    co_yield 0;

    std::cout << "Called second time\n";

    co_yield 1;
}

int main() {
    auto gen = caller_count(); 

    for (int j = 0; gen; j++)
        gen();
}

// Output
Generator::promise_type::get_return_object()
Generator::promise_type::initial_suspend()
Called first time
Generator::promise_type::yield_value(0)
Called second time
Generator::promise_type::yield_value(1)
Generator::promise_type::return_void()
Generator::promise_type::final_suspend()

Check part 2