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 RawPointer           = Scalar *;
        using ConstRawPointer      = const Scalar *;
        using Pointer              = std::shared_ptr<Scalar>;
        using ConstPointer         = std::shared_ptr<const Scalar>;
        using Reference            = Scalar &;
        using ConstReference       = const Scalar &;
        using SizeType             = size_t;
        using DifferenceType       = ptrdiff_t;
        using Iterator             = RawPointer;
        using ConstIterator        = ConstRawPointer;
        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) LIBRAPID_RELEASE_NOEXCEPT;

        ~Storage();

        void set(const Storage &other);

        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 RawPointer begin() noexcept;
        LIBRAPID_NODISCARD LIBRAPID_ALWAYS_INLINE RawPointer 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:
        template<typename P>
        LIBRAPID_ALWAYS_INLINE void initData(P begin, P end);

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

        LIBRAPID_ALWAYS_INLINE void resizeImpl(SizeType newSize, int);

        LIBRAPID_ALWAYS_INLINE void resizeImpl(SizeType newSize);

#if defined(LIBRAPID_NATIVE_ARCH) && !defined(LIBRAPID_APPLE)
        alignas(LIBRAPID_DEFAULT_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);

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

        ~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_DEFAULT_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 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);
#else
#   if defined(LIBRAPID_NATIVE_ARCH) && defined(LIBRAPID_MSVC)
            _aligned_free(ptr);
#   else
            free(ptr);
