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:
- How the type follows the rule of 5 mentioned.
- How the implementation doesn’t store type information anywhere, the type is captured in the lambda.
- How the lambda is converted to a function pointer (the plus sign before the lambda). The lambda becomes a plain function to which we store a pointer.
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