A use case for std::launder: SOO

In C++, container-like types often implement small object optimization (SOO). std::string does, std::any is encouraged to do, yenxo::Variant does it too.

SOO stores little objects in-place, and with big objects it falls back to a heap allocation. Heap allocations have cost, so we want to avoid it when possible.

SOO is a great opportunity to show a use case for std::launder. Shortly, std::launder is about informing the compiler that we are about to access a memory location in a way that the compiler doesn’t expect. Thus, it can do some safeguard work, like re-fetch the data to the registers. Here is a good explanation of why you need std::launder when you need it.

Below there is an example of an Any, a type-erased container that employs small object optimization and uses std::launder.

Note:

This capture-the-type-in-lambda (not object, but just type) technique is quite powerful and handy when you need type-erasure.

#include <iostream>
#include <cstddef>
#include <stdexcept>
#include <new>
#include <string>

/// \note For simplicity, doesn't support arrays.
/// \note We could use different lambdas instead of a lamda+Operation, but then the object would be larger.
class Any {
public:
    Any() noexcept : op{noop} {}

    template <class T> requires(sizeof(T) <= 8 && alignof(T) <= 8)
    explicit Any(T&& x) noexcept(noexcept(T{std::forward<T>(x)})) {
        static_assert(std::is_nothrow_destructible_v<T>, "for assignment operators to work");
        new (&ptr_or_storage.storage) T{std::forward<T>(x)};
        op = +[](PtrOrStorage& dst, PtrOrStorage const& src, size_t type, Operation op) noexcept {
            switch (op) {
            case Operation::copy_construct:
                new (&dst.storage) T{*std::launder(reinterpret_cast<T const*>(&src.storage))};
                break;
            case Operation::destruct:
                std::launder(reinterpret_cast<T*>(&dst.storage))->~T();
                break;
            case Operation::same_type:
                return typeid(T).hash_code() == type;
            }
            return false;
        };
    }

    /// \throw std::bad_alloc
    template <class T> requires(sizeof(T) > 8 || alignof(T) > 8)
    explicit Any(T&& x) noexcept(false) {
        ptr_or_storage.ptr = new T{std::forward<T>(x)};
        op = +[](PtrOrStorage& dst, PtrOrStorage const& src, size_t type, Operation op) {
            switch (op) {
            case Operation::copy_construct:
                dst.ptr = new T{*static_cast<T const*>(src.ptr)};
                break;
            case Operation::destruct:
                static_cast<T const*>(dst.ptr)->~T();
                delete static_cast<T const*>(dst.ptr);
                break;
            case Operation::same_type:
                return typeid(T).hash_code() == type;
            }
            return false;
        };
    }

    /// \throw std::bad_alloc
    Any(Any const& rhs) noexcept(false) : op{rhs.op} {
        op(ptr_or_storage, rhs.ptr_or_storage, unused_type, Operation::copy_construct);
    }

    /// \throw std::bad_alloc
    /// \note strong exception guarantee
    Any& operator=(Any const& rhs) noexcept(false) {
	Any tmp{rhs};
	return *this = std::move(tmp);
    }

    Any(Any&& rhs) noexcept : op{rhs.op}, ptr_or_storage{rhs.ptr_or_storage} {
        rhs.op = noop;
    }

    Any& operator=(Any&& rhs) noexcept {
        op(ptr_or_storage, unused_storage, unused_type, Operation::destruct);
        op = rhs.op;
        ptr_or_storage = rhs.ptr_or_storage;
        rhs.op = noop;
        return *this;
    }

    ~Any() noexcept {
        op(ptr_or_storage, unused_storage, unused_type, Operation::destruct);
    }

    bool has_value() const noexcept {
        return op != noop;
    }

    /// \throws std::logic_error
    /// \note strong exception guarantee
    template <class T>
    T const& value() const noexcept(false) {
        if (op == noop) {
            throw std::logic_error{"empty"};
        }

        if (!op(const_cast<PtrOrStorage&>(unused_storage), unused_storage, typeid(T).hash_code(), Operation::same_type)) {
            throw std::logic_error{"wrong type"};
        }

        return this->assume_value<T>();
    }

    /// \pre the value should be valid, otherwise UB
    template <class T>
    T const& assume_value() const noexcept {
        if constexpr (sizeof(T) <= 8 && alignof(T)) {
            return *std::launder(reinterpret_cast<T const*>(&ptr_or_storage.storage));
        } else {
            return *static_cast<T const*>(ptr_or_storage.ptr);
        }
    }

private:
    union PtrOrStorage {
        alignas(8) char storage[8];
        void const* ptr;
    };

    enum class Operation : short {
        copy_construct,
        destruct,
        same_type,
    };

    using Operator = bool (*)(PtrOrStorage& dst, PtrOrStorage const& src, size_t, Operation) noexcept(false);

    static constexpr Operator noop = +[](PtrOrStorage&, PtrOrStorage const&, size_t, Operation) noexcept {
        return false;
    };

    static constexpr size_t unused_type = 0;
    static constexpr PtrOrStorage unused_storage = {};

    Operator op;
    PtrOrStorage ptr_or_storage;
};

int main() {
    static_assert(sizeof(Any) == 16);

    Any x{10};
    Any y{std::string{"hello"}};

    std::cout << x.value<int>() << std::endl;
    try {
        x.value<char>();
    } catch (std::logic_error const&) {}

    std::cout << y.value<std::string>() << std::endl;

    y = x;
    std::cout << y.value<int>() << std::endl;
}

Build: g++ -std=c++20 -Wall -Wextra -fsanitize=address -O2 -DNDEBUG -fsanitize=leak -o /tmp/any /tmp/any.cpp

Build on godbolt: https://godbolt.org/z/Pb76j5cro

See some benchmarks comparing Any with the standard std::any to verify that this is a reasonable implementation: https://quick-bench.com/q/KWEqn7QubKu1qAaO0pkOK-eIyXA