Program Listing for File storage.hpp#

Return to documentation for file (librapid/include/librapid/array/storage.hpp)

#ifndef LIBRAPID_ARRAY_STORAGE_HPP
#define LIBRAPID_ARRAY_STORAGE_HPP

/*
 * This file defines the Storage class, which contains a contiguous
 * block of memory of a single data type.
 */

namespace librapid {
    namespace typetraits {
        template<typename Scalar_>
        struct TypeInfo<Storage<Scalar_>> {
            static constexpr bool isLibRapidType = true;
            using Scalar                         = Scalar_;
            using Backend                        = backend::CPU;
        };

        template<typename Scalar_, size_t... Dims>
        struct TypeInfo<FixedStorage<Scalar_, Dims...>> {
            static constexpr bool isLibRapidType = true;
            using Scalar                         = Scalar_;
            using Backend                        = backend::CPU;
        };

        LIBRAPID_DEFINE_AS_TYPE(typename Scalar, Storage<Scalar>);
    } // namespace typetraits

    template<typename Scalar_>
    class Storage {
    public:
        using Scalar                          = Scalar_;
        using Packet                          = typename typetraits::TypeInfo<Scalar>::Packet;
        static constexpr uint64_t packetWidth = typetraits::TypeInfo<Scalar>::packetWidth;
        using Pointer                         = Scalar *;
        using ConstPointer                    = const Scalar *;
        using Reference                       = Scalar &;
        using ConstReference                  = const Scalar &;
        using SizeType                        = size_t;
        using DifferenceType                  = ptrdiff_t;
        using Iterator                        = Pointer;
        using ConstIterator                   = ConstPointer;
        using ReverseIterator                 = std::reverse_iterator<Iterator>;
        using ConstReverseIterator            = std::reverse_iterator<ConstIterator>;

        Storage() = default;

        LIBRAPID_ALWAYS_INLINE explicit Storage(SizeType size);

        LIBRAPID_ALWAYS_INLINE explicit Storage(Scalar *begin, Scalar *end, bool ownsData);

        LIBRAPID_ALWAYS_INLINE Storage(SizeType size, ConstReference value);

        LIBRAPID_ALWAYS_INLINE Storage(const Storage &other);

        LIBRAPID_ALWAYS_INLINE Storage(Storage &&other) noexcept;

        template<typename V>
        LIBRAPID_ALWAYS_INLINE Storage(const std::initializer_list<V> &list);

        template<typename V>
        LIBRAPID_ALWAYS_INLINE explicit Storage(const std::vector<V> &vec);

        template<typename V>
        static Storage fromData(const std::initializer_list<V> &vec);

        template<typename V>
        static Storage fromData(const std::vector<V> &vec);

        LIBRAPID_ALWAYS_INLINE Storage &operator=(const Storage &other);

        LIBRAPID_ALWAYS_INLINE Storage &operator=(Storage &&other) noexcept;

        ~Storage();

        Storage toHostStorage() const;

        Storage toHostStorageUnsafe() const;

        Storage copy() const;

        template<typename ShapeType>
        static ShapeType defaultShape();

        LIBRAPID_ALWAYS_INLINE void resize(SizeType newSize);

        LIBRAPID_ALWAYS_INLINE void resize(SizeType newSize, int);

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE SizeType size() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReference operator[](SizeType index) const;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Reference operator[](SizeType index);

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Pointer data() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Pointer begin() noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Pointer end() noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstPointer begin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstPointer end() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator cbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator cend() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ReverseIterator rbegin() noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ReverseIterator rend() noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator rbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator rend() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator crbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator crend() const noexcept;

    private:
        template<typename P>
        LIBRAPID_ALWAYS_INLINE void initData(P begin, P end);

        template<typename P>
        LIBRAPID_ALWAYS_INLINE void initData(P begin, SizeType size);

#if defined(LIBRAPID_NATIVE_ARCH)
        alignas(LIBRAPID_MEM_ALIGN) Pointer m_begin = nullptr;
#else
        Pointer m_begin = nullptr; // Pointer to the beginning of the data
#endif

        SizeType m_size = 0;    // Number of elements in the Storage object
        bool m_ownsData = true; // Whether this Storage object owns the data it points to
    };

    template<typename Scalar_, size_t... Size_>
    class FixedStorage {
    public:
        using Scalar                   = Scalar_;
        using Pointer                  = Scalar *;
        using ConstPointer             = const Scalar *;
        using Reference                = Scalar &;
        using ConstReference           = const Scalar &;
        using SizeType                 = size_t;
        using DifferenceType           = ptrdiff_t;
        static constexpr SizeType Size = product<Size_...>();
        using Iterator                 = typename std::array<Scalar, product<Size_...>()>::iterator;
        using ConstIterator   = typename std::array<Scalar, product<Size_...>()>::const_iterator;
        using ReverseIterator = std::reverse_iterator<Iterator>;
        using ConstReverseIterator = std::reverse_iterator<ConstIterator>;

