0
votes

Implémentation d'un constructeur efficace et pratique pour un modèle de classe vectorielle C ++

J'essaie d'implémenter un constructeur pour un modèle de classe vectorielle C ++ à la fois efficace et pratique à utiliser. Ce dernier est, bien sûr, quelque peu subjectif - je vise quelque chose comme Vec2D myVec = Vec2D({1.0, 2.0}) .

Pour commencer, je pense à un modèle de classe pour les vecteurs de longueur fixe, donc pas d'utilisation immédiate pour std::vector je dirais. Avec template <typename T, unsigned short n> , deux options pour stocker les éléments du vecteur seraient T mElements[n] ou std::array<T, n> mElements . J'irais avec ce dernier (même stockage et quelques avantages supplémentaires par rapport au premier).

Passons maintenant au constructeur (et à la question) - quel devrait être son paramètre? Les options suivantes me viennent à l'esprit:

  • L'utilisation de std::array<T, n> initElements nécessiterait l'utilisation de doubles crochets courbes pour l'initialisation car c'est un agrégat, c'est-à-dire Vec2D myVec = Vec2D({{1.0, 2.0}}) . L'omission des accolades externes peut toujours être compilée, mais entraîne un avertissement. De plus, si nous devions généraliser cela à un tableau 2D, par exemple pour un modèle de classe de matrice, il faudrait quatre crochets courbes (ou triples en omettant à nouveau la paire externe, en prenant un avertissement pour acquis). Pas si pratique.
  • Utiliser T initElems[] nécessiterait par exemple un double dElems[2] = {1.0, 2.0} suivi de Vec2D myVec = Vec2D(dElems) , il n'est pas possible de passer directement {1.0, 2.0} comme argument. Pas si pratique.
  • Utilisation de std::initializer_list<T> , qui autoriserait Vec2D myVec{1.0, 2.0} . Cela se généralise également bien à un tableau 2D. Cependant, je ne vois pas comment on utiliserait cela comme constructeur lors de la surcharge d'opérateurs, disons operator + .
  • Utilisation de std::vector<T> . Cela permet l'utilisation de Vec2D myVec = Vec2D({1.0, 2.0}) , se généralise bien aux tableaux 2D et est facile à utiliser dans les opérateurs surchargés. Cependant, cela ne semble pas très efficace.

Le code (intentionnellement basique) ci-dessous reflète la dernière option. Existe-t-il des alternatives plus efficaces, sans perdre en commodité?

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

Avec l'utilisation

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);
    }
};


7 commentaires

qu'en est-il de T&&...


Quel est exactement le problème avec la liste d'initialisation et la surcharge + que vous mentionnez?


std::initializer_list n'a pas de conflit avec la surcharge d'opérateurs: some_vec + {1.0, 2.0} n'est en aucun cas autorisé. choisissez simplement std::initializer_list (ou T(&&)[N] pour une longueur fixe). Vector{1.0, 2.0} et Vector{1.0, 2.0} + Vector{3.0, 4.0} sont assez élégants.


OT: Je peux passer encore une autre idée pour votre classe vectorielle: (Pas) Définir une instance statique d'une classe . ;-)


@jrok Je suppose que j'aurais besoin de quelque chose pour stocker temporairement les résultats de mElements[k] + rightVector.mElements[k] avant de passer le résultat global au constructeur. Je n'ai pas beaucoup d'expérience avec std::initializer_list , mais il ne semble pas destiné au stockage de données. Comment cela fonctionnerait-il?


@RedFog Cette && (apparemment appelée référence rvalue ) est nouvelle pour moi, donc cette question porte déjà ses fruits! Quant à l'utilisation de std::initializer_list , je ne sais pas trop comment l'utiliser pour stocker (temporairement) des données dans un opérateur surchargé (voir aussi ma réponse à @jrok). Envisageriez-vous de rédiger votre commentaire comme réponse?


Il semble que vous cherchiez un std::valarray taille statique, est-ce le cas? Aussi, je suis d'accord avec @RedFog, il semble que T(&&)[N] , serait le type idéal à utiliser ici.


3 Réponses :


1
votes

Le moyen le plus pratique de procéder serait d'utiliser un guide de déduction, en modifiant votre exemple d'utilisation donné:

#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';
}

en quelque chose de beaucoup plus simple:

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"
}

Activation d'une utilisation similaire à std::array .

