I implemented a std::array
wrapper which primarily adds various constructors, since std::array
has no explicit constructors itself, but rather uses aggregate initialization.
I like to have some feedback on my code which heavily depends on template meta-programming. More particularly:
- Are there still cases where I can exploit move semantics or where I will unnecessarily copy large values (which can become a problem for large array elements)?
- Are there still cases where I can use more stringent conditions for enabling methods (i.e. SFINAE)? (e.g. type deduction/decaying of tuple elements).
- Are there elegant strategies for supporting
Array
s containing only one element (or even no elements at all)? (Note the potential conflicts with the copy and move constructor.Array
needs to be capable of handling pointer elements as well in the presence of inheritance. Furthermore,Array
will act as a base class in my code base.)? - Is it possible to chain
Array
constructors instead of always immediately redirect tostd::array
itself? - General guidelines, best practices?
Includes:
#include <array>
#include <iostream>
#include <tuple>
#include <utility>
Array Utilities:
namespace details {
template< typename ActionT, typename FromT, size_t...I >
constexpr decltype(auto) TransformArray(ActionT action,
const std::array< FromT, sizeof...(I) >& a,
std::index_sequence< I... >) {
using ToT = decltype(std::declval< ActionT >()(std::declval< FromT >()));
return std::array< ToT, sizeof...(I) >{ action(a[I])... };
}
template< typename T, size_t...I >
constexpr decltype(auto) FillArray(T value, std::index_sequence< I... >) {
return std::array< T, sizeof...(I) >{ (static_cast< void >(I), value)... };
}
template< size_t ToN, typename T, size_t...I >
constexpr decltype(auto) EnlargeArray(const std::array< T, sizeof...(I) >& a,
std::index_sequence< I... >) {
return std::array< T, ToN >{ a[I]... };
}
template< typename T, typename TupleT, std::size_t... I >
constexpr decltype(auto) TuppleToArray(const TupleT& t,
std::index_sequence< I... >) {
return std::array< T, sizeof...(I) >{ std::get< I >(t)... };
}
}
template< typename ActionT, typename FromT, size_t N >
constexpr decltype(auto) TransformArray(ActionT action,
const std::array< FromT, N >& a) {
return details::TransformArray(std::move(action), a,
std::make_index_sequence< N >());
}
template< typename ToT, typename FromT, size_t N >
constexpr decltype(auto) StaticCastArray(const std::array< FromT, N >& a) {
constexpr auto f = [](const FromT& v) {
return static_cast< ToT >(v);
};
return TransformArray(f, a);
}
template< typename ToT, typename FromT, size_t N >
constexpr decltype(auto) DynamicCastArray(const std::array< FromT, N >& a) {
constexpr auto f = [](const FromT& v) {
return dynamic_cast< ToT >(v);
};
return TransformArray(f, a);
}
template< typename ToT, typename FromT, size_t N >
constexpr decltype(auto) ConstCastArray(const std::array< FromT, N >& a) {
constexpr auto f = [](const FromT& v) {
return const_cast< ToT >(v);
};
return TransformArray(f, a);
}
template< typename ToT, typename FromT, size_t N >
constexpr decltype(auto) ReinterpretCastArray(const std::array< FromT, N >& a) {
constexpr auto f = [](const FromT& v) {
return reinterpret_cast< ToT >(v);
};
return TransformArray(f, a);
}
template< typename T, size_t N >
constexpr decltype(auto) FillArray(T value) {
return details::FillArray(value, std::make_index_sequence< N >());
}
template< size_t ToN, typename T, size_t FromN >
constexpr decltype(auto) EnlargeArray(const std::array< T, FromN >& a) {
return details::EnlargeArray< ToN >(a, std::make_index_sequence< FromN >());
}
template< typename T, typename... Ts >
constexpr decltype(auto) TuppleToArray(const std::tuple< T, Ts... >& t) {
constexpr auto N = sizeof...(Ts) + 1u;
return details::TuppleToArray< T >(t, std::make_index_sequence< N >());
}
Tuple Utilities:
namespace details {
template< typename T, size_t...I >
constexpr decltype(auto) ArrayToTupple(const std::array< T, sizeof...(I) >& a,
std::index_sequence< I... >) noexcept {
return std::make_tuple(a[I]...);
}
}
template< typename T, size_t N >
constexpr decltype(auto) ArrayToTupple(const std::array< T, N >& a) noexcept {
return details::ArrayToTupple(a, std::make_index_sequence< N >());
}
template< typename... ArgsT >
constexpr decltype(auto) ArgsToTuple(ArgsT&&... args) noexcept {
return std::make_tuple(std::forward< ArgsT >(args)...);
}
Array wrapper:
template< typename T, size_t N,
typename = std::enable_if_t< (N > 1) > >
struct Array : std::array< T, N > {
constexpr Array() noexcept
: std::array< T, N >{} {}
template< typename... ArgsT,
typename = std::enable_if_t< (N == sizeof...(ArgsT)) > >
constexpr Array(ArgsT&&... args) noexcept
: std::array< T, N >{ std::forward< ArgsT >(args)... } {}
template< size_t FromN,
typename = std::enable_if_t< (FromN < N) > >
constexpr Array(const Array< T, FromN >& a) noexcept
: std::array< T, N >(EnlargeArray< N >(a)) {}
template< size_t FromN, typename... ArgsT,
typename = std::enable_if_t< (FromN < N && (FromN + sizeof...(ArgsT)) == N) > >
constexpr Array(const Array< T, FromN >& a, ArgsT&&... args) noexcept
: std::array< T, N >(TuppleToArray(
std::tuple_cat(ArrayToTupple(a), ArgsToTuple(std::forward< ArgsT >(args)...)))) {}
constexpr Array(const Array& a) noexcept = default;
constexpr Array(Array&& a) noexcept = default;
template< typename U >
constexpr explicit Array(const Array< U, N >& a) noexcept
: std::array< T, N >(StaticCastArray< T >(a)) {}
~Array() = default;
constexpr Array& operator=(const Array& a) noexcept = default;
constexpr Array& operator=(Array&& a) noexcept = default;
// It would be nice to have properties in C++ (supported in msvc++ and Clang).
constexpr std::enable_if_t< ( 1 <= N ), T& > GetX() noexcept {
return std::array< T, N >::operator[](0);
}
constexpr std::enable_if_t< ( 2 <= N ), T& > GetY() noexcept {
return std::array< T, N >::operator[](1);
}
constexpr std::enable_if_t< ( 3 <= N ), T& > GetZ() noexcept {
return std::array< T, N >::operator[](2);
}
constexpr std::enable_if_t< ( 4 <= N ), T& > GetW() noexcept {
return std::array< T, N >::operator[](3);
}
constexpr std::enable_if_t< ( 1 <= N ), const T& > GetX() const noexcept {
return std::array< T, N >::operator[](0);
}
constexpr std::enable_if_t< ( 2 <= N ), const T& > GetY() const noexcept {
return std::array< T, N >::operator[](1);
}
constexpr std::enable_if_t< ( 3 <= N ), const T& > GetZ() const noexcept {
return std::array< T, N >::operator[](2);
}
constexpr std::enable_if_t< ( 4 <= N ), const T& > GetW() const noexcept {
return std::array< T, N >::operator[](3);
}
};
Some extra utilities for illustration purposes:
template< typename T, std::size_t N >
std::ostream& operator<<(std::ostream& os, const std::array< T, N >& a) {
for (auto i : a) { os << i << ' '; }
return os << '\n';
}
int main() {
constexpr Array< float, 5 > a;
std::cout << a;
constexpr Array< float, 5 > b( 1.5f, 2.5f, 3.5f, 4.5f, 5.5f );
std::cout << b;
constexpr Array< float, 5 > c{ 1.5f, 2.5f, 3.5f, 4.5f, 5.5f };
std::cout << c;
constexpr Array< float, 6 > d(c);
std::cout << d;
constexpr Array< float, 6 > e(c, 6.5f);
std::cout << e;
constexpr Array< int, 6 > f(e);
std::cout << f;
return 0;
}
Edit 1: Try It Online
auto
instead ofdecltype(auto)
(all methods return by value) (thanks to Incomputable)- Universal reference for
ActionT
+ perfect forwarding ofActionT
(thanks to Incomputable)
Edit 2: Try It Online
- All accessor/getter member methods are removed from
Array
, since they are not applicable to all possible derived classes ofArray
. A RGB color spectrum does not have X,Y,Z components, whereas a XYZ color spectrum does. A UVW normalized 3D texture position has its W component at index 2, whereas a 4D homogeneous position has its W component at index 3. Derived classes can implement these accessor/getter member methods themselves or inherit from pure abstract classes (as mentioned and illustrated by Incomputable). Furthermore, it is possible to add asize_t Index
template argument to specify the right index. Depending on the size of T, one can also return by value for small values, and by const reference for large values. Depending on the type of T, one can use different call conventions as well (e.g.__vectorcall
,__fastcall
, etc.). ArgsToTupple
is removed, since it is just a wrapper aroundstd::make_tupple
.- An extra alignment template argument
A
is added toArray
. Furthermore, some extraexplicit
constructors are added to support converting betweenArray
instances with a different alignment.
Edit 3: Try It Online
- Added a constructor for replicating a single value. This makes sense in the absence of
Array
s of at most one element. I personally see no use case for astd::array< T, 0 >
orstd::array< T, 1 >
either, given that the number of elements needs to be known at compile time. - Added a
std::is_convertible_v
type trait to construct anArray
from a givenArray
containing elements of a different type. This enables the construction of anArray< Array< T, N1 >, N2 >
by replicating a singleArray< T, N1 >
using the newly added constructor.