#   endif
#endif
        }

        template<typename T>
        std::shared_ptr<T> safeAllocate(size_t size) {
            using RawPointer = T *;
            using Pointer    = std::shared_ptr<T>;

#if defined(LIBRAPID_BLAS_MKLBLAS)
            // MKL has its own memory allocation function
            auto ptr = static_cast<RawPointer>(mkl_malloc(size * sizeof(T), 64));
#else
#   if defined(LIBRAPID_NATIVE_ARCH)
            // Force aligned memory
#       if defined(LIBRAPID_APPLE)
            // No memory allignment. It breaks everything for some reason
            auto ptr = static_cast<RawPointer>(std::malloc(size * sizeof(T)));
#       elif defined(LIBRAPID_MSVC) || defined(LIBRAPID_MINGW)
            auto ptr =
              static_cast<RawPointer>(_aligned_malloc(size * sizeof(T), global::memoryAlignment));
#       else
            auto ptr = static_cast<RawPointer>(
              std::aligned_alloc(global::memoryAlignment, size * sizeof(T)));
#       endif
#   else
            // No memory alignment
            auto ptr = static_cast<RawPointer>(std::malloc(size * sizeof(T)));
#   endif
#endif

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

            return Pointer(ptr, [size](RawPointer ptr) { safeDeallocate(ptr, size); });
        }

        template<typename T>
        std::shared_ptr<T> safePointerCopy(T *ptr, size_t size, bool ownsData) {
            using RawPointer = T *;
            using Pointer    = std::shared_ptr<T>;

            if (ownsData) {
                return Pointer(ptr, [size](RawPointer ptr) { safeDeallocate(ptr, size); });
            } else {
                return Pointer(ptr, [](RawPointer) {});
            }
        }

        template<typename T>
        std::shared_ptr<T> safePointerCopy(const std::shared_ptr<T> &ptr, size_t size,
                                           bool ownsData = true) {
            using RawPointer = T *;
            using Pointer    = std::shared_ptr<T>;

            if (ownsData) {
                return Pointer(ptr.get(), [size](RawPointer ptr) { safeDeallocate(ptr, size); });
            } else {
                return Pointer(ptr.get(), [](RawPointer) {});
            }
        }
    } // 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(detail::safePointerCopy(begin, std::distance(begin, end), ownsData)),
            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) {
        for (SizeType i = 0; i < size; ++i) { m_begin.get()[i] = value; }
    }

    template<typename T>
    Storage<T>::Storage(const Storage &other) :
            m_begin(other.m_begin), m_size(other.m_size), m_ownsData(other.m_ownsData) {}

    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(list.begin(), 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(vector.begin(), vector.end());
    }

    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>
    Storage<T> &Storage<T>::operator=(const Storage &other) {
        if (this != &other) {
            if (m_ownsData) {
                // If we own the data already, we can just copy the pointer since we know it won't
                // affect anything else. The shared pointer deals with the reference counting, so
                // we don't need to worry about other arrays that might be using the same data.
                m_begin = other.m_begin;
                m_size  = other.m_size;
            } else {
                LIBRAPID_ASSERT(m_size == other.m_size,
                                "Cannot copy storage with {} elements to dependent storage with "
                                "{} elements",
                                other.m_size,
                                m_size);

                // If we don't own the data, the size must be the same since it is being used
                // elsewhere, and we can't change it

                if (typetraits::TriviallyDefaultConstructible<T>::value) {
                    // Use a slightly faster memcpy if the type is trivially default constructible
                    std::uninitialized_copy(other.begin(), other.end(), m_begin.get());
                } else {
                    // Otherwise, use the standard copy algorithm
                    std::copy(other.begin(), other.end(), m_begin.get());
                }
            }
        }
        return *this;
    }

    template<typename T>
    Storage<T> &Storage<T>::operator=(Storage &&other) LIBRAPID_RELEASE_NOEXCEPT {
        if (this != &other) {
            if (m_ownsData) {
                std::swap(m_begin, other.m_begin);
                std::swap(m_size, other.m_size);
                m_ownsData = other.m_ownsData;
            } else {
                LIBRAPID_ASSERT(
                  size() == other.size(),
                  "Mismatched storage sizes. Cannot assign storage with {} elements to "
                  "dependent storage with {} elements",
                  other.size(),
                  size());

                if (typetraits::TriviallyDefaultConstructible<T>::value) {
                    // Use a slightly faster memcpy if the type is trivially default constructible
                    std::uninitialized_copy(other.begin(), other.end(), m_begin.get());
                } else {
                    // Otherwise, use the standard copy algorithm
                    std::copy(other.begin(), other.end(), m_begin.get());
                }
            }
        }
        return *this;
    }

    template<typename T>
    Storage<T>::~Storage() {
        // All deallocation is handled by the shared pointer, which has a custom deleter which
        // depends on whether the data is owned by the storage object or not. If it is owned, the
        // data is deallocated, otherwise it is left alone.
    }

    template<typename T>
    template<typename P>
    void Storage<T>::initData(P begin, P end) {
        m_size  = static_cast<SizeType>(std::distance(begin, end));
        m_begin = detail::safeAllocate<T>(m_size);

        if constexpr (typetraits::TypeInfo<T>::canMemcpy) {
            if constexpr (typetraits::TriviallyDefaultConstructible<T>::value) {
                // Use a slightly faster memcpy if the type is trivially default constructible
                std::uninitialized_copy(begin, end, m_begin.get());
            } else {
                // Otherwise, use the standard copy algorithm
                std::copy(begin, end, m_begin.get());
            }
        } else {
            // Since we can't memcpy, we have to copy each element individually
            for (SizeType i = 0; i < m_size; ++i) { m_begin.get()[i] = begin[i]; }
        }
    }

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

    template<typename T>
    void Storage<T>::set(const Storage<T> &other) {
        // We can simply copy the shared pointers across
        m_begin    = other.m_begin;
        m_size     = other.m_size;
        m_ownsData = other.m_ownsData;
    }

    template<typename T>
    auto Storage<T>::copy() const -> Storage {
        Storage ret;
        ret.initData(m_begin.get(), 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) {
        resizeImpl(newSize);
    }

    template<typename T>
    void Storage<T>::resize(SizeType newSize, int) {
        resizeImpl(newSize, 0);
    }

    template<typename T>
    LIBRAPID_ALWAYS_INLINE void Storage<T>::resizeImpl(SizeType newSize) {
        // Resize and retain data

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

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

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

        // Copy the data across
        if constexpr (typetraits::TriviallyDefaultConstructible<T>::value) {
            // Use a slightly faster memcpy if the type is trivially default constructible
            std::uninitialized_copy(
              oldBegin.get(), oldBegin.get() + ::librapid::min(oldSize, newSize), m_begin.get());
        } else {
            // Otherwise, use the standard copy algorithm
            std::copy(
              oldBegin.get(), oldBegin.get() + ::librapid::min(oldSize, newSize), m_begin.get());
        }
    }

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

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

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

    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.get()[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.get()[index];
    }

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

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

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

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

    template<typename T>
    auto Storage<T>::end() const noexcept -> ConstIterator {
        return m_begin.get() + 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.get() + m_size);
    }

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

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

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

    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) -> 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