3
votes

Alias ​​de variable de membre dans la spécialisation de modèle de classe

Supposons que j'écris une classe de modèle Vector pour représenter des points et des vecteurs dans un espace à N dimensions. Quelque chose comme ce qui suit:

template <typename T, int N>
struct Vector
{
    T data[N];
    // ...
};

Supposons en outre que, pour une raison quelconque, je veux que l'utilisateur puisse accéder aux données avec des noms significatifs dans le cas de plus petits vecteurs, par exemple en utilisant vx ou vy au lieu de v.data [0] et v.data [1] .

J'ai deux contraintes supplémentaires.

  1. L'accès au composant x ou y d'un vecteur ne doit pas être écrit comme un appel de fonction (par exemple, il doit être vx , pas vx () ).
  2. L'égalité suivante doit contenir sizeof (Vector ) == N * sizeof (T) .

J'ai examiné différentes approches possibles, y compris les références de variables membres, la répartition des balises et même le CRTP mais aucune d'entre elles ne répondait à toutes mes exigences. p>

Est-il même possible de créer de tels alias? Et si oui, comment pourrait-on le faire?


17 commentaires

Quel est le problème avec v.x () ?


Les objets doivent être adressables de manière unique, les membres ne partagent jamais d'adresse. Cela signifie que les membres de données non statiques, même ceux sans état, ont un impact effectif différent de zéro sur la taille de la classe. Donc insister sur l'ajout du membre de données (Vector :: x) et insister pour ne pas augmenter la taille de votre classe me semble contradictoire. Les exceptions à cela sont les union mais même dans ce cas, un seul membre peut être actif à un moment donné. Ce n'est pas approprié pour un alias.


C ++ n'a pas d'alias de variable. Il a des références mais celles-ci peuvent / viendront avec un coût d'espace. Comme une abstraction probable à coût nul pourrait écrire int & x = my_vector.data [0]; pour créer votre propre alias dans le site d'utilisation et cela sera probablement optimisé.


@ FrançoisAndrieux Il n'y a rien de mal avec v.x () . Cette question n'est qu'un prétexte pour tenter de résoudre un problème que je tripote depuis un certain temps.


@ FrançoisAndrieux Comme l'a souligné NathanOliver, la remarque de ne pas augmenter la taille de la classe est liée à l'une des approches que j'ai mentionnées, à savoir les références aux membres. Si j'ajoute des références à chaque cellule de données , en leur donnant effectivement des noms, j'augmenterai la taille globale de Vector .


@ J-M.Gorius Je comprends la motivation de l'exigence, mais je signale qu'elle semble contradictoire avec votre autre exigence.


@NathanOliver Cela pourrait être une solution, mais cela impose une charge supplémentaire à l'utilisateur de la classe.


@ J-M.Gorius La solution habituelle est d'ajouter des fonctions membres comme x () . Si vous avez expliqué votre objection à l'utilisation de cette solution, il pourrait être plus facile de trouver quelque chose qui fonctionne pour vous. Je ne pense pas qu'il existe une solution qui réponde à toutes les exigences que vous avez avancées.


@ FrançoisAndrieux La principale raison de l'exigence v.x est la compatibilité des API. Le type Vector est destiné à remplacer un ensemble de classes (par exemple Vector2 , Vector3 , etc.) afin d'éviter la duplication de code. Mais il y a déjà une quantité importante de code utilisant Vector2 et utilisant v.x partout.


@ J-M.Gorius Je ne vois pas comment vous pourriez y parvenir sans frais. Cela vaut peut-être la peine d'envisager de vous en tenir à ce que vous avez et d'ajouter un membre ou une fonction gratuite qui peut être obtenu / défini par index si vous avez besoin d'indexation. Cela montre pourquoi l'encapsulation en vaut la peine. Si les types de vecteurs utilisaient des fonctions à la place, même si elles semblaient inutiles à l'époque, cela rendrait le changement du back-end très facile. Vous ne savez jamais quelles pourraient être vos exigences à l'avenir.


@ J-M.Gorius Vous pouvez spécialiser la classe pour différentes tailles. Ainsi, Vector aurait juste un membre x , Vector aurait un x et y au lieu d'un tableau et ainsi de suite jusqu'à ce que vous souhaitiez que le cas générique soit simplement un tableau. Cela ne facilite pas beaucoup la vie, mais ils proviendraient au moins du même modèle de classe.


