“What’s your favorite C++20 feature?”

is one of my favorite interview questions, generic to the version one says they use. It gauges how often someone programs C++ on their computer (not just on Leetcode), and how well they understand the ecosystem they program in. I think there’s something to be said for a programmer that upgraded to C++20 specifically to use Modules avoid ODR violations, especially compared to a programmer who uses whatever C++ version brew install fetched at the time.

In many examples below, I’ll be using snippets from Raquest, a domain-specific language to replace Postman and Insomnia.

Use cppreference to create a list of your favorite C++ features. Here are a few of mine for inspiration:

C++11

Think of any modern C++ feature—chances are it’s from C++11. Below are five that I can’t imagine writing C++ without. Consider additional reading on rvalue references, smart pointers, decltype, final, = delete, and ~20 more things I didn’t list here.

Move semantics

Move semantics are the contract allowing one object to “steal” resources from another. Instead of copying large resources like heap allocations between object instances, you can move them, copying the pointer into the new object and hence ownership of the underlying heap object. Consider this elementary std::vector implementation:

class MyVector {
  public:
    size_t size_;
    int* data_;
    // ... constructor, copy constructor ...
    MyVector(MyVector&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        // you must leave [`other`] in a valid state
        other.size_ = 0;
        other.data_ = nullptr;
    }
    // ... copy/move assignment operators, destructor ...
};

Move semantics in C++ standardize the common idea of transferring ownership (or better, the responsibility to manage) of resources. Move constructors and assignment operators encapsulate the steal-then-nullify logic in one place.

Without move semantics, you’d do it the same as you would in C:

MyBuffer* move_buffer(MyBuffer* src) {
    MyBuffer* dest = malloc(sizeof(MyBuffer)); 
    dest->data = src->data;
    dest->size = src->size;
    src->data = NULL;
    src->size = 0;
    return dest; 
}

Along with the rule of five, move semantics provide a universal protocol for moving resources that C++ compilers and develoeprs understand.

Lambda expressions

Lambda expressions create closures. Below is a use of a lambda to construct a function in-place for use in a thread pool.

ThreadPool pool(config().jobs);
for (const auto &filename : input_filenames.value()) {
    futures.emplace_back(
        pool.submit([filename] { return Raquest(filename).run(); }));
}

The lambda here is [filename] { return Raquest(filename).run(); }. It’s an anonymous function capturing the filename from the for each loop and constructing an object with it.

nullptr

Consider this code:

void f(char *ptr);
void f(int num);
f(NULL);

What does f(NULL) refer to? With #define NULL 0, f(int num) will be called, despite the likely intention. Calling f(nullptr) would remove this ambiguity.

An explicit nullptr_t type removes ambiguity with types. With #define NULL 0 effectively making NULL an int, how do you resolve overloaded functions?

auto

As a fan of Almost Always Auto C++, auto is one of to most common keywords in my codebases. It allows the compiler to deduce the type of an lvalue based on its initializer.

It’s elementary use case is just to infer the types of literals:

auto x = 42; // int
auto pi = 3.14159; // double

But you can also use it to infer the type of generic lambdas, for example.

auto custom_hello_world(const std::string& greeting) {
    // the returned lambda is generic: it accepts any type that can be converted to a string.
    return [greeting](auto&& name) -> std::string {
        return greeting + ", " + std::forward<decltype(name)>(name) + "!";
    };
}

Lastly, I often use it as a shorthand reference for super long types. Hopefully you aren’t using namespace std.

std::vector<std::future<
  std::expected<CurlResponse, std::vector<std::unique_ptr<Error>>>>>
  futures;
...
for (auto &fut : futures)

constexpr

constexpr gets crazy pretty quickly, but in essence, it denotes computations to be performed at compile-time.

Consider this program printing square(5) to std::cout:

constexpr int square(int x) {
    return x * x;
}
 
int main() {
    constexpr int val = square(5);
    std::cout << val << "\n";
    return 0;
}

The compiler computed itself and embedded 25 as an immediate value, with no call to square:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 25
        mov     esi, 25
        mov     edi, OFFSET FLAT:std::cout
        call    std::ostream::operator<<(int)
        mov     eax, 0
        leave
        ret

Now what if we remove comptime?

int square(int x) {
    return x * x;
}
 
int main() {
    int val = square(5);
    std::cout << val << "\n";
    return 0;
}

The binary now calls square at runtime:

square(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, eax
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     edi, 5
        call    square(int)
        mov     DWORD PTR [rbp-4], eax
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:std::cout
        call    std::ostream::operator<<(int)
        mov     eax, 0
        leave
        ret

C++14

C++14 is described as “a minor version after the major version C++11, featuring mainly minor improvements and defect fixes”. C++14 isn’t the best standard for this interview question, but there’s still a few features to note:

decltype(auto)

decltype is useful when declaring types that are difficult or impossible to declare using standard notation, like lambda-related types or types that depend on template parameters.
cppreference

decltype(auto) expands on upon that, particularly for ergonomics when implementing functions not only generic to a T, but also to whether that T is a reference or not.

Consider:

struct MyClass {
    int value;
    MyClass(int v) : value(v) {}
    
    // overriding copy constructor to print when copied
    MyClass(const MyClass& other) : value(other.value) {
        std::cout << "Copied\n";
    }
};
 
// auto: the returned type is MyClass (copy).
auto get_object_copy(std::vector<MyClass>& vec, size_t i) {
    return vec[i];
}
 
// decltype(auto): preserves the reference type.
decltype(auto) get_object_ref(std::vector<MyClass>& vec, size_t i) {
    return vec[i];
}
 
int main() {
    // construct objs
    std::vector<MyClass> objects;
    objects.emplace_back(100);
 
    // use auto fn, observe side effects
    std::cout << "Using `auto` fn: ";
    auto copy_obj = get_object_copy(objects, 0);
    copy_obj.value = 200;
    std::cout << "Original object value: " << objects[0].value << std::endl;
 
    // use decltype(auto) fn, observe side effects
    std::cout << "Using `decltype(auto)` fn: ";
    decltype(auto) ref_obj = get_object_ref(objects, 0);
    std::cout << "\n";
    ref_obj.value = 300;
    std::cout << "Original object value: " << objects[0].value << std::endl;
 
    return 0;
}

The output is:

Using `auto` fn: Copied
Original object value: 100
Using `decltype(auto)` fn:
Original object value: 300

In the output, we see that the auto function called the copy constructor, while decltype(auto) preserved the reference it was passed (& vec).

[[deprecated]]

It’s an annotation for library devs to signal users to migrate to new APIs:

[[deprecated("Use new_implementation instead.")]]
void bad_implementation() {...}
 
void new_implementation() {...}

C++17

We’re back to the path of major releases with C++17.

std::variant

std::variant is a type-safe union. You can use std::get_if, std::get, and std::visit to access the underlying T.

I find std::visit combined with comptime to be quite powerful:

constexpr inline std::string ParserError::get_brief() const {
  return std::visit(
    [](auto &&arg) {
      using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, MalformedSectionHeader>)
          return "malformed section header";
        else if constexpr (std::is_same_v<T, ExpectedColonInHeaderAssignment>)
          return "missing colon in header assignment";
       },
    info_);
}

See also: constexpr virtual functions from C++20.

std::optional

std::optional is quite nice for handling nullish yet non-error states. Consider this function for finding a certain header if it exists in an HTTP response:

std::optional<std::string>
CurlResponse::get_header_value(const std::string &key) const {
    auto it = headers.find(key);
    if (it != headers.end()) {
        return it->second;
    }
    return std::nullopt;
}

With std::optional, the “not found” state is encoded in std::nullopt. Other ways to do this might involve new, passing a std::string& as a parameter, or worries about ownership. There exist good reasons to use each of these tactics, but I find std::optional fits my style best.

std::reduce

std::reduce is a declarative and parallelizable way to fold collections.

Compare this imperative way to sum a collection:

int sum_for_loop = 0;
for (int n : numbers) {
  sum_for_loop += n;
}

To this one using std::reduce:

int sum_par = std::reduce(std::execution::seq, numbers.begin(), numbers.end(), 0);

Swap seq for par for free (and libre) potential parallelism.

C++20

Another major release.

Semaphores

Via platform-specific APIs like POSIX’s pthread.h, you could of course use semaphores before C++20, and even before C++98. But now, it’s a bit cleaner and more maintainable for platform-agnostic use cases.

// C++20
#include <semaphore>
std::counting_semaphore<2> sem(2);
 
// before
#include <pthread.h>
sem_t sem;
sem_init(&sem, 0, 2);

[[unlikely]]

[The [[likely]] and [[unlikely]] directives] allow the compiler to optimize for the case where paths of execution including that statement are more or less likely than any alternative path of execution that does not include such a statement.
cppreference

Note that there are many reasons to avoid these attributes. That’s why it’s my C++ features list. Write C++, read the reference, and make your own opinions.