        FixedStorage();

        LIBRAPID_ALWAYS_INLINE explicit FixedStorage(const Scalar &value);

        LIBRAPID_ALWAYS_INLINE FixedStorage(const FixedStorage &other);

        LIBRAPID_ALWAYS_INLINE FixedStorage(FixedStorage &&other) noexcept;

        LIBRAPID_ALWAYS_INLINE explicit FixedStorage(const std::initializer_list<Scalar> &list);

        LIBRAPID_ALWAYS_INLINE explicit FixedStorage(const std::vector<Scalar> &vec);

        LIBRAPID_ALWAYS_INLINE FixedStorage &operator=(const FixedStorage &other) noexcept;

        LIBRAPID_ALWAYS_INLINE FixedStorage &operator=(FixedStorage &&other) noexcept = default;

        ~FixedStorage() = default;

        template<typename ShapeType>
        static ShapeType defaultShape();

        LIBRAPID_ALWAYS_INLINE void resize(SizeType newSize);

        LIBRAPID_ALWAYS_INLINE void resize(SizeType newSize, int);

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE SizeType size() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE FixedStorage copy() const;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReference operator[](SizeType index) const;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Reference operator[](SizeType index);

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Pointer data() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Iterator begin() noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE Iterator end() noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator begin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator end() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator cbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstIterator cend() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ReverseIterator rbegin() noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ReverseIterator rend() noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator rbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator rend() const noexcept;

        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator crbegin() const noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE ConstReverseIterator crend() const noexcept;

    private:
#if defined(LIBRAPID_NATIVE_ARCH) && !defined(LIBRAPID_APPLE)
        alignas(LIBRAPID_MEM_ALIGN) std::array<Scalar, Size> m_data;
#else
        // No memory alignment on Apple platforms or if it is disabled
        std::array<Scalar, Size> m_data;
#endif
    };

    // Trait implementations
    namespace typetraits {
        template<typename T>
        struct IsStorage : std::false_type {};

        template<typename Scalar>
        struct IsStorage<Storage<Scalar>> : std::true_type {};

        template<typename T>
        struct IsFixedStorage : std::false_type {};

        template<typename Scalar, size_t... Size>
        struct IsFixedStorage<FixedStorage<Scalar, Size...>> : std::true_type {};
    } // namespace typetraits

    namespace detail {
        template<typename T>
        void safeDeallocate(T *ptr, size_t size) {
            if (!ptr) return;

            auto ptr_ = LIBRAPID_ASSUME_ALIGNED(ptr);
            LIBRAPID_ASSUME(ptr_ != nullptr);
            LIBRAPID_ASSUME(size > 0);
            if constexpr (!std::is_trivially_destructible_v<T>) {
                for (size_t i = 0; i < size; ++i) { ptr_[i].~T(); }
            }

#if defined(LIBRAPID_BLAS_MKLBLAS)
            mkl_free(ptr_);
#elif defined(LIBRAPID_APPLE)
            free(ptr_);
#elif defined(LIBRAPID_MSVC) || defined(LIBRAPID_MINGW)
            _aligned_free(ptr_);
#else
            free(ptr_);
#endif
        }

        template<typename T>
        T *safeAllocate(size_t size) {
            if (size == 0) return nullptr;

            using Pointer = T *;

#if defined(LIBRAPID_BLAS_MKLBLAS)
            // MKL has its own memory allocation function
            auto ptr = static_cast<Pointer>(mkl_malloc(size * sizeof(T), 64));
#elif defined(LIBRAPID_APPLE)
            // Use posix_memalign
            void *_ptr;
            auto err = posix_memalign(&_ptr, LIBRAPID_MEM_ALIGN, size * sizeof(T));
            LIBRAPID_ASSERT(err == 0, "posix_memalign failed with error code {}", err);
            auto ptr = static_cast<Pointer>(_ptr);
#elif defined(LIBRAPID_MSVC) || defined(LIBRAPID_MINGW)
            Pointer ptr;
            try {
                ptr = static_cast<Pointer>(_aligned_malloc(size * sizeof(T), LIBRAPID_MEM_ALIGN));
            } catch (const std::exception &) {
                LIBRAPID_ASSERT(false, "Failed to allocate {} bytes of memory", size * sizeof(T));
            }
#else
            auto ptr =
              static_cast<Pointer>(std::aligned_alloc(LIBRAPID_MEM_ALIGN, size * sizeof(T)));
#endif

            LIBRAPID_ASSERT(
              ptr != nullptr, "Failed to allocate {} bytes of memory", size * sizeof(T));

            // If the type cannot be trivially constructed, we need to
            // initialize each value
            auto ptr_ = LIBRAPID_ASSUME_ALIGNED(ptr);
            LIBRAPID_ASSUME(ptr_ != nullptr);
            LIBRAPID_ASSUME(size > 0);
            if constexpr (!typetraits::TriviallyDefaultConstructible<T>::value &&
                          !std::is_array<T>::value) {
                for (Pointer p = ptr_; p != ptr_ + size; ++p) { new (p) T(); }
            }

            return ptr_;
        }

