13
\$\begingroup\$

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 Arrays 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 to std::array itself?
  • General guidelines, best practices?

Try It Online

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 of decltype(auto) (all methods return by value) (thanks to Incomputable)
  • Universal reference for ActionT + perfect forwarding of ActionT (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 of Array. 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 a size_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 around std::make_tupple.
  • An extra alignment template argument A is added to Array. Furthermore, some extra explicit constructors are added to support converting between Array 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 Arrays of at most one element. I personally see no use case for a std::array< T, 0 > or std::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 an Array from a given Array containing elements of a different type. This enables the construction of an Array< Array< T, N1 >, N2 > by replicating a single Array< T, N1 > using the newly added constructor.
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Great. Now make a github repo and share it with the world. \$\endgroup\$ Commented Apr 30, 2018 at 18:30
  • \$\begingroup\$ @Pheo: Github: just the sample | Github: larger project using the sample ;) \$\endgroup\$
    – Matthias
    Commented Apr 30, 2018 at 18:46
  • \$\begingroup\$ Um... Dang. That's a big project. \$\endgroup\$ Commented Apr 30, 2018 at 18:52
  • 1
    \$\begingroup\$ @Matthias, I recommend waiting a little bit before accepting an answer. People feel discouraged to post their own if one is already accepted. One or two days is usually a good time period. \$\endgroup\$ Commented Apr 30, 2018 at 19:39
  • 1
    \$\begingroup\$ And there's no problem whatsoever explaining that in a follow-up question. Might want to link back to this one for bonus context and comparison :-) \$\endgroup\$
    – Mast
    Commented Sep 2, 2018 at 8:44

2 Answers 2

11
\$\begingroup\$

Encouraging ineffective/wrong usage

May be for your usage case it is important, but casts are usually an indication that something could be improved or fixed. static_casts are usually implicit. dynamic_casts are somewhat arguable, but there should be some better solution, albeit harder to find. const_casts are outright wrong (there is a case when non-const member function calls const version and then const_casts the constness away). reinterpret_casts are usually done in a more explicit manner. Having them hidden somewhere is asking for trouble.

decltype(auto)

I argued with @Quuxplusone about the feature. I argued that it is safe to use in general. I was wrong. The feature requires a great deal of care to be used correctly. This case is borderline dangerous. Never use decltype(auto) where value needs to be returned. Don't use it unless reference is expected and the referenceness needs to be preserved.

Accept by forwarding reference

Some people also call it generalized reference. It is particularly applicable to ActionT. IIRC the new wording says that the temporaries do not need to fully "materialize" if they will not outlive the scope. Transform function could take input array by forwarding reference too, as not all actions might operate on const array elements.

Inheriting constructors

It would be better to use "using declarations" (thanks to @BenSteffan) to inherit constructors and duplicate them as part of Array.

Curiously recurring templates

In my opinion, rather than adding every possible utility member function, one should write x_getter<T>, which SFINAE's correctly based on T. That will reduce the interface bloat greatly and allows people to choose.

Here is an example of x_getter I mentioned in the comments on a stub class:

#include <array>
#include <iostream>

template <typename T>
struct x_getter
{
    friend T;

    double x() const
    {
        return (*static_cast<const T*>(this))[0];
    }

    void x(double new_x)
    {
        (*static_cast<T*>(this))[0] = new_x;
    }

private:
    x_getter() = default; //prevents accidental creation
};

struct coordinates : std::array<double, 3>, x_getter<coordinates>
{
    using std::array<double, 3>::array;
};


int main()
{
    coordinates origin{};
    std::cout << origin.x();
    origin.x(12.7);
    std::cout << ' ' << origin.x() << '\n';
}

Demo.

It actually took me a trip to stackoverflow to get it to work, but now I understand why it works. static_cast is the right cast, because it will cast downwards the inheritance tree, and the only child that can hold the getter is T itself, thus no dynamic_cast is needed. It would be great though to smoothen out the ugly casts. I believe it also has a disadvantage in aggregates, but I believe it is not applicable in this case.