@ FrançoisAndrieux J'irai certainement pour quelque chose comme ça, mais j'aimerais voir si quelqu'un trouve une autre solution.


Si vous n'êtes pas contraint à C ++, pouvez-vous utiliser le langage de programmation D? Il prend en charge les modèles qui peuvent faire ce que vous recherchez.


@Eljay Cela doit être fait en C ++.


Un problème intéressant. La solution peut finir par être un fichier d'en-tête assez volumineux en C ++, N étant contraint par la profondeur de vos spécialisations et de nombreuses répétitions. Ne devrait pas avoir d'impact sur l'efficacité.


Pourquoi voulez-vous la deuxième contrainte? Cette contrainte est difficile à garantir entre différents compilateurs en raison des différentes politiques de remplissage et d'alignement.


@ J-M.Gorius, ce genre de manigances peut parfois être résolu en utilisant Macros X . N'ayez pas le temps de donner une réponse complète, mais en bref, ils vous permettent d'avoir un accès indexé aux membres nommés, et à partir de là, le chemin n'est pas long vers la solution (je pense - je n'y ai pas réfléchi).


3 Réponses :


-1
votes

Voici une solution possible (même si je pense que c'est une mauvaise pratique, et je ne sais pas vraiment si elle est portable):

template <typename T, int N> union Vector;

template <typename T> union Vector<T, 1> {
    struct { T x; };
    T data[1];
};

template <typename T> union Vector<T, 2> {
    struct { T x, y; };
    T data[2];
};

template <typename T> union Vector<T, 3> {
    struct { T x, y, z; };
    T data[3];
};

template <typename T> union Vector<T, 4> {
    struct { T x, y, z, w; };
    T data[4];
};

Voici un exemple de ce qui se passe:

int main() {
    Vector<int, 10> vec;
    vec.x = 100;
    vec.y = 200;
    vec.z = 300;
    vec.data[3] = vec.data[2] + 100;

    printf("%d %d %d %d\n", vec.data[0], vec.data[1], vec.data[2], vec.data[3]);
    printf("size = %d\n", (int) sizeof(vec));

    return 0;
}

Output:
    100 200 300 400
    size = 40


14 commentaires

C'est un comportement indéfini à lire à partir d'un membre du syndicat si ce n'était pas le dernier à être affecté. Ce n'est pas une solution.


C'est UB pour tous les types non triviaux.


De plus, cette solution rendrait z accessible même pour Vector (?).


@NathanOliver Autant que je sache, c'est UB pour tous les types. Pouvez-vous créer un lien vers une référence qui explique pourquoi il est acceptable pour les types triviaux?


@ FrançoisAndrieux Si je lis ceci correctement, le devoir mettra fin au durée de vie de la structure anonyme et commence la durée de vie du tableau (tant qu'il est trivial) donc le seul UB est en fait le vec.data [2] + 100 depuis vec.data [2 ] a une valeur indéterminée. C'est donc UB dans tous les cas, mais pas pour la raison du changement de membre.


@ J-M.Gorius vous pouvez activer le modèle uniquement pour N> 2 en utilisant std :: enable_if et probablement le spécialiser pour N <= 2


@ JM.Gorius voici une autre bibliothèque relativement populaire (GLM) utilisant ma solution possible proposée lien


@NathanOliver J'ai mis à jour ma réponse pour répondre à votre commentaire.


Je ne sais pas si T x; T y; est compatible avec la mise en page avec T data [2]; mais je vais y jeter un œil.


Je ne trouve rien qui indique que deux T sont compatibles avec la mise en page avec un T [2] . Bien que je ne serais pas surpris si c'était le cas, jusqu'à ce que quelqu'un puisse montrer que c'est vrai, je devrai supposer que c'est toujours UB.


@GeorgiGerganov Cela semble être le modèle commun utilisé par toutes les petites bibliothèques mathématiques fournissant des types de vecteurs.


Notez que dans la question liée, les deux struct utilisés dans enum sont exactement les mêmes, à l'exception de leurs noms et des noms de leurs membres.


Pourriez-vous donner un exemple de bibliothèque célèbre qui utilise ce modèle? Autant que je sache, au moins Eigen n'utilise pas ce modèle.


@xskxzr on pourrait affirmer que GLM est célèbre. Mais de toute façon, je n'ai trouvé aucune autre grande / célèbre bibliothèque utilisant cette astuce. J'ai trouvé une conférence CppCon 2018 où le présentateur affirme que cette approche syndicale est totalement sûre pour les types triviaux. La question des membres actifs de la norme ne devrait pas être un problème dans le monde réel. Voici la présentation



2
votes

(Ce n'est pas une réponse, c'est un commentaire avec un exemple de code, qui ne correspond pas à un commentaire, et ne se met pas bien en forme s'il peut être inséré dans un commentaire.) dans l'autre sens, et exprimez le vecteur comme un groupe de champs, puis mappez un getter / setter d'index à chacun de ces champs?

Suppression du paramètre de modèle N pour simplifier le problème:

< pré> XXX


12 commentaires

Le paramètre de modèle N est important, car c'est celui qui est responsable de mon problème en premier lieu. J'ai déjà une classe Vector3 qui fonctionne bien, mais je veux factoriser le code et introduire des vecteurs de taille arbitraires.


Hmm, mais chacun des champs nommés nécessiterait une spécialisation partielle pour chaque taille arbitraire prise en charge. Ce qui va à l'encontre du but, à moins que le but ne soit de pouvoir dire Vector plutôt que Vector3 .


Le grand défi serait de désactiver les membres en fonction de N . Peut-être pourriez-vous hériter conditionnellement des types de base qui fournissent x , y et z ?


@Eljay Le but est d'avoir quelque chose comme en utilisant Vector3 = Vector avec Vector étant une version spécialisée de Vector qui incorporerait les accesseurs x , y et z .


@ FrançoisAndrieux Cela s'apparente à l'envoi de balises, mais pour les variables membres. Malheureusement, je n'ai trouvé aucun moyen d'appliquer ces techniques aux variables, uniquement aux fonctions membres.


@ J-M.Gorius Je ne vois pas la relation entre l'héritage de vos membres de données et l'envoi de balises. Je veux introduire un type de base que vous spécialisez et qui fournit strictement les données membres. Vous pouvez ensuite en hériter et implémenter vos fonctions membres dans la classe commune Vector comme le suggère cette réponse. Bien que je ne vois pas comment vous voulez gérer les dimensions supérieures à 3 ou 4, il est donc difficile de donner un exemple.


@ FrançoisAndrieux J'ai mal compris ce que vous vouliez dire en premier lieu. En fait, les seules dimensions pour lesquelles il doit y avoir un membre nommé au lieu d'un simple opérateur d'indexation sont 2, 3 et 4.


En essayant de trouver un exemple, il semble que cela se résume à simplement implémenter Vector2 , Vector3 et Vector4 et leur fournir un alias basé sur un modèle, ce qui vous ramène à la case départ. La seule chose qui sauve est peut-être que cela vous permet d'écrire des implémentations simples, puis d'implémenter des fonctionnalités communes sous forme de fonctions libres ou d'un type commun.


@ FrançoisAndrieux Je viens de trouver ce dépôt GitHub , qui semble répondre partiellement à ma question sur la première vue. Je vais y jeter un œil plus précisément dans les plus brefs délais.


@ J-M.Gorius Cette solution implémente en fait simplement chacun des vec2, vec3 et vec4 séparément et repose également sur des extensions de compilateur qui prennent en charge l'accès aux membres inactifs d'un syndicat. Voici le type de base de vec3 , c'est la même chose que d'avoir Vector3 mais il repose également sur un comportement non défini. Si cette solution est acceptable pour vous, alors vous pouvez aussi bien conserver ce que vous avez maintenant et ajouter un Vector qui alias chaque type de vecteur concret.


@ J-M.Gorius comme le souligne correctement François, ils utilisent exactement la solution que j'ai proposée. Gardez simplement à l'esprit que c'est UB.


@ FrançoisAndrieux en fait, il s'avère que ce cas particulier n'est pas UB. Voir ici pour plus d'informations - lien . J'ai mis à jour ma réponse pour refléter cela.



0
votes

Si les macros sont autorisées, cela semble faisable.

Première tentative (belle mais pas parfaite ...)

#define DefineVector(VAR, ...) \
    template<typename T> \
    struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
      : Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)> { \
        T VAR; \
    T& operator[](int index) { \
        if constexpr(std::is_standard_layout_v<T>) { \
            return *(&VAR - (BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) - index)); \
        } else { \
            if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
            return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
        } \
    } \
}

Pour y parvenir ce qui précède:

#include <boost/preprocessor/variadic/size.hpp>

template<typename T, size_t DIMENSIONS>
struct Vector;

#define DefineVector(VAR, ...) \
    template<typename T> \
    struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
      : Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)> { \
        T VAR; \
        T& operator[](int index) { \
            if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
            return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
        } \
    }