        template<typename T, typename V>
        void fastCopy(T *__restrict dst, const V *__restrict src, size_t size) {
            if (size == 0) return;
            LIBRAPID_ASSERT(dst != nullptr, "Cannot copy to nullptr");
            LIBRAPID_ASSERT(src != nullptr, "Cannot copy from nullptr");

            LIBRAPID_ASSUME(dst != nullptr);
            LIBRAPID_ASSUME(src != nullptr);

            if constexpr (std::is_same_v<T, V>) {
                if constexpr (typetraits::TriviallyDefaultConstructible<T>::value) {
                    // Use a slightly faster memcpy if the type is trivially default constructible
                    std::uninitialized_copy(src, src + size, dst);
                } else {
                    // Otherwise, use the standard copy algorithm
                    std::copy(src, src + size, dst);
                }
            } else {
                // Cannot use memcpy if the types are different
                std::copy(src, src + size, dst);
            }
        }
    } // namespace detail

    template<typename T>
    Storage<T>::Storage(SizeType size) :
            m_begin(detail::safeAllocate<T>(size)), m_size(size), m_ownsData(true) {
    }

    template<typename T>
    Storage<T>::Storage(Scalar *begin, Scalar *end, bool ownsData) :
            m_begin(begin), m_size(std::distance(begin, end)), m_ownsData(ownsData) {
    }

    template<typename T>
    Storage<T>::Storage(SizeType size, ConstReference value) :
            m_begin(detail::safeAllocate<T>(size)), m_size(size), m_ownsData(true) {
        auto ptr_ = LIBRAPID_ASSUME_ALIGNED(m_begin);
        for (SizeType i = 0; i < size; ++i) { ptr_[i] = value; }
    }

    template<typename T>
    Storage<T>::Storage(const Storage &other) : m_size(other.m_size), m_ownsData(true) {
        if (m_size == 0) return; // Quick return

        // Copy the data from `other`
        initData(other.begin(), other.end());
    }

    template<typename T>
    Storage<T>::Storage(Storage &&other) noexcept :
            m_begin(std::move(other.m_begin)), m_size(std::move(other.m_size)),
            m_ownsData(std::move(other.m_ownsData)) {
        other.m_begin    = nullptr;
        other.m_size     = 0;
        other.m_ownsData = false;
    }

    template<typename T>
    template<typename V>
    Storage<T>::Storage(const std::initializer_list<V> &list) :
            m_begin(nullptr), m_size(0), m_ownsData(true) {
        initData(static_cast<const V *>(list.begin()), static_cast<const V *>(list.end()));
    }

    template<typename T>
    template<typename V>
    Storage<T>::Storage(const std::vector<V> &vector) :
            m_begin(nullptr), m_size(0), m_ownsData(true) {
        initData(static_cast<const V *>(vector.data()), vector.size());
    }

    template<typename T>
    template<typename V>
    auto Storage<T>::fromData(const std::initializer_list<V> &list) -> Storage {
        return Storage(list);
    }

    template<typename T>
    template<typename V>
    auto Storage<T>::fromData(const std::vector<V> &vec) -> Storage {
        return Storage(vec);
    }

    template<typename T>
    auto Storage<T>::operator=(const Storage &other) -> Storage & {
        if (this != &other) {
            if (other.m_size == 0) return *this; // Quick return

            size_t oldSize = m_size;
            m_size         = other.m_size;
            if (oldSize != m_size) LIBRAPID_UNLIKELY {
                    if (m_ownsData) LIBRAPID_LIKELY {
                            // Reallocate
                            detail::safeDeallocate(m_begin, oldSize);
                            m_begin = detail::safeAllocate<Scalar>(m_size);
                        }
                    else
                        LIBRAPID_UNLIKELY {
                            // We don't own the data, so we can't reallocate
                            LIBRAPID_ASSERT(false, "Cannot copy data into dependent storage");
                        }
                }

            detail::fastCopy(m_begin, other.m_begin, m_size);
        }
        return *this;
    }

