0

I'm trying to implement a constructor for a C++ vector class template that is both efficient and convenient to use. The latter is, of course, somewhat subjective — I'm aiming at something like Vec2D myVec = Vec2D({1.0, 2.0}).

To start with, I'm thinking about a class template for fixed-length vectors, so no immediate use for std::vector I'd say. With template <typename T, unsigned short n>, two options to store the elements of the vector would be T mElements[n] or std::array<T, n> mElements. I would go with the latter (same storage and some added benefits compared to the former).

Now, on to the constructor (and the question) — what should be its parameter? The following options come to mind:

  • Using std::array<T, n> initElements would require the use of double curved brackets for initialisation as it is an aggregate, i.e. Vec2D myVec = Vec2D({{1.0, 2.0}}). Omitting the outer curly brackets might still compile, though results in a warning. Additionally, if we were to generalise this to a 2D array, e.g. for a matrix class template, it would require quadruple curved brackets (or triple when omitting the outer pair again, taking a warning for granted). Not so convenient.
  • Using T initElems[] would require e.g. double dElems[2] = {1.0, 2.0} followed by Vec2D myVec = Vec2D(dElems), it is not possible to directly pass {1.0, 2.0} as argument. Not so convenient.
  • Using std::initializer_list<T>, which would allow Vec2D myVec{1.0, 2.0}. This also nicely generalises to a 2D array. However, I don't see how one would use this as a constructor when overloading operators, say operator +.
  • Using std::vector<T>. This allows the use of Vec2D myVec = Vec2D({1.0, 2.0}), nicely generalises to 2D arrays, and is easy to use in overloaded operators. However, it does not seem very efficient.

The (intentionally basic) code below reflects the last option. Are there alternatives which are more efficient, without losing convenience?

template <typename T, unsigned short n>
class Vector {
    public:
    std::array<T, n> mElements;

    // Constructor
    Vector(std::vector<T> initElements) {
        for (unsigned short k = 0; k < n; k++) {
            mElements[k] = initElements[k];
        }
    }

    // Overloaded operator +
    Vector operator + (const Vector& rightVector) const {
        std::vector<T> sumVec(n);
        for (unsigned short k = 0; k < n; k++) {
            sumVec[k] = mElements[k] + rightVector.mElements[k];
        }
        return Vector(sumVec);
    }
};

With the usage