Modules

C++20 modules can:

  • better organize code
  • avoid ODR violations
  • improve compile times
  • deduplicate content between headers and source files
  • be annoying to use with CMake

In Raquest, I have a custom thread pool defined in thread_pool.cppm that encapsulates threading logic.

Here’s what it looks like:

module;
// #include ...
export module ThreadPool;
export class ThreadPool {
  public:
    explicit ThreadPool(size_t threads);
 
    /**
     * @brief Schedule a new task.
     * @return A future to retrieve the result later.
     */
    template <class F, class... Args>
    auto submit(F &&f, Args &&...args)
        -> std::future<typename std::invoke_result_t<F, Args...>>;
 
    /**
     * @brief Signal all threads to stop and join them during destruction.
     */
    ~ThreadPool();
 
  private:
    std::vector<std::thread> workers_;
    std::queue<std::function<void()>> tasks_;
    std::mutex queue_mutex_;
    std::condition_variable cond_;
    bool stop_ = false;
};
// function implementations ...

You can see how what would be split between .hpp and .cpp is now just in .cppm.

With this in my CMakeLists.txt:

add_library(ThreadPool STATIC
    src/thread_pool.cppm
)
 
target_sources(ThreadPool
    PUBLIC
        FILE_SET CXX_MODULES
        FILES
            src/thread_pool.cppm
)

I can use import ThreadPool throughout my application to use the pre-compiled module.

constexpr virtual functions

I’ve really enjoyed using these in to move the mapping of custom enum Error variants to error messages to comptime.

With a pure virtual function get_brief defined in an abstract Error class:

/**
 * @brief An abstract Error class.
 */
class Error {
  public:
    explicit Error(const std::string &file_name) : file_name_(file_name) {}
 
    virtual ~Error() = default;
 
    /**
     * @brief A constexpr to map inner error Type enumerators to
     * short titles for error output headings.
     */
    constexpr virtual std::string get_brief() const = 0;
 
    const std::string file_name_;
};

I can constexpr this:

constexpr inline std::string ParserError::get_brief() const {
  return std::visit(
    [](auto &&arg) {
      using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, MalformedSectionHeader>)
          return "malformed section header";
        else if constexpr (std::is_same_v<T, ExpectedColonInHeaderAssignment>)
          return "missing colon in header assignment";
    },
    info_);
}

See also: std::variant from C++17.

using enum

When using enums, use scoped enums, and use using enum. This is the best way to handle scope namespace clashing with enums which is a common annoyance from C.

Consider this snippet without using enum:

enum class Color { Red, Green, Blue };
void paint(Color c) {
    switch (c) {
        case Color::Red:
            std::cout << "Painting red.\n";
            break;
        case Color::Green:
            std::cout << "Painting green.\n";
            break;
        case Color::Blue:
            std::cout << "Painting blue.\n";
            break;
    }
}

And with:

enum class Color { Red, Green, Blue };
using enum Color;
void paint(Color c) {
    switch (c) {
        case Red:
            std::cout << "Painting red.\n";
            break;
        case Green:
            std::cout << "Painting green.\n";
            break;
        case Blue:
            std::cout << "Painting blue.\n";
            break;
    }
}

C++23

std::expected

std::expected is similar to std::optional, but allows you to choose between a success value or an error value, not just a success value or std::nullopt.

Consider the same ThreadPool example from Modules above:

std::vector<std::future<
    std::expected<CurlResponse, std::vector<std::unique_ptr<Error>>>>>
    futures;

The std::expected here expresses that the function within the future returns a CurlResponse on success and a vector of errors on failure.

if consteval

if consteval makes comptime vs runtime branching clearer.

Imagine we’re writing a program that wants to call a square function at comptime sometimes, and at runtime sometimes:

constexpr int square_dispatch(int x) {
    if (std::is_constant_evaluated()) {
        return compile_time_square(x);
    } else {
        return fallback_square(x);
    }
}

It’s a bit cleaner in our case to do:

int square_dispatch(int x) {
    if consteval {
        return compile_time_square(x);
    } else {
        return x * x;
    }
}

std::unreachable

std::unreachable is the C++ std way to do __builtin_unreachable(), or __assume(false) for those still using MSVC.

Consider:

const char* color_to_string(Color c) {
    switch (c) {
        case Color::Red:   return "Red";
        case Color::Green: return "Green";
        case Color::Blue:  return "Blue";
    }
    std::unreachable(); // undefined behavior if we reach here
}