    template<typename T>
    auto Storage<T>::operator=(Storage &&other) noexcept -> Storage & {
        if (this != &other) {
            m_begin    = std::move(other.m_begin);
            m_size     = std::move(other.m_size);
            m_ownsData = std::move(other.m_ownsData);

            other.m_begin    = nullptr;
            other.m_size     = 0;
            other.m_ownsData = false;
        }
        return *this;
    }

    template<typename T>
    Storage<T>::~Storage() {
        if (m_ownsData) { detail::safeDeallocate(m_begin, m_size); }
    }

    template<typename T>
    template<typename P>
    void Storage<T>::initData(P begin, P end) {
        // Quick return in the case of empty range
        if (begin == nullptr || end == nullptr || begin == end) return;

        m_size          = static_cast<SizeType>(std::distance(begin, end));
        m_begin         = detail::safeAllocate<T>(m_size);
        m_ownsData      = true;
        auto thisBegin  = LIBRAPID_ASSUME_ALIGNED(m_begin);
        auto otherBegin = LIBRAPID_ASSUME_ALIGNED(begin);
        detail::fastCopy(thisBegin, otherBegin, m_size);
    }

    template<typename T>
    template<typename P>
    void Storage<T>::initData(P begin, SizeType size) {
        initData(begin, begin + size);
    }

    template<typename T>
    auto Storage<T>::toHostStorage() const -> Storage {
        return copy();
    }

    template<typename T>
    auto Storage<T>::toHostStorageUnsafe() const -> Storage {
        return copy();
    }

    template<typename T>
    auto Storage<T>::copy() const -> Storage {
        Storage ret;
        ret.initData(m_begin, m_size);
        return ret;
    }

    template<typename T>
    template<typename ShapeType>
    auto Storage<T>::defaultShape() -> ShapeType {
        return ShapeType({0});
    }

    template<typename T>
    auto Storage<T>::size() const noexcept -> SizeType {
        return m_size;
    }

    template<typename T>
    void Storage<T>::resize(SizeType newSize) {
        // Resize and retain data
        LIBRAPID_ASSERT(newSize > 0, "Cannot resize to a size of 0");
        if (newSize == size()) return;

        LIBRAPID_ASSERT(m_ownsData, "Dependent storage cannot be resized");

        // Copy the existing data to a new location
        Pointer oldBegin = LIBRAPID_ASSUME_ALIGNED(m_begin);
        SizeType oldSize = m_size;

        // Allocate a new block of memory
        m_begin = LIBRAPID_ASSUME_ALIGNED(detail::safeAllocate<T>(newSize));
        m_size  = newSize;

        // Copy the data
        detail::fastCopy(m_begin, oldBegin, std::min(oldSize, newSize));

        // Free the old block of memory
        detail::safeDeallocate(oldBegin, oldSize);
    }

    template<typename T>
    void Storage<T>::resize(SizeType newSize, int) {
        // Resize and discard data

        LIBRAPID_ASSERT(newSize > 0, "Cannot resize to a size of 0");

        if (size() == newSize) return;
        LIBRAPID_ASSERT(m_ownsData, "Dependent storage cannot be resized");

        Pointer oldBegin = LIBRAPID_ASSUME_ALIGNED(m_begin);
        SizeType oldSize = m_size;

        // Allocate a new block of memory
        m_begin = detail::safeAllocate<T>(newSize);
        m_size  = newSize;

        // Free the old block of memory
        detail::safeDeallocate(oldBegin, oldSize);
    }

    template<typename T>
    auto Storage<T>::operator[](Storage<T>::SizeType index) const -> ConstReference {
        LIBRAPID_ASSERT(index < size(), "Index {} out of bounds for size {}", index, size());
        return m_begin[index];
    }

    template<typename T>
    auto Storage<T>::operator[](Storage<T>::SizeType index) -> Reference {
        LIBRAPID_ASSERT(index < size(), "Index {} out of bounds for size {}", index, size());
        return m_begin[index];
    }

    template<typename T>
    auto Storage<T>::data() const noexcept -> Pointer {
        return m_begin;
    }

    template<typename T>
    auto Storage<T>::begin() noexcept -> Pointer {
        return m_begin;
    }

    template<typename T>
    auto Storage<T>::end() noexcept -> Pointer {
        return m_begin + m_size;
    }

    template<typename T>
    auto Storage<T>::begin() const noexcept -> ConstPointer {
        return m_begin;
    }

