2
votes

Mappage d'énumération bidirectionnel efficace

Having:

Mapper<A, B> mapper(
     {
     {A::FirstA,   B::SecondB},
     {A::SecondA,  B::FirstB}
     },
     {A::InvalidA, B::InvalidB} // this is for conversions, where no mapping is specified
);

Comment activer quelque chose comme ça?

B b = mapper[A::FirstA];
A a = mapper[B::SecondB];

Une solution possible est de créer un Mapper classe de modèle, qui permet de spécifier le mappage via la liste d'initialisation dans le constructeur, quelque chose comme:

enum class A : int {
    FirstA,
    SecondA,
    InvalidB
};

enum class B : int {
    FirstB,
    SecondB,
    InvalidB
};

Mais en interne, cela nécessitera des compromis - soit deux cartes (de A à B et de B à A ) ou une carte, par exemple de A à B et une recherche linéaire des conversions de B à A ).

Est-il possible d'implémenter cela dans le standard C ++ 14 afin que:

  • aucun double conteneur n'est utilisé
  • les performances de recherche sont également bonnes dans les deux sens
  • la définition et l'utilisation du mappage sont relativement simples (la mise en œuvre interne n'a pas besoin de l'être)

Avec les exigences, que:

  • il ne s'agit pas d'un mappage d'identité (c'est-à-dire que les valeurs de A ne sont pas mappées aux mêmes valeurs sous-jacentes de B
  • les types sous-jacents de A et B peuvent différer
  • le mappage est connu lors de la compilation

?


19 commentaires

