blob: 73ba7ccb4f8916df5cd3186e438d875e3aae72c9 [file]
#ifndef _RIVE_LAZY_VECTOR_HPP_
#define _RIVE_LAZY_VECTOR_HPP_
#include <algorithm>
#include <cstddef>
#include <utility>
#include <vector>
namespace rive
{
// 8 B owning pointer to a heap-allocated std::vector<T>, allocated on first
// mutation. Saves 16 B per object compared to an inline std::vector<T> (24 B
// header) for fields that are empty most of the time.
//
// Use when you have a per-object list that's empty for the majority of
// instances (typical fits: dependents/observers/optional adornments). Do NOT
// use when the vector is usually populated — the extra heap allocation makes
// it strictly worse than a regular vector for that case.
//
// All read operations are safe on an empty (never-allocated) wrapper. The
// hot-path null check is a single load + branch; reads return iterators into
// a shared static empty vector when the wrapper has no backing storage.
template <typename T> class LazyVector
{
public:
using value_type = T;
using const_iterator = typename std::vector<T>::const_iterator;
LazyVector() = default;
~LazyVector() { delete m_v; }
// Deep copy. Preserves the "copying the owner clones its list" semantic
// that a plain std::vector member would have provided. An empty source
// (m_v == nullptr) copies as an empty wrapper — no allocation. This
// keeps Component / Drawable / etc. copyable (Core::clone path).
LazyVector(const LazyVector& other) :
m_v(other.m_v != nullptr ? new std::vector<T>(*other.m_v) : nullptr)
{}
LazyVector& operator=(const LazyVector& other)
{
if (this != &other)
{
delete m_v;
m_v =
other.m_v != nullptr ? new std::vector<T>(*other.m_v) : nullptr;
}
return *this;
}
LazyVector(LazyVector&& other) noexcept : m_v(other.m_v)
{
other.m_v = nullptr;
}
LazyVector& operator=(LazyVector&& other) noexcept
{
if (this != &other)
{
delete m_v;
m_v = other.m_v;
other.m_v = nullptr;
}
return *this;
}
bool empty() const { return m_v == nullptr || m_v->empty(); }
std::size_t size() const { return m_v == nullptr ? 0 : m_v->size(); }
// Append unconditionally. Allows duplicates (use pushUnique to dedup).
void push_back(T value)
{
if (m_v == nullptr)
{
m_v = new std::vector<T>();
}
m_v->push_back(std::move(value));
}
// Append only if `value` is not already present. Centralizes the
// find-then-push idiom used by callers that maintain set semantics.
void pushUnique(T value)
{
if (m_v != nullptr &&
std::find(m_v->begin(), m_v->end(), value) != m_v->end())
{
return;
}
push_back(std::move(value));
}
// Erase every element equal to `value`. No-op when wrapper is empty.
// Uses the erase-remove idiom — safe but removes ALL duplicates if any.
void eraseAll(const T& value)
{
if (m_v == nullptr)
{
return;
}
m_v->erase(std::remove(m_v->begin(), m_v->end(), value), m_v->end());
}
void clear()
{
if (m_v != nullptr)
{
m_v->clear();
}
}
// Const iteration always works, even on a never-allocated wrapper —
// returns iterators into a shared static empty vector when m_v is null.
// Inlined null check (no view() indirection) so the compiler can keep
// m_v in a register across both begin/end loads in a range-for.
const_iterator begin() const
{
return m_v ? m_v->begin() : emptyView().begin();
}
const_iterator end() const { return m_v ? m_v->end() : emptyView().end(); }
// Returns a stable const reference suitable as the return value of
// accessor methods. Always valid — falls back to a shared empty when
// the wrapper has no backing storage.
const std::vector<T>& view() const { return m_v ? *m_v : emptyView(); }
private:
std::vector<T>* m_v = nullptr;
static const std::vector<T>& emptyView()
{
static const std::vector<T> EMPTY;
return EMPTY;
}
};
} // namespace rive
#endif