Le moyen le plus simple et le plus efficace de créer un vecteur de taille statique permettant cela serait sans doute d'utiliser un __attriubute__ C ++ non standard.

En modélisant le T __attribute__((vector_size(N))) , nous nous retrouvons avec ce qui suit:

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

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

Ok, d'accord, je plaisante, ne fais pas ça, n'abordons pas le monde des attributs, des extensions de compilateur et du code non portable.

Le moyen le plus simple de le rendre aussi pratique à utiliser que le std::array sous-jacent serait soit de créer un guide de déduction de type, d'hériter de std::array , soit de ne pas implémenter un constructeur, les deux derniers autorisant le std::array constructeur de std::array prend effet, car la classe n'a pas de constructeur en soi.

Je pense que, dans ce cas, vous pourrez peut-être simplement hériter de std::array , sans énerver tous les développeurs C ++ sur SO.

Quelque chose du genre:

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

std::enable_if , vous voudrez peut-être utiliser quelques std::enable_if ou assertions pour vous assurer de ne créer que des vecteurs de type numérique, car cela semble être ce que vous voulez utiliser.

Cela devrait avoir la même utilisation de la mémoire qu'un std::array . Il n'initialise aucun type supplémentaire, contrairement à votre exemple qui avait construit std::vector s partout.

Cela correspond-il à votre utilisation et à vos objectifs prévus?


8 commentaires

Bien, cela fonctionnerait, mais à strictement parler, il faudrait utiliser vector_t x = {{ 1.0, 2.0 }} et ainsi de suite (c'est-à-dire des doubles crochets au lieu de simples), voir le premier élément de la liste comparant les options dans ma question . Ce n'est pas un désastre pour un std:array 1D, mais comme mentionné, cela ne se généralise pas très bien à un std::array 2D, car cela aurait besoin de quadruple (ou triple) accolades.


Que voulez-vous dire? Pouvez-vous expliquer comment vous voulez qu'il se généralise dans un tableau 2D? Il ne devrait pas avoir besoin de crochets triples ou quadruples pour les tableaux 2D.


De plus, quelle est la justification de la définition de decltype(auto) self = *this; plutôt que d'utiliser *this directement?


@Ailurus à propos de soi, techniquement, sans optimisation, this pointeur serait déréférencé plusieurs fois. Bien sûr, le code peut être écrit de nombreuses manières différentes, la façon dont vous voudriez l'utiliser dépend de vous. Modifiez-le en fonction de votre style.


Disons que je voudrais définir un modèle de classe de matrice utilisant le stockage 2D, par exemple std::array< std::array<T, n>, m > pour une matrice avec m lignes et n colonnes. L'initialisation d'une telle chose n'est pas si agréable car elle nécessite des accolades triples (strictement parlées, même quadruples), voir par exemple cette question


Je vois de quoi vous parlez maintenant, je pense que je pourrais peut-être trouver un moyen de contourner cela. Je vais regarder comment std :: tuples et paires gèrent cela. Bien que, je pense que cela fonctionne bien avec std::initializer_list s.


Utiliser std::initializer_list comme paramètre de constructeur fonctionnerait, bien que je ne sache pas comment combiner (efficacement) cela avec des opérateurs de surcharge (voir les commentaires originaux). Une réponse basée sur cette approche (et / ou en utilisant T(&&)[N] ) serait formidable.


@Ailurus T(&&)[N] ne fonctionnera certainement pas, parce que nous finirions avec T(&&)[N](&&)[N] et maintenant nous avons des références de références, donc vous obtiendrez juste des erreurs. Vous auriez en quelque sorte besoin d'obtenir le modèle pour autoriser T(&&)[N] , T(&&)[N][N] , ou T(&&)[N][N][N] , ce que je doute est possible. À moins que vous ne souhaitiez uniquement un tableau 2D, ni plus ni moins.



1
votes

Vous pouvez également utiliser des packs de paramètres, qui (combinés à un polymorphisme sympa) peuvent vous permettre de faire des choses comme ceci:

#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); }
    }
};

Où Vector est défini comme suit:

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


1 commentaires

Approche intéressante! Ces modèles variadiques semblent utiles (bien que l'opérateur d'ellipse ... semble un peu étrange)



0
votes

Vous pouvez directement utiliser les paramètres pour initialiser la classe de base

Code non testé.

template <typename... Args>
Vector(Args...&& args) : mEVector(std::forward<Args>(args)...) { };


0 commentaires