#pragma once #include namespace mln { /** * `Mutable` is a non-nullable uniquely owning reference to a `T`. It can be * efficiently converted to `Immutable`. * * The lifecycle of `Mutable` and `Immutable` is as follows: * * 1. Create a `Mutable` using `makeMutable(...)` * 2. Mutate it freely * 3. When you're ready to freeze its state and enable safe cross-thread * sharing, move assign or move construct it to `Immutable` * * The reason that `Mutable` exists, rather than simply using a * `std::unique_ptr`, is to take advantage of the underlying * single-allocation optimization provided by `std::make_shared`. */ template class Mutable { public: Mutable(Mutable&&) noexcept = default; Mutable& operator=(Mutable&&) noexcept = default; Mutable(const Mutable&) = delete; Mutable& operator=(const Mutable&) = delete; T* get() noexcept { return ptr.get(); } T* operator->() noexcept { return ptr.get(); } T& operator*() noexcept { return *ptr; } private: Mutable(std::shared_ptr&& s) noexcept : ptr(std::move(s)) {} std::shared_ptr ptr; template friend class Immutable; // NOLINTNEXTLINE(readability-redundant-declaration) template friend Mutable makeMutable(Args&&...); // NOLINTNEXTLINE(readability-redundant-declaration) template friend Mutable staticMutableCast(const Mutable&) noexcept; }; template Mutable makeMutable(Args&&... args) { return Mutable(std::make_shared(std::forward(args)...)); } template Mutable staticMutableCast(const Mutable& u) noexcept { return Mutable(std::static_pointer_cast(u.ptr)); } /** * `Immutable` is a non-nullable shared reference to a `const T`. * Construction requires a transfer of unique ownership from a `Mutable`; * once constructed it has the same behavior as `std::shared_ptr` but * with better indication of intent. * * Normally one should not share state between threads because it's difficult to * verify the absence of read/write data races. `Immutable` provides a guarantee * that no writes are possible, and instances therefore can be freely * transferred and shared between threads. */ template class Immutable { public: template Immutable(Mutable&& s) noexcept : ptr(std::const_pointer_cast(std::move(s.ptr))) {} template Immutable(Immutable s) noexcept : ptr(std::move(s.ptr)) {} Immutable(Immutable&&) noexcept = default; Immutable(const Immutable&) = default; template Immutable& operator=([[maybe_unused]] Mutable&& s) noexcept { ptr = std::const_pointer_cast(std::move(s.ptr)); return *this; } Immutable& operator=(Immutable&&) noexcept = default; Immutable& operator=(const Immutable&) = default; const T* get() const noexcept { return ptr.get(); } const T* operator->() const noexcept { return ptr.get(); } const T& operator*() const noexcept { return *ptr; } friend bool operator==(const Immutable& lhs, const Immutable& rhs) noexcept { return lhs.ptr == rhs.ptr; } friend bool operator!=(const Immutable& lhs, const Immutable& rhs) noexcept { return lhs.ptr != rhs.ptr; } private: Immutable(std::shared_ptr&& s) noexcept : ptr(std::move(s)) {} std::shared_ptr ptr; template friend class Immutable; // NOLINTNEXTLINE(readability-redundant-declaration) template friend Immutable staticImmutableCast(const Immutable&) noexcept; }; template Immutable staticImmutableCast(const Immutable& u) noexcept { return Immutable(std::static_pointer_cast(u.ptr)); } /** * Constrained mutation of an immutable reference. Makes a temporarily-mutable * copy of the input Immutable using the inner type's copy constructor, runs the * given callable on the mutable copy, and then freezes the copy and reassigns * it to the input reference. * * Note that other Immutables referring to the same inner instance are not * affected; they continue to referencing the original immutable instance. */ template void mutate(Immutable& immutable, Fn&& fn) { Mutable mut = makeMutable(*immutable); std::forward(fn)(*mut); immutable = std::move(mut); } } // namespace mln