    template<typename T>
    auto Storage<T>::end() const noexcept -> ConstPointer {
        return m_begin + m_size;
    }

    template<typename T>
    auto Storage<T>::cbegin() const noexcept -> ConstIterator {
        return begin();
    }

    template<typename T>
    auto Storage<T>::cend() const noexcept -> ConstIterator {
        return end();
    }

    template<typename T>
    auto Storage<T>::rbegin() noexcept -> ReverseIterator {
        return ReverseIterator(m_begin + m_size);
    }

    template<typename T>
    auto Storage<T>::rend() noexcept -> ReverseIterator {
        return ReverseIterator(m_begin);
    }

    template<typename T>
    auto Storage<T>::rbegin() const noexcept -> ConstReverseIterator {
        return ConstReverseIterator(m_begin + m_size);
    }

    template<typename T>
    auto Storage<T>::rend() const noexcept -> ConstReverseIterator {
        return ConstReverseIterator(m_begin);
    }

    template<typename T>
    auto Storage<T>::crbegin() const noexcept -> ConstReverseIterator {
        return rbegin();
    }

    template<typename T>
    auto Storage<T>::crend() const noexcept -> ConstReverseIterator {
        return rend();
    }

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage() = default;

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage(const Scalar &value) {
        for (size_t i = 0; i < Size; ++i) { m_data[i] = value; }
    }

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage(const FixedStorage &other) = default;

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage(FixedStorage &&other) noexcept = default;

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage(const std::initializer_list<Scalar> &list) {
        LIBRAPID_ASSERT(list.size() == size(), "Initializer list size does not match storage size");
        for (size_t i = 0; i < Size; ++i) { m_data[i] = list.begin()[i]; }
    }

    template<typename T, size_t... D>
    FixedStorage<T, D...>::FixedStorage(const std::vector<Scalar> &vec) {
        LIBRAPID_ASSERT(vec.size() == size(), "Initializer list size does not match storage size");
        for (size_t i = 0; i < Size; ++i) { m_data[i] = vec[i]; }
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::operator=(const FixedStorage &other) noexcept -> FixedStorage & {
        if (this != &other) {
            for (size_t i = 0; i < Size; ++i) { m_data[i] = other.m_data[i]; }
        }
        return *this;
    }

    // template<typename T, size_t... D>
    // auto FixedStorage<T, D...>::operator=(FixedStorage &&other) noexcept
    //   -> FixedStorage & = default;

    template<typename T, size_t... D>
    template<typename ShapeType>
    auto FixedStorage<T, D...>::defaultShape() -> ShapeType {
        return ShapeType({D...});
    }

    template<typename T, size_t... D>
    void FixedStorage<T, D...>::resize(SizeType newSize) {
        LIBRAPID_ASSERT(newSize == size(), "FixedStorage cannot be resized");
    }

    template<typename T, size_t... D>
    void FixedStorage<T, D...>::resize(SizeType newSize, int) {
        LIBRAPID_ASSERT(newSize == size(), "FixedStorage cannot be resized");
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::size() const noexcept -> SizeType {
        return Size;
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::copy() const -> FixedStorage {
        return FixedStorage<T, D...>();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::operator[](SizeType index) const -> ConstReference {
        LIBRAPID_ASSERT(index < size(), "Index out of bounds");
        return m_data[index];
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::operator[](SizeType index) -> Reference {
        LIBRAPID_ASSERT(index < size(), "Index out of bounds");
        return m_data[index];
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::data() const noexcept -> Pointer {
        return const_cast<Pointer>(m_data.data());
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::begin() noexcept -> Iterator {
        return m_data.begin();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::end() noexcept -> Iterator {
        return m_data.end();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::begin() const noexcept -> ConstIterator {
        return m_data.begin();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::end() const noexcept -> ConstIterator {
        return m_data.end();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::cbegin() const noexcept -> ConstIterator {
        return begin();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::cend() const noexcept -> ConstIterator {
        return end();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::rbegin() noexcept -> ReverseIterator {
        return ReverseIterator(end());
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::rend() noexcept -> ReverseIterator {
        return ReverseIterator(begin());
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::rbegin() const noexcept -> ConstReverseIterator {
        return ConstReverseIterator(end());
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::rend() const noexcept -> ConstReverseIterator {
        return ConstReverseIterator(begin());
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::crbegin() const noexcept -> ConstReverseIterator {
        return rbegin();
    }

    template<typename T, size_t... D>
    auto FixedStorage<T, D...>::crend() const noexcept -> ConstReverseIterator {
        return rend();
    }
} // namespace librapid

#endif // LIBRAPID_ARRAY_STORAGE_HPP