using Vec2D = Vector<double, 2>;
Vec2D myVec = Vec2D({1.0, 2.0}); 
Ailurus
  • 731
  • 1
  • 10
  • 23
  • 2
    what about `T&&...` – Bernd Oct 26 '20 at 11:50
  • What exactly is the problem with initializer list and overloading + that you mention? – jrok Oct 26 '20 at 12:19
  • 1
    `std::initializer_list` has no conflict with operator overloading: `some_vec + {1.0, 2.0}` is not allowed in any case. just choose `std::initializer_list` (or `T(&&)[N]` for fixed length). `Vector{1.0, 2.0}` and `Vector{1.0, 2.0} + Vector{3.0, 4.0}` is elegant enough. – RedFog Oct 26 '20 at 12:23
  • O.T.: I can spend yet another idea for your vector class: (Not) [Defining static instance of a class](https://stackoverflow.com/a/64167388/7478597). ;-) – Scheff's Cat Oct 26 '20 at 12:36
  • @jrok I assume I'd need something to temporarily store the results of `mElements[k] + rightVector.mElements[k]` before passing the overall result to the constructor. I don't have much experience with `std::initializer_list`, but it does not seem intended for storing data. How would this work? – Ailurus Oct 26 '20 at 13:04
  • @RedFog This `&&` (apparently called *rvalue reference*) is new to me, so this question is already paying off! As for using `std::initializer_list`, I'm not quite sure how to use it to (temporarily) store data in an overloaded operator (see also my reply to @jrok). Would you consider writing up your comment as an answer? – Ailurus Oct 27 '20 at 14:23
  • It seems like you're looking for a statically sized `std::valarray`, is this the case? Also, I have to agree with @RedFog, it seems like `T(&&)[N]`, would be the ideal type to use here. –  Nov 08 '20 at 19:40

3 Answers3

1

The most convienient way to make this would be to use a deduction guide, changing your given usage example:

using Vec2D = Vector<double, 2>;
Vec2D myVec = Vec2D({1.0, 2.0});

into something much simpler:

Vector<double, 2> myVec = { 1.0, 2.0 };

// or even
// Vector myVec = { 1.0, 2.0 };

Enabling a usage similar to std::array.

Arguably the easiest, and most efficient way to create a statically sized vector that allows this would be to use a non-standard C++ __attriubute__.

Templating the T __attribute__((vector_size(N))), we end up with the following:

using ushort = unsigned short;

template <typename T, ushort N>
using Vector = T __attribute__((
    vector_size(sizeof(T) * N) //  the number of bytes in a single `T`, multiplied by the number of elements, `N`
));

int main() {
    using vector_t = Vector<double const, 2>;

    vector_t x = { 1.0, 2.0 };

    vector_t y = { 9.0, 3.0 };

    vector_t z = x + y;

    std::wcout << z[0] << ", " << z[1] << '\n'; // "10, 5\n"
}

Okay, okay, I'm joking, don't do that, let's not touch upon the world of attributes, compiler extensions, and nonportable code.

The simplest way to make it as convenient to use as the underlying std::array, would be to either, create a type deduction guide, inherit from std::array, or to not implement a constructor, the latter two allow the std::array constructor to take effect, as the class has no constructor on its own.

I think, in this case, you might be able to simply inherit from std::array, without pissing off every C++ developer on SO.

Something along the lines of:

#include <iostream>
#include <vector>
#include <array>

using u16 = unsigned short const;

template <typename T, u16 N>
struct Vector : std::array<T, N> {
    Vector<T, N> operator + (Vector<T, N> & rightVector) {
        decltype(auto) self = *this;
        Vector<T, N> sumVec;

        for ( ushort i = 0; i < N; ++i ) {
            sumVec[i] = self[i] + rightVector[i];
        }

        return sumVec;
    }

    Vector operator += (Vector const& rightVector) {
        decltype(auto) self = *this;

        for ( ushort i = 0; i < N; ++i ) {
            self[i] += rightVector[i];
        }

        return this;
    }

    Vector operator ++ () {
        decltype(auto) self = *this;

        for ( T& item : self ) {
            ++item;
        }

        return this;
    }

    Vector operator ++ (int const) {
        decltype(auto) self = *this;

        Vector temporary = self;

        ++self;

        return temporary;
    }


    Vector operator - (Vector const& rightVector) const {
        decltype(auto) self = *this;

        Vector<T, N> sumVec;

        for ( ushort i = 0; i < N; ++i ) {
            sumVec[i] = self[i] - rightVector[i];
        }

        return Vector(sumVec);
    }
};

int main() {
    using vector_t = Vector<double, 2>;

    vector_t x = { 1.0, 2.0 };
    vector_t y = { 9.0, 3.0 };
    vector_t z = x + y; // { 10.0. 4.0 }

    std::wcout << z[0] << ", " << z[1] << '\n';
}

Although, you might want to use a few std::enable_ifs or assertions to make sure that you only create Vectors of a numerical type, as that seems to be what you want to use.

This should have the same memory usage as a std::array. It doesn't initialize any extra types, in contrast to your example that had constructed std::vectors everywhere.

Does this fit your intended usage and goals?

  • Right, that would work, but strictly spoken one would need to use `vector_t x = {{ 1.0, 2.0 }}` and so on (i.e. double curly brackets instead of single ones), see the first item of the list comparing options in my question. Not a disaster for a 1D `std:array`, but as mentioned, this does not nicely generalise to a 2D `std::array`, as this would need quadruple (or triple) curly brackets. – Ailurus Nov 09 '20 at 18:53
  • What do you mean? Can you elaborate on how you want it to generalize into a 2D array? It shouldn't need triple or quadruple brackets for 2D arrays. –  Nov 09 '20 at 18:55
  • In addition, what is the rationale behind defining `decltype(auto) self = *this;` rather than using `*this` directly? – Ailurus Nov 09 '20 at 18:56
  • 1
    @Ailurus about the self, technicially, without optimization, the `this` pointer would be dereferenced multiple times. Of course, the code can be written plenty of different ways, how you would want to use it is up to you. Change it to fit your style appropriately. –  Nov 09 '20 at 18:58
  • Let's say I'd like to define a matrix class template using 2D storage, e.g. `std::array< std::array, m >` for a matrix with `m` rows and `n` columns. Initialising such a thing is not so nice as it requires triple curly brackets (strictly spoken even quadruple ones), see e.g. [this question](https://stackoverflow.com/questions/12844475/why-cant-simple-initialize-with-braces-2d-stdarray) – Ailurus Nov 09 '20 at 19:02
  • I see what you're referring to now, I think I might be able to find a way around that. I'll look into how std::tuples and pairs handle that. Although, I believe that this works fine with `std::initializer_list`s. –  Nov 09 '20 at 21:26
  • Using `std::initializer_list` as constructor parameter would work, though I'm not sure how to (efficiently) combine this with overloading operators (see original comments). An answer based on this approach (and/or using `T(&&)[N]`) would be great. – Ailurus Nov 12 '20 at 16:40
  • 1
    @Ailurus `T(&&)[N]` definitely won't work, because we would end up with `T(&&)[N](&&)[N]` and now we have references of references, so you'll just get errors. You would somehow need to get the template to allow `T(&&)[N]`, `T(&&)[N][N]`, or `T(&&)[N][N][N]`, which I doubt is possible. Unless you only want a 2D array, and no further / no less. –  Nov 12 '20 at 16:55
1

You could also make use of parameter packs, which (combined with some nice polymorphism) can enable you to do stuff like this:

Vector<int, 3> v1{std::vector<int>{1, 2, 3}};
Vector<double, 3> v2 = {5., 6., 7.};
Vector<float, 3> v3 = v1 + v2;

Where Vector is defined as follows:

#include <vector>
#include <array>
#include <algorithm>
#include <cassert>

template <typename T, size_t n>
struct Vector : std::array<T, n>
{
    /* Default constructor, needed as recursion endpoint */
    Vector() = default;

    /* Recursive constructor that takes an arbitrary number of arguments
     * Dangerous: only adds the last n arguments */
    template <typename... Ts>
    Vector(T v, Ts... args) : Vector(args...) { addItem(v); };

    /* Vector constructor that takes an arbitrary std::vector v
     * Dangerous: only adds the first n items */
    template <typename T2>
    explicit Vector(const std::vector<T2>& v) : added{std::min(v.size(), n)}
    {
        assert(v.size() == n);
        for (size_t i = 0; i < added; i++)
        {
            (*this)[i] = (T)v[i];
        }
    }

    /* Copy constructor: takes a Vector of any type, but of the same length */
    template <typename T2>
    Vector(const std::array<T2, n>& v) : added{n}
    {
        for (size_t i = 0; i < added; i++)
        {
            (*this)[i] = (T)v[i];
        }
    }

    /* Example of a polymorphic addition function */
    template <typename T2>
    Vector<T, n> operator+(const Vector<T2, n>& v)
    {
        Vector<T, n> vr{*this};
        for (size_t i = 0; i < n; i++)
        {
            vr[i] += (T)v[i];
        }
        return vr;
    }

private:
    size_t added{0};

    /* Needed for recursive constructor */
    void addItem(const T& t)
    {
        added++;
        if (added <= n) { (*this)[n - added] = t; }
        else { assert(false); }
    }
};
T. J. Evers
  • 391
  • 1
  • 13
  • Interesting approach! These [variadic templates](https://en.wikipedia.org/wiki/Variadic_template) look useful (though the ellipsis operator `...` looks a bit strange) – Ailurus Nov 12 '20 at 16:49
0

You could directly use the parameters to initialise the base class

Untested code.

template <typename... Args>
Vector(Args...&& args) : mEVector(std::forward<Args>(args)...) { };
Surt
  • 15,501
  • 3
  • 23
  • 39