Pourquoi devez-vous éviter d'utiliser deux cartes? Cela ne consomme que 2 fois plus de mémoire qu'une carte, ce qui n'est pas beaucoup supposé qu'ils ont une valeur enum (ont la taille d'environ un int), et surtout vous ne vous souciez pas trop de l'implémentation interne?


La carte est-elle connue en temps constant?


@ user202729, c'est dans le domaine intégré, et nécessaire pour le mappage non seulement entre deux énumérations, mais plus encore, donc avoir le 2 * [mappings_for_one_enum * 2 * int_size] est pertinent.


@ user202729 si par temps constant vous voulez dire compilation , alors oui.


La valeur entière des énumérations est-elle garantie d'être consécutive?


@Timo, eh bien, c'est la solution que j'ai également proposée dans la question, mais elle comporte des mises en garde.


Peut-être boost :: bimap


@ user202729, oui, peut également être garanti que les valeurs commencent à 0 - les énumérations sont générées à partir du document de spécification, il est donc possible de s'en assurer.


@NathanOliver, je suis également tombé sur boost :: bimap lors de la recherche d'une solution en premier, mais malheureusement, boost n'est pas une option, devrait se faire au moyen du C ++ standard.


Si les deux énumérations ont la même valeur, vous pouvez simplement les lancer entre elles


Dans ce cas, vous pouvez soit utiliser une métaprogrammation de modèle pour que le mappeur consomme de la mémoire O (1), soit une recherche de tableau (ils consommeront de la mémoire O (N) où N = nombre d'éléments d'énumération) Dans les deux cas, la recherche O (1) .


@AlanBirtles, non, ce n'est pas une carte d'identité.


Veuillez alors fournir un exemple minimal reproductible , votre exemple peut être implémenté avec un cast


Je suis tenté de fermer pour dupliquer avec stackoverflow.com/questions/21760343/...


@AlanBirtles, mis à jour, devrait être exhaustif maintenant.


@Pixelchemist Dans ce cas, les valeurs sont des énumérations dans une plage et sont connues en temps constant, donc une solution plus efficace existe.


@Pixelchemist, a manqué cette autre question lors de la recherche, mais bien que la réponse soit acceptée, elle ne résout pas le problème de la double carte, bien qu'elle améliore la solution originale dans l'autre article ...


Pour être parfaitement clair: la recherche au moment de la compilation est-elle suffisante? Ou est-il nécessaire que le mappage puisse également être spécifié au moment de l'exécution?


@MaxLanghof, a mis à jour la question (mentionnée dans le commentaire plus tôt) - le mappage est connu au moment de la compilation.


3 Réponses :


2
votes

Vous pouvez le faire assez facilement en utilisant des modèles de fonctions et une spécialisation complète. Vous faites en sorte que le modèle principal renvoie la casse non valide, puis les spécialisations renverront les mappages que vous voulez.

Si vous avez

MAKE_ENUM_MAP(A, A::InvalidA, B, B::InvalidB)

, vous pouvez ajouter tous les mappages des valeurs comme

#define MAKE_ENUM_MAP(from, from_value, to, to_value) \
template<from> \
auto mapper() { return to_value; } \
template<to> \
auto mapper() { return from_value; }

et ensuite vous l'appelleriez comme

MAKE_ENUM_MAP(A, B)
ADD_MAPPING(A::FirstA, B::SecondB)
ADD_MAPPING(A::SecondA, B::FirstB)

Cela ne vous laisse aucun conteneur. Vous pouvez même créer des macros pour rendre cela plus facile comme

#define MAKE_ENUM_MAP(from, to) \
template<from> \
auto mapper() { return to::Invalid; } \
template<to> \
auto mapper() { return from::Invalid; }


#define ADD_MAPPING(from_value, to_value) \
template<> \
auto mapper<from_value>() { return to_value; } \
template<> \
auto mapper<to_value>() { return from_value; }

, puis vous les utiliserez comme

B b = mapper<A::FirstA>();
A a = mapper<B::SecondB>();

pour générer tout le code pour vous. La version ci-dessus utilise une valeur d'énumération unique de Invalid pour la casse non valide des mappages. Si vous ne le souhaitez pas, vous pouvez ajouter à la macro les valeurs from et to à utiliser pour les mappages non valides comme

template<>
B mapper<A::FirstA>() { return B::SecondB; }
template<>
B mapper<A::SecondA>() { return B::FirstB; }
template<>
A mapper<B::FirstB>() { return A::SecondA; }
template<>
A mapper<B::SecondB>() { return A::FirstA; }

et vous l'appelleriez comme

template<A>
B mapper() { return B::InvalidB; }
template<B>
A mapper() { return A::InvalidA; }


0 commentaires

1
votes

La solution de Nathan est difficile à battre en termes d'élégance de mise en œuvre. Mais si vous avez désespérément besoin d'une solution qui ne repose pas sur des macros ou qui peut également être utilisée au moment de l'exécution, en voici une où vous spécifiez le mappage dans une simple liste de paires.

Au cœur de celle-ci, nous sommes en utilisant le fait que les deux énumérations doivent avoir des valeurs intégrales sous-jacentes contiguës (commençant à zéro), ce qui signifie que nous pouvons représenter les mappages dans les deux directions comme de simples tableaux. Tout cela est constexpr donc aucune surcharge dans le cas de la compilation. Pour une utilisation au moment de l'exécution, cela stocke les informations deux fois pour permettre une recherche instantanée, mais ne prend que le stockage N (sizeof (A) + sizeof (B)) . Je ne connais aucune structure de données qui fasse mieux (c'est-à-dire ne stocke aucune donnée supplémentaire au-delà de l'un des deux tableaux et est meilleure que la recherche linéaire dans les deux sens). Notez que cela prend le même stockage que le stockage des paires elles-mêmes (mais ne gagne rien de la bijectivité du mappage).

using MyMappingList = PairList<
    MyMappingPair<A::A1, B::B2>,
    MyMappingPair<A::A2, B::B3>,
    MyMappingPair<A::A3, B::B4>,
    MyMappingPair<A::A4, B::B1>
    >;

auto mapper = makeMapper<A, B>(MyMappingList{});

(Cela utilise des expressions de pliage + un opérateur virgule pour attribuer des valeurs aux éléments du tableau, voir par exemple ici a>).

Le code utilisateur fournit une liste de paires de mappages à utiliser et c'est fait:

template<class TA, class TB, class ... Pairs>
struct Mapper
{
    constexpr static std::array<TA, sizeof...(Pairs)> generateAIndices()
    {
        std::array<TA, sizeof...(Pairs)> ret{};
        ((void)((ret[static_cast<std::size_t>(Pairs::tb)] = Pairs::ta), 0), ...);
        return ret;
    }
    constexpr static std::array<TB, sizeof...(Pairs)> generateBIndices()
    {
        std::array<TB, sizeof...(Pairs)> ret{};
        ((void)((ret[static_cast<std::size_t>(Pairs::ta)] = Pairs::tb), 0), ...);
        return ret;
    }

    constexpr TB operator[](TA ta)
    {
        return toB[static_cast<std::size_t>(ta)];
    }
    constexpr TA operator[](TB tb)
    {
        return toA[static_cast<std::size_t>(tb)];
    }

    static constexpr std::array<TA, sizeof...(Pairs)> toA = generateAIndices();
    static constexpr std::array<TB, sizeof...(Pairs)> toB = generateBIndices();
};

Démo comprenant des cas de test complets au moment de la compilation et un code d'exécution extrêmement efficace (littéralement juste mov ).


Voici une version précédente qui fonctionne uniquement à la compilation (voir aussi l'historique des révisions): https://godbolt.org/z/GCkAhn


3 commentaires

@Hiroki Ne vous inquiétez pas, j'ai d'abord eu un autre et je l'ai supprimé pendant que je travaillais sur le même que vous.


@Hiroki Cependant, j'ai l'impression qu'en utilisant une supercherie xor trop intelligente, on pourrait être en mesure de réduire de moitié l'espace de stockage requis (pensez à une liste xor-doublement liée). Vous avez peut-être une idée?


@ MaxLanghof Ah je comprends. Vous êtes intelligent à ce stade :).



1
votes

Si vous devez effectuer la recherche au moment de l'exécution, la méthode suivante fonctionnerait avec la complexité O (1) dans les deux sens.

Étant donné que tous vos énumérateurs de A et B ne sont pas initialisés, le premier énumérateur a la valeur zéro, le second a la valeur 1 , et ainsi de suite. En considérant ces entiers à zéro comme des indices de tableaux, nous pouvons construire une carte bidirectionnelle en utilisant deux tableaux. Par exemple, en supposant que le mappage actuel est

// compile-time construction.
constexpr biEnumMap<3> mapper({{
    {A::FirstA  , B::SecondB },
    {A::SecondA , B::FirstB  },
    {A::InvalidA, B::InvalidB} }});

// compile-time tests, A to B.
static_assert(mapper(A::FirstA  ) == B::SecondB );
static_assert(mapper(A::SecondA ) == B::FirstB  );
static_assert(mapper(A::InvalidA) == B::InvalidB);

// compile-time tests, B to A.
static_assert(mapper(B::FirstB  ) == A::SecondA );
static_assert(mapper(B::SecondB ) == A::FirstA  );
static_assert(mapper(B::InvalidB) == A::InvalidA);

// run-time tests, A to B.
std::vector<A> vA = {A::FirstA, A::SecondA, A::InvalidA};
assert(mapper(vA[0]) == B::SecondB );
assert(mapper(vA[1]) == B::FirstB  );
assert(mapper(vA[2]) == B::InvalidB);    

// run-time tests, B to A.
std::vector<B> vB = {B::FirstB, B::SecondB, B::InvalidB};
assert(mapper(vB[0]) == A::SecondA );
assert(mapper(vB[1]) == A::FirstA  );
assert(mapper(vB[2]) == A::InvalidA);   

, définissons les deux tableaux suivants

template<std::size_t N>
class biENumMap
{
    A arrA[N];
    B arrB[N];

public:
    constexpr biENumMap(const std::array<std::pair<A,B>, N>& init) 
        : arrA(), arrB()
    {        
        for(std::size_t i = 0; i < N; ++i)
        {
            const auto& p = init[i];
            arrA[static_cast<std::size_t>(p.second)] = p.first;
            arrB[static_cast<std::size_t>(p.first) ] = p.second;
        }
    }

    constexpr A operator()(B b) const{
        return arrA[static_cast<std::size_t>(b)];
    }

    constexpr B operator()(A a) const{
        return arrB[static_cast<std::size_t>(a)];
    }
};

arrA [i] est l'énumérateur de A correspondant à i -ième énumérateur de B , et vice versa. Dans cette configuration, nous pouvons effectuer une recherche de A a à B comme arrB [std :: size (a)] , et vice versa , avec la complexité O (1).


La classe suivante biENumMap est un exemple d'implémentation de la méthode bidirectionnelle ci-dessus avec C ++ 14 et plus. Veuillez noter que puisque le étendu constexpr est disponible à partir de C ++ 14 , ici le ctor peut également être une expression constante. Deux surcharges operator () sont des fonctions de recherche de A et B , respectivement. Celles-ci peuvent également être des expressions constantes et cette classe nous permet d'effectuer une recherche bidirectionnelle à la fois à la compilation et à l'exécution:

A arrA[2] = {A::SecondA, A::FirstA},
B arrB[2] = {B::SecondB, B::FirstB},

Nous pouvons utiliser cette classe comme suit: p>

DÉMO

A::FirstA  (=0) <--> B::SecondB (=1),
A::SecondA (=1) <--> B::FirstB  (=0),


0 commentaires