.. Copyright 2023 Julien Blanc Distributed under the Boost Software License, Version 1.0. https://www.boost.org/LICENSE_1_0.txt Design principles ================= The indexed_array type ---------------------- The library is designed around a single template type, which gave its name to the library, ``indexed_array``. It is defined as follows: .. code:: cpp template class indexed_array Its semantic is very similar to the one of the standard ``std::array``. The ``Value`` parameters determines the type of the data stored in the container, while the ``Indexer`` type is in charge of providing the following information: - the size of the container - the “type” to use for the index - a mapping between an external index and an unsigned integer number in the range ``[0 - (size-1)]``. The implementation of ``indexed_array`` is thus pretty straightforward: * an ``array`` private member * a ``at(typename Indexer::index)`` (and respective ``operator[]``) public member * all remaining interface from ``std::array`` wrapped It resides in the namespace ``indexed_array::detail``, and a smart using directive is used in the namespace ``indexed_array`` to allow using ``indexed_array::detail::indexed_array`` with the correct template arguments automagically. The default_indexer type ------------------------ Now that we have a basic generic array type, most of the work resides in providing indexers. The library provides an implementation, called ``default_indexer``, which should be suitable for most purposes, and a few helper types used to customize the ``default_indexer``. .. code:: cpp template struct default_indexer { // default implementation is empty }; In all indexers, to avoid code duplication, the ``at`` method is templated on a boolean parameter, which will tell if it needs to do bound check or not. It shall throw an ``std::out_of_range`` exception if requested to do bound checking, and the index given is invalid (``indexed_array`` won’t do bound check when accessing the inner array). Handling integers and enum the same way ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In several situations (index computations), we want to handle integral types and enum types the same way. So, one of the first thing we do is define the following helper: .. code:: cpp template struct integral_value { }; template struct integral_value::value, void> > { static inline constexpr T value = v; }; template struct integral_value::value, void> > { static inline constexpr typename std::underlying_type::type value = static_cast::type>(v); }; template static inline constexpr auto const integral_value_v = integral_value::value; This will allow us to convert enum values to their underlying integral type, both at compile and run time, while keeping integral value unchanged. This will help reduce boilerplate code. Indexing using an integral (or enum) index_range ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The first thing that is needed is a type to represent such an index (we prefix with index to avoid any confusion with the C++20 range library). The library provides one, whose definition is as simple as : .. code:: cpp template maxInclusive> struct index_range { using type = decltype(minInclusive); static inline constexpr type const min = minInclusive; static inline constexpr type const max = maxInclusive; }; Then we just have to write the following specialization (see source code for actual implementation): .. code:: cpp template struct default_indexer< index_range, typename std::enable_if_t< std::is_integral::value || std::is_enum::value, void>> { // ... }; So this works for any integral or enum type. Indexing using an integer sequence ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Such indexing provides a mapping between the value and the index in the sequence. The standard library already provides a type to represent such a sequence, it’s called ``std::integer_sequence``, so we just need to write a specialization for it. But there's a caveat: ``std::integer_sequence`` is not defined for enum types. We need thus to define our own, which we will call ``value_sequence``. The rest is pretty straightforward, but there’s a pretty cheap optimization we can do: detect if the sequence is contiguous, and in that case use an index_range-like scheme. We thus provide two specializations: .. code:: cpp template struct default_indexer< value_sequence, typename std::enable_if_t >::value, void> > { // ... }; template struct default_indexer< value_sequence, typename std::enable_if_t >::value, void> > { // ... }; This require the definition of the ``is_contiguous_sequence`` helper. A design choice has been done here, a sequence is considered a valid contiguous sequence even if it contains duplicate values, as long as they are grouped together. This choice has been made to support aliases in enum values (see below, indexing with a ``describe``-d enum). Which means that we have: .. code:: cpp static_assert(is_contiguous_sequence>::value); static_assert(is_contiguous_sequence>::value); static_assert(! is_contiguous_sequence>::value); static_assert(! is_contiguous_sequence>::value); Indexing using a described enum ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``describe`` library is a compile time reflection library designed by Peter Dimov, and is part of ``boost``. The ``indexed_library`` provides first class integration for enums who have compile time introspection using this library. To implement that, the required part is to convert a ``describe_enumerators`` to a ``value_sequence``. After that is done, we can simply use the ``default_indexer`` for integer sequences. We benefit automatically from the optimization for contiguous sequences (supporting this use case was the main motivation for this optimization in the first place). Doing this conversion is pretty easy, and integrating enums is as simple as: .. code:: cpp template struct describe_to_integer_sequence { }; template typename L, typename... Args> struct describe_to_integer_sequence > { using type = value_sequence; }; template struct default_indexer::value, void> > { using helper_list_type = typename describe_to_value_sequence >::type; using index = Enum; static inline constexpr auto const size = default_indexer::size; template static constexpr auto at(Enum v) noexcept(!throws_on_error) { return default_indexer::template at(v); } }; Adding support of other compile-time enum introspection mechanisms has proven to be relatively simple as well. Going multidimensional ---------------------- Adapting indexed_array ~~~~~~~~~~~~~~~~~~~~~~ Now that we have a container that can be arbitrarily indexed, it makes a lot of sense to have some indexes, not in the form of just one value, but in the form of a set of values. As long as these values can be safely converted into an integer in the range ``[0 - (size -1)]``, everything is fine. This can be done easily by defining the following indexer: .. code:: cpp struct custom_multidimensional_index { using index = mp11::mp_list; inline static constexpr size_t const size = 8; template static constexpr auto at(int v1, Foo v2) { auto res = v1 * 4 + static_cast(v2) + 1; if constexpr(c) { if(res < 0 || static_cast(res) >= size) throw std::out_of_range("Invalid index"); } return res; } }; The problem resides with the definition of the ``at()`` and ``operator[]`` methods. They can’t take an ``Indexer::index`` anymore. We need to split that into separate arguments, so that we can write: .. code:: cpp auto value = my_multidim_array.at(2, Foo::bar); auto value2 = my_multidim_array[{1, Foo::bar}]; // notice the {}, won't be needed in C++23 To allow this, @TODO rewrite this part, implementation changed Adapting default_indexer ~~~~~~~~~~~~~~~~~~~~~~~~ The default indexer now needs to be adapted, so that we can write: .. code:: cpp indexed_array, my_custom_index, Foo> my_data; What we need is just a bit of machinery to transform that into: .. code:: cpp indexed_array>, my_custom_index, default_indexer >>> And then specialize default_indexer when taking an mp_list of indexers: .. code:: cpp template struct default_indexer, typename std::enable_if_t...>::value, void> > { using index = boost::mp11::mp_list; static inline constexpr auto const size = product_v; template static constexpr auto at(typename Args::index... args) noexcept { return at_computation_helper::template at(args...); } }; ``product_v`` is a compile time helper that will gives the product of all given values. ``at`` function is implemented in a quite classical way: .. code:: cpp template struct at_computation_helper { template static constexpr auto at(typename Arg::index idx, typename Args::index... rem) { return Arg::template at(idx) * product_v + at_computation_helper::template at(rem...); } }; template struct at_computation_helper { template static constexpr auto at(typename Arg::index idx) { return Arg::template at(idx); } }; Safe initialization ------------------- The ``safe_arg`` template class is used, during initialization, to assert that: - the correct number of arguments is given for the array (not less, not more) - the order of the arguments matches the one of the indexer So, we got two assertions to check: - ``at(idx) == `` (for each item in the list) - ``Indexer::size == `` The first assertion is checked recursively for each argument: .. code:: cpp template constexpr bool correct_index_() { static_assert(X == at_helper::at(), "Invalid value for initializer"); return X == at_helper::at() && correct_index_(); } A ``typename u`` is used here, instead of a direct value. This allow us to support multidimensional indexers. ``at_helper`` is here for that: call the indexer with the correct arguments. And the terminal case is pretty straightforward. This one also checks the number of arguments: .. code:: cpp template constexpr bool correct_index_() { static_assert(!(X < Indexer::size), "Not enough initializers provided"); static_assert(!(X > Indexer::size), "Too many initializers provided"); return X == Indexer::size; } Now we just have to add the constructor in ``indexed_array``: .. code:: cpp // safe_arg constructor template < typename... Args, std::enable_if_t > >::value, bool> = true> constexpr indexed_array(Args&&... args) : data_{static_cast(args)...} { static_assert(detail::correct_index(), "Argument mismatch"); }