#define DefineVector1(VAR) \
    template<typename T> \
    struct Vector<T, 1> { \
        T VAR; \
        T& operator[](int index) { \
            /* in case index != 0 this is UB */ \
            return VAR; \
        } \
    }

DefineVector1(x);
DefineVector(y, x);
DefineVector(z, y, x);
// TODO: create recursive macro for DefineVector(z, y, x)
// that will create the two above recursively

http: // coliru.stacked-crooked.com/a/42625e9c198e1e58

EDIT : Ajout de l'opérateur [] pour résoudre le problème soulevé dans le commentaire.


Deuxième tentative: avec des noms de champs plus agréables

L'OP a demandé que les champs aient des noms plus beaux, comme x, y, z.

C'est un défi. Mais les macros viennent encore à la rescousse:

int main() {
    Vector<int, 3> vec;
    vec[0] = 1;
    vec[1] = 2;
    vec[2] = 3;
    std::cout << vec.x + vec.y + vec.z; // 6
}

Avec le code suivant:

#define VAR_NAME(num) t##num

#define DefineVector(num) \
    template<typename T> \
    struct Vector<T, num> : Vector<T, num-1> { \
        T VAR_NAME(num); \
        T& operator[](int index) { \
            if(index == num-1) return VAR_NAME(num); \
            return Vector<T, num-1>::operator[](index); \
        } \
    }

