This involves some quite tricky changes in the way types are composed to form an iterator-pipeline. Some wrappers are added as adaptors or for additional safety-checks, and to provide a builder-API. Unfortunately, when building a new `IterExplorer` iterator pipeline from an existing pipeline naively, composing all those types will add several unecessary intermediary wrapper-layers. Worse even, the handling of `BaseAdapter` prevents the new tuple-zipping iterator actually to pass-through any `expandChildren()` call. These issues are a consequence of using templated types, instead of fixed types with an interface; we can not just determine if some wrapper is present — unless the wrapper itself ''helps by exposing a tag.'' Even while I must admit that the whole packaging and adaptation machinery of `IterExplorer` looks dangerously complex already, using dedicated type tags for this single purpose seems like a tenable soulution.
585 lines
18 KiB
C++
585 lines
18 KiB
C++
/*
|
||
PATH-ARRAY.hpp - sequence of path-like component-IDs in fixed storage
|
||
|
||
Copyright (C)
|
||
2017, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
*/
|
||
|
||
|
||
/** @file path-array.hpp
|
||
** Foundation abstraction to implement path-like component sequences.
|
||
** This library class can be used to build a path abstraction for data structure access
|
||
** or some similar topological coordinate system, like e.g. [UI coordinates](\ref ui-coord.hpp)
|
||
** A PathArray is an iterable sequence of literal component IDs, implemented as tuple of lib::Literal
|
||
** held in fixed inline storage with possible heap allocated (and thus unlimited) extension storage.
|
||
** It offers range checks, standard iteration and array-like indexed component access; as a whole
|
||
** it is copyable, while actual components are immutable after construction. PathArray can be
|
||
** bulk initialised from an explicit sequence of literals; it is normalised on construction
|
||
** to trim and fill interspersed missing elements. Iteration and equality comparison are
|
||
** built on top of the normalisation; iteration starts with the first nonempty element.
|
||
**
|
||
** @remark the choice of implementation layout is arbitrary and not based on evidence.
|
||
** A delegate structure with fixed inline storage looked like an interesting programming challenge.
|
||
** Using just a heap based array storage would have been viable likewise.
|
||
** @todo when UICoord is in widespread use, collect performance statistics and revisit this design.
|
||
**
|
||
** @see PathArray_test
|
||
** @see UICoord_test
|
||
** @see stage::interact::UICoord
|
||
** @see view-spec-dsl.hpp
|
||
*/
|
||
|
||
|
||
#ifndef LIB_PATH_ARRAY_H
|
||
#define LIB_PATH_ARRAY_H
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/symbol.hpp"
|
||
#include "lib/iter-adapter.hpp"
|
||
#include "lib/meta/variadic-helper.hpp"
|
||
#include "lib/format-obj.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <utility>
|
||
#include <string>
|
||
#include <memory>
|
||
#include <array>
|
||
|
||
|
||
namespace lib {
|
||
namespace error = lumiera::error;
|
||
|
||
using std::string;
|
||
using std::forward;
|
||
using lib::Literal;
|
||
using util::unConst;
|
||
using util::isnil;
|
||
|
||
namespace con { // Implementation helper: flexible heap based extension storage....
|
||
|
||
/**
|
||
* Heap-allocated extension storage for an immutable sequence of literal strings.
|
||
* The size of the allocation is determined and fixed once, at construction time,
|
||
* derived from the number of initialisers. The first slot within the allocation
|
||
* stores this length. Extension can be _empty_ (default constructed),
|
||
* in which case no heap allocation is performed.
|
||
*/
|
||
class Extension
|
||
{
|
||
using PStorage = Literal*;
|
||
|
||
PStorage storage_;
|
||
|
||
|
||
static size_t&
|
||
size (PStorage& p)
|
||
{
|
||
REQUIRE (p);
|
||
return reinterpret_cast<size_t&> (p[0]);
|
||
}
|
||
|
||
/** allocate a copy.
|
||
* @note caller has to manage the allocated memory
|
||
* @warning call to Literal's ctor deliberately elided
|
||
*/
|
||
PStorage
|
||
newCopy() const
|
||
{
|
||
size_t siz = 1 + size (unConst(this)->storage_);
|
||
const char** alloc = new const char*[siz];
|
||
std::copy (storage_, storage_+siz, alloc);
|
||
return reinterpret_cast<PStorage> (alloc);
|
||
}
|
||
|
||
|
||
public:
|
||
~Extension()
|
||
{
|
||
if (storage_)
|
||
delete[] storage_;
|
||
}
|
||
|
||
Extension() noexcept
|
||
: storage_{nullptr}
|
||
{ }
|
||
|
||
template<typename...ELMS>
|
||
explicit
|
||
Extension (ELMS&& ...elms)
|
||
: storage_{new Literal[1 + sizeof...(ELMS)]} // proper alignment maintained here (TICKET #1204)
|
||
{
|
||
size(storage_) = sizeof...(ELMS);
|
||
new(storage_+1) Literal[sizeof...(ELMS)] {forward<ELMS>(elms)...};
|
||
}
|
||
|
||
Extension (Extension const& r)
|
||
: storage_{r.storage_? r.newCopy() : nullptr}
|
||
{ }
|
||
|
||
Extension (Extension&& rr) noexcept
|
||
: storage_{nullptr}
|
||
{
|
||
if (rr.storage_)
|
||
std::swap (storage_, rr.storage_);
|
||
}
|
||
|
||
Extension& operator= (Extension o)
|
||
{
|
||
std::swap (storage_, o.storage_);
|
||
return *this;
|
||
}
|
||
|
||
|
||
|
||
operator bool() const { return not empty(); }
|
||
bool empty() const { return not storage_;}
|
||
|
||
size_t
|
||
size() const
|
||
{
|
||
return storage_? size(unConst(this)->storage_)
|
||
: 0;
|
||
}
|
||
|
||
Literal const&
|
||
operator[] (size_t idx) const
|
||
{
|
||
REQUIRE (storage_ and idx < size());
|
||
return storage_[1+idx];
|
||
}
|
||
|
||
size_t
|
||
indexOf (Literal const* pos) const
|
||
{
|
||
REQUIRE (isValid (pos));
|
||
return pos - (storage_+1);
|
||
}
|
||
|
||
|
||
bool
|
||
isValid (Literal const* pos) const
|
||
{
|
||
return storage_
|
||
and storage_ < pos
|
||
and pos < storage_ + (1 + size (unConst(this)->storage_));
|
||
}
|
||
|
||
void
|
||
resizeTo (size_t cnt)
|
||
{
|
||
if (cnt > size())
|
||
{ // need more storage
|
||
if (storage_)
|
||
{ // copy content to new expanded storage
|
||
auto target = new const char* [cnt+1];
|
||
auto pos = std::copy (storage_, storage_+1+size(), target);
|
||
for ( ; pos < target+1+cnt; ++pos)
|
||
*pos = nullptr;
|
||
delete[] storage_;
|
||
storage_ = reinterpret_cast<PStorage> (target);
|
||
}
|
||
else // allocate and init empty
|
||
storage_ = new Literal [cnt+1];
|
||
|
||
size (storage_) = cnt;
|
||
return;
|
||
}
|
||
if (not storage_) return;
|
||
if (cnt == 0)
|
||
{
|
||
delete[] storage_;
|
||
storage_ = nullptr;
|
||
}
|
||
else
|
||
size(storage_) = cnt;
|
||
// excess elements now unreachable
|
||
// note: delete[] knows original size
|
||
}
|
||
};
|
||
}//(End)Implementation helper
|
||
|
||
|
||
|
||
using meta::pickArg;
|
||
using meta::pickInit;
|
||
using meta::IndexSeq;
|
||
|
||
/**
|
||
* Abstraction for path-like topological coordinates.
|
||
* A sequence of Literal strings, with array-like access and
|
||
* standard iteration. Implemented as fixed size inline tuple
|
||
* with heap allocated unlimited extension space.
|
||
* @note contents are normalised on initialisation
|
||
* - leading empty elements are filled with Symbol::EMPTY
|
||
* - empty elements in the middle are replaced by "*"
|
||
* - trailing empty elements and "*" are trimmed
|
||
*/
|
||
template<size_t chunk_size>
|
||
class PathArray
|
||
{
|
||
static_assert (0 < chunk_size, "PathArray chunk_size must be nonempty");
|
||
|
||
using CcP = const char*;
|
||
using LiteralArray = std::array<Literal, chunk_size>;
|
||
|
||
LiteralArray elms_;
|
||
con::Extension tail_;
|
||
|
||
/**
|
||
* @internal delegate ctor to place the initialiser arguments appropriately
|
||
* @remarks the two index sequences passed by pattern match determine which
|
||
* arguments go to the inline array, and which go to heap allocated extension.
|
||
* The inline array has fixed size an is thus filled with trailing `NULL` ptrs,
|
||
* which is achieved with the help of meta::pickInit(). The con::Extension
|
||
* is an embedded smart-ptr, which, when receiving additional tail arguments,
|
||
* will place and manage them within a heap allocated array.
|
||
*/
|
||
template<size_t...prefix, size_t...rest, typename...ARGS>
|
||
PathArray (IndexSeq<prefix...>
|
||
,IndexSeq<rest...>
|
||
,ARGS&& ...args)
|
||
: elms_{pickInit<prefix,CcP> (forward<ARGS>(args)...) ...}
|
||
, tail_{pickArg<rest> (forward<ARGS>(args)...) ...}
|
||
{
|
||
this->normalise();
|
||
}
|
||
|
||
/**
|
||
* @internal rebinding helper for building sequences of index numbers,
|
||
* to route the initialiser arguments into the corresponding storage
|
||
* - the leading (`chunk_size`) arguments go into the LiteralArray inline
|
||
* - all the remaining arguments go into heap allocated extension storage
|
||
*/
|
||
template<typename...ARGS>
|
||
struct Split
|
||
{
|
||
using Prefix = typename meta::BuildIndexSeq<chunk_size>::Ascending;
|
||
using Rest = typename meta::BuildIdxIter<ARGS...>::template After<chunk_size>;
|
||
};
|
||
|
||
public:
|
||
template<typename...ARGS>
|
||
explicit
|
||
PathArray (ARGS&& ...args)
|
||
: PathArray(typename Split<ARGS...>::Prefix()
|
||
,typename Split<ARGS...>::Rest()
|
||
,forward<ARGS> (args)...)
|
||
{ }
|
||
|
||
PathArray(PathArray&&) = default;
|
||
PathArray(PathArray const&) = default;
|
||
PathArray(PathArray& o) : PathArray((PathArray const&)o) { }
|
||
PathArray& operator= (PathArray const&) = default;
|
||
PathArray& operator= (PathArray &&) = default;
|
||
////////////////////////TICKET #963 Forwarding shadows copy operations
|
||
|
||
|
||
size_t
|
||
size() const
|
||
{
|
||
return tail_? chunk_size + tail_.size()
|
||
: findInlineEnd() - elms_.begin();
|
||
}
|
||
|
||
size_t
|
||
leafLevel() const
|
||
{
|
||
return empty()? 0 : size()-1;
|
||
}
|
||
|
||
bool
|
||
empty() const
|
||
{
|
||
return not elms_[0]; // normalise() ensures nonnull unless completely empty
|
||
}
|
||
|
||
/** joins nonempty content, separated by slash */
|
||
operator string() const;
|
||
|
||
|
||
/** Array style indexed access.
|
||
* @throws error::Invalid on bound violation
|
||
* @return reference to the normalised content
|
||
* @note returns Symbol::EMPTY for leading empty elements,
|
||
* even while iteration will skip such entries.
|
||
*/
|
||
Literal const&
|
||
operator[] (size_t idx) const
|
||
{
|
||
Literal* elm = unConst(this)->getPosition (idx);
|
||
if (not elm)
|
||
throw error::Invalid ("Accessing index "+util::toString(idx)
|
||
+" on PathArray of size "+ util::toString(size())
|
||
,error::LUMIERA_ERROR_INDEX_BOUNDS);
|
||
return *elm;
|
||
}
|
||
|
||
/** reverse lookup of actual path content
|
||
* @param content reference to actual content _residing within the path_
|
||
* @return index position of the content within the path
|
||
* @throws error::Invalid when the given storage location
|
||
* is outside the data content storage of this path
|
||
*/
|
||
size_t
|
||
indexOf (Literal const& content) const
|
||
{
|
||
if (elms_.begin() <= &content and &content < elms_.end())
|
||
return &content - elms_.begin();
|
||
if (tail_.isValid (&content))
|
||
return chunk_size + tail_.indexOf (&content);
|
||
|
||
throw error::Invalid ("Referred content "+util::toString(&content)
|
||
+" is not located within the storage of PathArray "
|
||
+string(*this));
|
||
}
|
||
|
||
|
||
protected: /* ==== Iteration control API for IterAdapter ==== */
|
||
|
||
/** Implementation of Iteration-logic: pull next element. */
|
||
friend void
|
||
iterNext (const PathArray*, const Literal*& pos)
|
||
{
|
||
++pos;
|
||
}
|
||
|
||
/** Implementation of Iteration-logic: detect iteration end. */
|
||
friend bool
|
||
checkPoint (const PathArray* src, const Literal*& pos)
|
||
{
|
||
REQUIRE (src);
|
||
if (pos == src->elms_.end() and src->tail_)
|
||
pos = &src->tail_[0];
|
||
else
|
||
if (not src->isValid (pos))
|
||
{
|
||
pos = nullptr;
|
||
return false;
|
||
}
|
||
ENSURE ( (src->elms_.begin() <= pos and pos < src->elms_.end())
|
||
or src->tail_.isValid(pos));
|
||
return true;
|
||
}
|
||
|
||
public:
|
||
using const_iterator = lib::IterAdapter<Literal const*, PathArray const*>;
|
||
using iterator = const_iterator;
|
||
|
||
using value_type = Literal;
|
||
using reference = Literal&;
|
||
using const_reference = Literal const&;
|
||
|
||
/** @remark iteration is defined to begin with real content */
|
||
iterator begin() const { return firstNonempty(); }
|
||
iterator end() const { return iterator{}; }
|
||
|
||
friend iterator begin(PathArray const& pa) { return pa.begin();}
|
||
friend iterator end (PathArray const& pa) { return pa.end(); }
|
||
|
||
|
||
|
||
private: /* ===== implementation details ===== */
|
||
|
||
bool
|
||
isValid (Literal const* pos) const
|
||
{
|
||
return pos
|
||
and (tail_.isValid(pos)
|
||
or (elms_.begin() <= pos and pos < elms_.end()
|
||
and *pos));
|
||
}
|
||
|
||
iterator
|
||
firstNonempty () const
|
||
{
|
||
iterator startPos{this, elms_.begin()};
|
||
while (startPos && isnil (*startPos))
|
||
++startPos;
|
||
return startPos;
|
||
}
|
||
|
||
/**
|
||
* find _effective end_ of data in the inline array,
|
||
* i.e. the position _behind_ the last usable content
|
||
*/
|
||
Literal const*
|
||
findInlineEnd() const
|
||
{
|
||
Literal const* lastPos = elms_.begin() + chunk_size-1;
|
||
Literal const* beforeStart = elms_.begin() - 1;
|
||
while (lastPos != beforeStart and not *lastPos)
|
||
--lastPos;
|
||
return ++lastPos; // at start if empty, else one behind the last
|
||
}
|
||
|
||
|
||
/* ===== manipulation by UICoord and Builder subclasses ===== */
|
||
protected:
|
||
/**
|
||
* @internal access content element by index
|
||
* @return pointer to storage, `null` if out of bounds
|
||
*/
|
||
Literal*
|
||
getPosition (size_t idx)
|
||
{
|
||
Literal const* elm =nullptr;
|
||
if (idx < chunk_size)
|
||
elm = elms_[idx]? &elms_[idx] : nullptr;
|
||
else
|
||
if (idx-chunk_size < tail_.size())
|
||
elm = &tail_[idx-chunk_size];
|
||
|
||
return const_cast<Literal*> (elm);
|
||
}
|
||
|
||
/**
|
||
* @internal ensure storage for the indicated position exists
|
||
* @return pointer to the storage, either existing or reallocated
|
||
*/
|
||
Literal*
|
||
expandPosition (size_t idx)
|
||
{
|
||
if (chunk_size <= idx and size() <= idx)
|
||
tail_.resizeTo(idx+1 - chunk_size);
|
||
if (idx < chunk_size)
|
||
return elms_.begin() + idx;
|
||
|
||
ENSURE (idx-chunk_size < tail_.size());
|
||
return const_cast<Literal*> (&tail_[idx-chunk_size]);
|
||
}
|
||
|
||
/** @internal force new content into the given entry */
|
||
void
|
||
setContent (Literal* pos, const char* val)
|
||
{
|
||
REQUIRE (pos);
|
||
*reinterpret_cast<const char**> (pos) = val;
|
||
}
|
||
|
||
void
|
||
truncateTo (size_t newSize)
|
||
{
|
||
if (newSize < chunk_size)
|
||
{
|
||
Literal* end = elms_.end();
|
||
Literal* pos = getPosition(newSize);
|
||
if (pos)
|
||
for ( ; pos!=end; ++pos)
|
||
setContent (pos, nullptr);
|
||
tail_.resizeTo (0);
|
||
}
|
||
else
|
||
if (newSize-chunk_size < tail_.size())
|
||
tail_.resizeTo (newSize - chunk_size);
|
||
// otherwise: keep current size, don't grow
|
||
}
|
||
|
||
/**
|
||
* establish the _contract_ of PathArray
|
||
* - no null pointer within the valid storage range
|
||
* - storage is precisely trimmed at the end
|
||
* - missing leading elements are filled with Symbol::EMPTY
|
||
* - missing inner elements are interpolated as "*"
|
||
*/
|
||
void
|
||
normalise()
|
||
{
|
||
if (size() == 0) return;
|
||
const char* fill = Symbol::EMPTY;
|
||
|
||
Literal* end = elms_.end();
|
||
Literal* pos = elms_.begin();
|
||
for ( ; pos!=end; ++pos)
|
||
if (isnil (*pos))
|
||
setContent (pos, fill);
|
||
else
|
||
if (fill==Symbol::EMPTY)
|
||
fill = Symbol::ANY;
|
||
|
||
if (tail_)
|
||
{
|
||
// normalise data in extension storage
|
||
pos = getPosition (chunk_size);
|
||
end = pos + tail_.size();
|
||
for ( ; pos!=end; ++pos)
|
||
if (isnil (*pos))
|
||
setContent (pos, fill);
|
||
else
|
||
if (fill==Symbol::EMPTY)
|
||
fill = Symbol::ANY;
|
||
}
|
||
|
||
size_t idx = size();
|
||
// trim trailing fill
|
||
while (idx and fill == *getPosition (--idx))
|
||
setContent (getPosition(idx), nullptr);
|
||
|
||
if (idx >= chunk_size-1)
|
||
tail_.resizeTo (idx+1 - chunk_size);
|
||
else
|
||
tail_.resizeTo (0);
|
||
}
|
||
};
|
||
|
||
|
||
|
||
|
||
template<size_t chunk_size>
|
||
inline
|
||
PathArray<chunk_size>::operator string() const
|
||
{
|
||
if (this->empty()) return "";
|
||
|
||
string buff;
|
||
size_t expectedLen = this->size() * 10;
|
||
buff.reserve (expectedLen); // heuristic
|
||
for (Literal elm : *this)
|
||
buff += elm + "/";
|
||
|
||
// chop off last delimiter
|
||
size_t len = buff.length();
|
||
ASSERT (len >= 1);
|
||
buff.resize(len-1);
|
||
return buff;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Equality comparison of arbitrary PathArray objects
|
||
*/
|
||
template<size_t cl, size_t cr>
|
||
bool
|
||
operator== (PathArray<cl> const& l, PathArray<cr> const& r)
|
||
{
|
||
if (l.size() != r.size()) return false;
|
||
|
||
typename PathArray<cl>::iterator lp = l.begin();
|
||
typename PathArray<cl>::iterator rp = r.begin();
|
||
while (lp and rp)
|
||
{
|
||
if (*lp != *rp) return false;
|
||
++lp;
|
||
++rp;
|
||
}
|
||
return isnil(lp) and isnil(rp);
|
||
}
|
||
|
||
template<size_t cl, size_t cr>
|
||
bool
|
||
operator!= (PathArray<cl> const& l, PathArray<cr> const& r)
|
||
{
|
||
return not (l == r);
|
||
}
|
||
|
||
|
||
|
||
}// namespace lib
|
||
#endif /*LIB_PATH_ARRAY_H*/
|