Some Ideas on C++ Programming

2025-05-25

About const

The const keyword can:

const can also modify the method itself. In a const method, the this pointer is immutable — meaning the method cannot modify the object's member variables.

A single method can have both const and non-const versions. When both exist, a const object will call the const version, while a non-const object will call the non-const version.

If only one version exists:

Therefore, methods should be defined as const whenever possible.

#include <iostream>

class Foo {
public:
    void print() {
        i++;
        std::cout << "non-const version" << std::endl;
    }
    void print() const {
        std::cout << "const version" << std::endl;
    }
private:
    int i = 0;
};

int main() {
    Foo a;
    a.print(); // prints "non-const version"
    const Foo b;
    b.print(); // prints "const version"
}

Use explicit on Constructors

In C++, single-argument constructors allow implicit type conversion by default. This means a function parameter of type T can accept an argument of type X, as long as T defines a single-argument constructor taking X.

This feature can sometimes be useful. For example, RocksDB's Slice class. Wherever a Slice parameter is needed, passing a const std::string& works directly, avoiding boilerplate code for constructing a temporary Slice object.

Sometimes, however, this implicit conversion is undesirable. Adding the explicit keyword to the constructor prevents it from participating in implicit conversions. The constructor must then be called explicitly.

For multi-argument constructors, explicit is also useful. It prevents implicitly converting a braced-init-list to the object type, which would otherwise happen in contexts like copy-list-initialization or function calls. This avoids unintended implicit constructions, not "construct then copy" — copy/move behavior is separate.

For example, with a non-explicit constructor, push_back({1, "a"}) could implicitly construct a Foo and insert it. With explicit, this fails to compile — you must call the constructor explicitly:

class Foo {
    explicit Foo(int a, std::string b) : a(a), b(b) {}
};

std::vector<Foo> v;
v.push_back({1, "a"});  // compile error
v.emplace_back(1, "a"); // compiles fine

Explicitly Delete Unwanted Constructors

C++ allows defining copy constructors, copy assignment operators, move constructors, and move assignment operators. These let programmers customize behavior during copying and moving.

For non-value objects, these are often unnecessary and can cause bugs from unexpected implicit calls. A good practice is to mark them as deleted, preventing the compiler from auto-generating them. Then, when an unexpected call occurs, the compiler will report an error.

class Foo {
public:
    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;
    Foo(Foo&&) = delete;
    Foo& operator=(Foo&&) = delete;
};

Mark Performance-Sensitive Methods as noexcept

For performance-sensitive methods, marking them noexcept indicates they will not throw exceptions. This allows the compiler to optimize the generated instructions for better performance.

If exceptions are unavoidable, marking the method as noexcept(false) can serve as a text-level reminder to maintainers that this function may throw and is a candidate for closer review.

About Virtual Functions

When a virtual function is called through a parent class object, the runtime must look up the vtable to find the child class's function address, then load and execute it. This incurs some performance cost — not because of the dynamic lookup itself, but because the vtable prevents function inlining.

One solution is the Curiously Recurring Template Pattern (CRTP):

template<class T>
class Parent {
    void foo() {
        static_cast<T*>(this)->fooImpl();
    }
};

class Child : public Parent<Child> {
    void fooImpl() { ... }
};

In this pattern, the parent class already knows the child class type. So in the foo method, there is no need to look up the child's function definition — it is called directly, removing the barrier to inlining.

About Optional

C++ std::optional is a monad from functional programming, used to represent a value that may or may not exist.

Despite C++'s claim of "zero-cost abstractions," this is not entirely true here. The implementation of std::optional requires an extra bool to indicate whether the value exists, typically 1 byte. On most platforms, memory addresses are aligned — for example, 8-byte aligned on AMD64 Linux — which can cause the object to occupy more memory.

#include <iostream>
#include <optional>

class Foo {
public:
    Foo() = default;
    void print() const {
        std::cout
            << "size of i: " << sizeof(this->i) << "\n"
            << "size of l: " << sizeof(this->l) << std::endl;
    }
private:
    long i = 0;
    std::optional<long> l = std::nullopt;
};

int main() {
    Foo a;
    std::optional<Foo> b;
    std::optional<std::optional<Foo>> c;

    a.print();
    std::cout << "size of a: " << sizeof(a) << std::endl;
    std::cout << "size of b: " << sizeof(b) << std::endl;
    std::cout << "size of c: " << sizeof(c) << std::endl;
}

Output:

size of i: 8
size of l: 16
size of a: 24
size of b: 32
size of c: 40

For performance-sensitive code, use std::optional with caution.