template<typename T, size_t N>
struct Vector;

template<typename T>
struct Vector<T, 1> {
    T t1;
    T& operator[](int index) {
        // in case index != 0 this is UB
        return t1;
    }
};

DefineVector(2);
DefineVector(3);
DefineVector(4);

// TODO:
// replace 3 declarations above with a single *DefineVectorsRecursively(4);*
// by using recursive macros
// see: https://stackoverflow.com/questions/12447557/can-we-have-recursive-macros
// leaving this as a further exercise...

Code: http://coliru.stacked-crooked.com/a/2550eede71dc9b5e


Mais attendez, l'opérateur [] n'est pas si efficace

J'ai pensé avoir un opérateur plus efficace [] dans le cas où T est un type de mise en page standard , avec l'implémentation suivante:

int main() {
    Vector<int, 4> vec;
    vec[0] = 1; // same as: vec.t1 = 1;
    vec[1] = 2; // same as: vec.t2 = 2;
    vec[2] = 3; // same as: vec.t3 = 3;
    vec[3] = 4; // same as: vec.t4 = 4;
    std::cout << vec.t1 + vec.t2 + vec.t3 + vec.t4; // 10
}

La (BAD) tentative: http://coliru.stacked-crooked.com/a/d367e770f107995f

Malheureusement - l'optimisation ci-dessus est illégale

Pour le présenté mise en œuvre, tout vecteur avec DIMENSIONS> 1 n'est pas une classe de mise en page standard (même si T est) car il a des membres à la fois dans la classe de base et dans la classe dérivée :

10.1 [class.prop]

[3] Une classe S est une classe de mise en page standard si elle: ...

[3.6] a tous les membres de données non statiques et les champs de bits dans la classe et sa base classes déclarées pour la première fois dans la même classe ...

La tentative d'optimisation ci-dessus a donc un comportement indéfini - le compilateur n'est pas obligé de garder l'adresse des membres de la hiérarchie d'héritage dans leur ordre.

La solution initiale est toujours valide.


2 commentaires

De cette façon, le tableau T data est manquant. OP veut y accéder via des alias: "Je veux que l'utilisateur puisse accéder aux données avec des noms significatifs dans le cas de petits vecteurs, par exemple en utilisant vx ou vy au lieu de v.data [0] et v.data [1] ". Sinon, ils peuvent simplement déclarer ... Vector {T x, y, z; };


opérateur ajouté []