The new version of the code can be reviewed in Simple multi-dimensional Array class in C++11 - follow-up.
The following code implements a simple multi-dimensional array class (hyper_array::array
).
I modeled most (if not all) the feature on the orca_array.hpp
header by Pramod Gupta after watching his talk at this year's cppcon.
I think that orca_array
is fine. However, a more generic implementation might prove interesting as well (e.g. reducing code repetition, gaining more efficiency through compile-time computations/verification and allowing more dimensions (even though the relevance of the latter feature is debatable)).
The element type and the number of dimensions is given at compile-time. The length along each dimension is specified at run-time.
As a start, there are 2 configuration options:
HYPER_ARRAY_CONFIG_Check_Bounds
: controls run-time checking of index bounds,HYPER_ARRAY_CONFIG_Overload_Stream_Operator
: enables/disables the overloading ofoperator<<(std::ostream&, const hyper_array::array&)
.
The implementation requires some C++11 features and uses very basic template (meta)programming and constexpr
computation when possible.
I have mainly the following goals:
- Self-contained (no dependency to external libraries), single-header implementation
- Minimal code repetition (if any) and clarity/"readability" of implementation
- Conforming to "good" programming practices in modern C++ while developing a solution that might prove interesting to use for others
- Clean API "that makes sense" to the user
- As much compile-time computation/evaluation/input validation as possible (template-metaprogramming,
constexpr
?) - Maximum efficiency while remaining written in standard C++11 (I made an exception once for
std::make_unique()
, but I'll probably remove it), - Allow inclusion in STL containers while still being efficient
My current concerns are:
- I'm sure that many computations could be done and
for
loops could be unwound at compile time, but I haven't wrapped my head around template metaprogramming yet to come up with an appropriate solution, - Performance,
- The API: it's still very basic, but I'm open to suggestions for making it more relevant (
orca_array
was just my starting point).
hyper_array.hp`
#pragma once// make sure that -std=c++11 or -std=c++14 ... is enabled in case of clang and gcc#if (__cplusplus < 201103L) // C++11 ? #error "hyper_array requires a C++11-capable compiler"#endif// <editor-fold desc="Configuration">#ifndef HYPER_ARRAY_CONFIG_Check_Bounds/// Enables/disables run-time validation of indices in methods like [hyper_array::array::at()](@ref hyper_array::array::at())/// This setting can be overridden by defining `HYPER_ARRAY_CONFIG_Check_Bounds` before the inclusion/// of this header or in the compiler arguments (e.g. `-DHYPER_ARRAY_CONFIG_Check_Bounds=0` in gcc and clang)#define HYPER_ARRAY_CONFIG_Check_Bounds 1#endif#ifndef HYPER_ARRAY_CONFIG_Overload_Stream_Operator/// Enables/disables `operator<<()` overloading for hyper_array::array#define HYPER_ARRAY_CONFIG_Overload_Stream_Operator 1#endif// </editor-fold>// <editor-fold desc="Includes">// std//#include <algorithm> // used during dev, replaced by compile-time equivalents in hyper_array::internal#include <array> // std::array for hyper_array::array::dimensionLengths and indexCoeffs#include <memory> // unique_ptr for hyper_array::array::_dataOwner#if HYPER_ARRAY_CONFIG_Overload_Stream_Operator#include <ostream> // ostream for the overloaded operator<<()#endif#if HYPER_ARRAY_CONFIG_Check_Bounds#include <sstream> // stringstream in hyper_array::array::validateIndexRanges()#endif#include <type_traits> // template metaprogramming stuff in hyper_array::internal// </editor-fold>/// The hyper_array lib's namespacenamespace hyper_array{// <editor-fold defaultstate="collapsed" desc="Internal Helper Blocks">/// Helper functions for hyper_array::array's implementation/// @note Everything related to this namespace is subject to change and must not be used by user codenamespace internal{ /// Checks that all the template arguments are integral types using `std::is_integral` template <typename T, typename... Ts> struct are_integral : std::integral_constant<bool, std::is_integral<T>::value&& are_integral<Ts...>::value> {}; template <typename T> struct are_integral<T> : std::is_integral<T> {}; /// Compile-time sum template <typename T> constexpr T ct_plus(const T x, const T y) { return x + y; } /// Compile-time product template <typename T> constexpr T ct_prod(const T x, const T y) { return x * y; } /// Compile-time equivalent to `std::accumulate()` template< typename T, ///< result type std::size_t N, ///< length of the array typename O ///< type of the binary operation> constexpr T ct_accumulate(const ::std::array<T, N>& arr, ///< accumulate from this array const size_t first, ///< starting from this position const size_t length, ///< accumulate this number of elements const T initialValue, ///< let this be the accumulator's initial value const O& op ///< use this binary operation ) { // https://stackoverflow.com/a/33158265/865719 return (first < (first + length)) ? op(arr[first], ct_accumulate(arr, first + 1, length - 1, initialValue, op)) : initialValue; } /// Compile-time equivalent to `std::inner_product()` template< typename T, ///< the result type typename T_1, ///< first array's type size_t N_1, ///< length of the first array typename T_2, ///< second array's type size_t N_2, ///< length of the second array typename O_SUM, ///< summation operation's type typename O_PROD ///< multiplication operation's type> constexpr T ct_inner_product(const ::std::array<T_1, N_1>& arr_1, ///< perform the inner product of this array const size_t first_1, ///< from this position const ::std::array<T_2, N_2>& arr_2, ///< with this array const size_t first_2, ///< from this position const size_t length, ///< using this many elements from both arrays const T initialValue, ///< let this be the summation's initial value const O_SUM& op_sum, ///< use this as the summation operator const O_PROD& op_prod ///< use this as the multiplication operator ) { // same logic as `ct_accumulate()` return (first_1 < (first_1 + length)) ? op_sum(op_prod(arr_1[first_1], arr_2[first_2]), ct_inner_product(arr_1, first_1 + 1, arr_2, first_2 + 1, length - 1, initialValue, op_sum, op_prod)) : initialValue; }}// </editor-fold>/// A multi-dimensional array/// Inspired by [orca_array](https://github.com/astrobiology/orca_array)template< typename ElementType, ///< elements' type size_t Dimensions ///< number of dimensions>class array{ // Types ///////////////////////////////////////////////////////////////////////////////////////public: using SizeType = size_t; ///< used for measuring sizes and lengths using IndexType = size_t; ///< used for indices // Attributes ////////////////////////////////////////////////////////////////////////////////// // <editor-fold desc="Static Attributes">public: static constexpr SizeType dimensions = Dimensions; // </editor-fold> // <editor-fold desc="Class Attributes">public: // ::std::array's are used here mainly because they are initializable // from `std::initialzer_list` and they support move semantics // cf. hyper_array::array's constructors // I might replace them with a "lighter" structure if it satisfies the above 2 requirements const ::std::array<SizeType, Dimensions> dimensionLengths; ///< number of elements in each dimension const SizeType dataLength; ///< total number of elements in [data](@ref data) const ::std::array<SizeType, Dimensions> indexCoeffs; ///< coefficients to use when computing the index ///< C_i = \prod_{j=i+1}^{n-2} L_j if i in [0, n-2] ///< | 1 if i == n-1 ///< ///< where n : Dimensions - 1 (indices start from 0) ///< | C_i : indexCoeffs[i] ///< | L_j : dimensionLengths[j] ///< @see at()private: /// handles the lifecycle of the dynamically allocated data array /// The user doesn't need to access it directly /// If the user needs access to the allocated array, they can use [data](@ref data) (constant pointer) std::unique_ptr<ElementType[]> _dataOwner;public: /// points to the allocated data array ElementType* const data; // </editor-fold> // methods /////////////////////////////////////////////////////////////////////////////////////public: /// It doesn't make sense to create an array without specifying the dimension lengths array() = delete; /// no copy-construction allowed (orca_array-like behavior) array(const array&) = delete; /// enable move construction /// allows inclusion of hyper arrays in e.g. STL containers array(array<ElementType, Dimensions>&& other) : dimensionLengths (std::move(other.dimensionLengths)) , dataLength {other.dataLength} , indexCoeffs (std::move(other.indexCoeffs)) , _dataOwner {other._dataOwner.release()} // ^_^ , data {_dataOwner.get()} {} /// the usual way for constructing hyper arrays template <typename... DimensionLengths> array(DimensionLengths&&... dimensions) : dimensionLengths{{static_cast<SizeType>(dimensions)...}} , dataLength{internal::ct_accumulate(dimensionLengths, 0, Dimensions, static_cast<SizeType>(1), internal::ct_prod<SizeType>)} , indexCoeffs([this] { ::std::array<SizeType, Dimensions> coeffs; coeffs[Dimensions - 1] = 1; for (SizeType i = 0; i < (Dimensions - 1); ++i) { coeffs[i] = internal::ct_accumulate(dimensionLengths, i + 1, Dimensions - i - 1, static_cast<SizeType>(1), internal::ct_prod<SizeType>); } return coeffs; // hopefully, NRVO should kick in here }()) #if (__cplusplus < 201402L) // C++14 ? , _dataOwner{new ElementType[dataLength]} // std::make_unique() is not part of C++11 :( #else , _dataOwner{std::make_unique<ElementType[]>(dataLength)} #endif , data{_dataOwner.get()} { // compile-time input validation // can't put them during dimensionLengths' initialization, so they're here now static_assert(sizeof...(DimensionLengths) == Dimensions,"The number of dimension lengths must be the same as ""the array's number of dimensions (i.e. \"Dimentions\")"); static_assert(internal::are_integral< typename std::remove_reference<DimensionLengths>::type...>::value,"The dimension lengths must be of integral types"); } /// Returns the length of a given dimension at run-time SizeType length(const size_t dimensionIndex) const { #if HYPER_ARRAY_CONFIG_Check_Bounds if (dimensionIndex >= Dimensions) { throw std::out_of_range("The dimension index must be within [0, Dimensions-1]"); } #endif return dimensionLengths[dimensionIndex]; } /// Compile-time version of [length()](@ref length()) template <size_t DimensionIndex> SizeType length() const { static_assert(DimensionIndex < Dimensions,"The dimension index must be within [0, Dimensions-1]"); return dimensionLengths[DimensionIndex]; } /// Returns the element at the given index tuple /// Usage: /// @code /// hyper_array::array<double, 3> arr(4, 5, 6); /// arr.at(3, 1, 4) = 3.14; /// @endcode template<typename... Indices> ElementType& at(Indices&&... indices) { return data[rawIndex(std::forward<Indices>(indices)...)]; } /// `const` version of [at()](@ref at()) template<typename... Indices> const ElementType& at(Indices&&... indices) const { return data[rawIndex(std::forward<Indices>(indices)...)]; } /// Returns the actual index of the element in the [data](@ref data) array /// Usage: /// @code /// hyper_array::array<int, 3> arr(4, 5, 6); /// assert(&arr.at(3, 1, 4) == &arr.data[arr.rawIndex(3, 1, 4)]); /// @endcode template<typename... Indices> IndexType rawIndex(Indices&&... indices) const { #if HYPER_ARRAY_CONFIG_Check_Bounds return rawIndex_noChecks(validateIndexRanges(std::forward<Indices>(indices)...)); #else return rawIndex_noChecks({static_cast<IndexType>(indices)...}); #endif }private: #if HYPER_ARRAY_CONFIG_Check_Bounds template<typename... Indices> ::std::array<IndexType, Dimensions> validateIndexRanges(Indices&&... indices) const { // compile-time input validation static_assert(sizeof...(Indices) == Dimensions,"The number of indices must be the same as ""the array's number of dimensions (i.e. \"Dimentions\")"); static_assert(internal::are_integral< typename std::remove_reference<Indices>::type...>::value,"The indices must be of integral types"); // runtime input validation ::std::array<IndexType, Dimensions> indexArray = {{static_cast<IndexType>(indices)...}}; // check all indices and prepare an exhaustive report (in oss) // if some of them are out of bounds std::ostringstream oss; for (size_t i = 0; i < Dimensions; ++i) { if ((indexArray[i] >= dimensionLengths[i]) || (indexArray[i] < 0)) { oss << "Index #"<< i << " [== "<< indexArray[i] << "]"<< " is out of the [0, "<< (dimensionLengths[i]-1) << "] range. "; } } // if nothing has been written to oss then all indices are valid if (oss.str().empty()) { return indexArray; } else { throw std::out_of_range(oss.str()); } } #endif IndexType rawIndex_noChecks(::std::array<IndexType, Dimensions>&& indexArray) const { // I_{actual} = \sum_{i=0}^{N-1} {C_i \cdot I_i} // // where I_{actual} : actual index of the data in the data array // N : Dimensions // C_i : indexCoeffs[i] // I_i : indexArray[i] return internal::ct_inner_product(indexCoeffs, 0, indexArray, 0, Dimensions, static_cast<IndexType>(0), internal::ct_plus<IndexType>, internal::ct_prod<IndexType>); }};// <editor-fold desc="orca_array-like declarations">template<typename ElementType> using array1d = array<ElementType, 1>;template<typename ElementType> using array2d = array<ElementType, 2>;template<typename ElementType> using array3d = array<ElementType, 3>;template<typename ElementType> using array4d = array<ElementType, 4>;template<typename ElementType> using array5d = array<ElementType, 5>;template<typename ElementType> using array6d = array<ElementType, 6>;template<typename ElementType> using array7d = array<ElementType, 7>;// </editor-fold>}#if HYPER_ARRAY_CONFIG_Overload_Stream_Operator/// Pretty printing to STL streams/// Should print something like/// @code/// [Dimensions:1];[dimensionLengths: 5 ];[dataLength:5];[indexCoeffs: 1 ];[data: 0 1 2 3 4 ]/// @endcodetemplate <typename T, size_t D>std::ostream& operator<<(std::ostream& out, const hyper_array::array<T, D>& ha){ out << "[Dimensions:"<< ha.dimensions << "]"; out << ";[dimensionLengths: "; for (auto& dl : ha.dimensionLengths) { out << dl << ""; } out << "]"; out << ";[dataLength:"<< ha.dataLength << "]"; out << ";[indexCoeffs: "; for (auto& ic : ha.indexCoeffs) { out << ic << ""; } out << "]"; out << ";[data: "; for (typename hyper_array::array<T, D>::IndexType i = 0; i < ha.dataLength; ++i) { out << ha.data[i] << ""; } out << "]"; return out;}#endif
Test program
// g++ -std=c++11 -std=c++11 -fdiagnostics-show-option -Wall -Wextra -Wpedantic -Werror -Wconversion hyper_array_playground.cpp -o hyper_array_playground#include <iostream>#include <vector>#include "hyper_array/hyper_array.hpp"using namespace std;int main(){ // 3d array { hyper_array::array3d<double> a{2, 3, 4}; int c = 0; for (size_t i = 0; i < a.length<0>(); ++i) // hyper_array { // should for (size_t j = 0; j < a.length<1>(); ++j) // probably { // implement for (size_t k = 0; k < a.length<2>(); ++k) // some { // kind a.at(i, j, k) = c++; // of } // iterator } // to prevent } // so much typing cout << a << endl; cout << "(a.length(1) == a.length<1>()): "<< (a.length(1) == a.length<1>()) << endl; } // 1D array { hyper_array::array1d<double> a{5}; int c = 0; for (size_t i = 0; i < a.length<0>(); ++i) { a.at(i) = c++; } cout << a << endl; } // size w.r.t. std::array { constexpr size_t elementCount = 10; hyper_array::array1d<double> aa{hyper_array::array1d<double>{elementCount}}; // 40 bytes bigger than std::array... cout << "sizeof(aa): "<< (sizeof(aa) + (elementCount*sizeof(double))) << endl; cout << "sizeof(std::array): "<< sizeof(std::array<double, elementCount>) << endl; } // in STL containers (e.g. std::vector) { vector<hyper_array::array2d<double>> v; v.emplace_back(hyper_array::array2d<double>{1,2}); v.push_back(hyper_array::array2d<double>{2,1}); } cout << "done"<< endl;}
New versions of hyper_array
can now be found on Github.