Now you can write several of these facades, and let users choose which ones they want. This will make mixing and matching easier for users. Also, [[no_unique_address]] in C++20 will make it as efficient as hand written code (currently they take up a little bit of space inside of the object, in C++20 empty base optimization will hopefully be performed using the tags).

Naming convention

The convention is rather unusual, and more C# style. Also, names could follow more standard library style to be grasped immediately. For example, TransformArray could be renamed to transform_construct, and be placed into utils namespace.

Tuple utilities

std::array is considered to be tuple by standard library. All of the helpers for std::tuple work the same way for std::array in terms of template metaprogramming. Thus the utilities are not too useful.

\$\endgroup\$
8
  • \$\begingroup\$ "expression (statement?)" -> Actually neither, this is a using-declaration (please excuse the pedantic nit-picking). \$\endgroup\$ Commented Apr 30, 2018 at 19:26
  • \$\begingroup\$ @BenSteffan, thanks, and you're welcome to nit-pick. \$\endgroup\$ Commented Apr 30, 2018 at 19:27
  • 1
    \$\begingroup\$ @Matthias, as I mentioned, when you want to return a value, don't use decltype(auto). auto will do quite well. decltype(auto) is useful in cases where you want to preserve referenceness, otherwise I would stay away from it, to not return a reference to temporary accidentally (compiler will probably emit a warning, but still it shouldn't be allowed). \$\endgroup\$ Commented Apr 30, 2018 at 19:31
  • 1
    \$\begingroup\$ @Matthias, 4) I just thought that you got tired of duplicating all of the member functions. 5) I'll update the post with example, but it will take time 6) That is why I usually place naming conventions downwards, as long as they're consistently applied it is ok 7) I meant that std::tuple_element, std::tuple_size work the same way on tuples and arrays, and everything else is handled by your other functions. \$\endgroup\$ Commented Apr 30, 2018 at 19:42
  • 1
    \$\begingroup\$ @Matthias, you’re right about fourth point. Only constructor inheritance is useful. I’ll update the post tomorrow. I’m going to sleep, but I’ll answer your questions tomorrow, so you can pile them up :) \$\endgroup\$ Commented Apr 30, 2018 at 20:23
2
\$\begingroup\$

Would it be better to have clearly labeled make_array functions for each kind of construction?

auto a1 = make_repeated<int,15>(42);
auto a2 = array_from_tuple (t);

Can we count on the compiler to elide the prvalue and construct the declared variable in-place without any copying? If so, you can effectively write named constructors.

\$\endgroup\$
5
  • \$\begingroup\$ My actual use case is to let multiple classes inherit from Array. This is a possible alternative at the cost of always calling the right labeled functions in all child class constructors. These child class constructors, however, cannot be replaced (1) since I rely on explicit/implicit constructors depending on the conversions I want to support and (2) since I still want initializations via a constructor (e.g. RGB rgb(1.0f, 0.0f, 1.0f);). Unfortunately, I use the base Array as well for which I still like the same kind of initializations via a constructor. \$\endgroup\$
    – Matthias
    Commented May 1, 2018 at 13:50
  • \$\begingroup\$ The labeled functions, however, are more expressive, since at the moment I am not able to integrate the replication of a single value (confusing in my current setup). \$\endgroup\$
    – Matthias
    Commented May 1, 2018 at 13:52
  • \$\begingroup\$ Replicate single value: hint: index_sequence, comma operator, pack expansion in initializer list. \$\endgroup\$
    – JDługosz
    Commented May 1, 2018 at 18:38
  • \$\begingroup\$ I already have that method (see array utilities above). It just seems confusing to add it in a non-labeled constructor. But at a second thought, I can make it an explicit constructor. \$\endgroup\$
    – Matthias
    Commented May 2, 2018 at 6:14
  • \$\begingroup\$ In C++17, the return value optimization is mandated by the standard, so we can safely rely on it :) \$\endgroup\$
    – L. F.
    Commented Aug 25, 2019 at 23:10

Not the answer you're looking for? Browse other questions tagged or ask your own question.