2 votes

Comment retourner une variante à partir d'un itérateur d'entrée avec de hautes performances ?

J'ai un décodeur de format de fichier qui retourne un fichier personnalisé itérateur d'entrée . Le type de valeur de cet itérateur (lorsqu'il est déréférencé avec la fonction *iter ) peut être l'un des nombreux types de jetons.

Voici un exemple d'utilisation simplifié :

File file {"/path/to/file"};

for (const auto& token : file) {
    // do something with token
}

Comment cela peut-il token ont plusieurs types possibles ? En fonction du type du jeton, le type de sa charge utile change également.

La performance est importante ici pendant la traversée. Je ne veux pas d'allocation inutile, par exemple. C'est pourquoi le type de l'itérateur est un itérateur de saisie Dès que l'on fait avancer l'itérateur, le jeton précédent est invalidé, conformément aux exigences de la directive sur la sécurité des données. InputIterator étiquette.

J'ai deux idées en tête pour l'instant :

  1. Utilisez un seul Token avec une classe privée union de toutes les charges utiles possibles (avec leurs récupérateurs publics) et un ID de type public ( enum ) getter. L'utilisateur doit basculer sur cet ID de type pour savoir quel getter de charge utile appeler :

    for (const auto& token : file) {
        switch (token.type()) {
        case Token::Type::APPLE:
            const auto& apple = token.apple();
            // ...
            break;
    
        case Token::Type::BANANA:
            const auto& banana = token.banana();
            // ...
            break;
    
        // ...
        }
    }

    Bien que ce soit probablement ce que je choisirais en C, je ne suis pas un fan de cette solution en C++ car l'utilisateur peut toujours appeler le mauvais getter et rien ne peut le faire respecter (sauf les vérifications à l'exécution que je veux éviter pour des raisons de performances).

  2. Créer un résumé Token qui possède une classe de base accept() pour accepter un visiteur, et plusieurs classes concrètes (une pour chaque type de charge utile) héritant de cette classe de base. Dans l'objet itérateur, instanciez un exemplaire de chaque classe concrète au moment de la création. Ayez également un Token *token membre. Lors de l'itération, remplissez l'objet de charge utile pré-alloué approprié, et définissez this->token = this->specificToken . Faire operator*() retourner this->token (référence à). Demander à l'utilisateur d'utiliser un visiteur pendant l'itération (ou pire, utiliser dynamic_cast ) :

    class MyVisitor : public TokenVisitor {
    public:
        void visit(const AppleToken& token) override {
            // ...
        }
    
        void visit(const BananaToken& token) override {
            // ...
        }
    };
    
    TokenVisitor visitor;
    
    for (const auto& token : file) {
        token.accept(visitor);
    }

    Cela introduit des appels de fonction supplémentaires pour chaque jeton, dont au moins un qui est virtuel, mais ce n'est peut-être pas la fin du monde ; je reste ouvert à cette solution.

Y a-t-il une autre solution intéressante ? Je considère que le fait de renvoyer un boost::variant o std::variant est identique à l'idée n°2.

2voto

skypjack Points 5516

Bien que ce soit probablement ce que je choisirais en C, je ne suis pas un fan de cette solution en C++ car l'utilisateur peut toujours appeler le mauvais getter et rien ne peut le faire respecter (à l'exception des vérifications à l'exécution que je veux éviter pour des raisons de performances).

Vous pouvez inverser l'approche et accepter un objet appelable au lieu de renvoyer un itérateur à l'utilisateur. Vous pouvez alors itérer le conteneur en interne et envoyer le bon type. De cette façon, les utilisateurs ne peuvent plus faire d'erreurs en ignorant les informations transportées par votre union balisée, car vous êtes chargé de les prendre en considération.

Voici un exemple minimal et fonctionnel pour montrer ce que je veux dire :

#include <vector>
#include <utility>
#include <iostream>

struct A {};
struct B {};

class C {
    struct S {
        enum { A_TAG, B_TAG } tag;
        union { A a; B b; };
    };

public:
    void add(A a) {
        S s;
        s.a = a;
        s.tag = S::A_TAG;
        vec.push_back(s);
    }

    void add(B b) {
        S s;
        s.b = b;
        s.tag = S::B_TAG;
        vec.push_back(s);
    }

    template<typename F>
    void iterate(F &&f) {
        for(auto &&s: vec) {
            if(s.tag == S::A_TAG) {
                std::forward<F>(f)(s.a);
            } else {
                std::forward<F>(f)(s.b);
            }
        }
    }

private:
    std::vector<S> vec;
};

void f(const A &) {
    std::cout << "A" << std::endl;
}

void f(const B &) {
    std::cout << "B" << std::endl;
}

int main() {
    C c;
    c.add(A{});
    c.add(B{});
    c.add(A{});
    c.iterate([](auto item) { f(item); });

}

Vous pouvez le voir fonctionner sur Coliru .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X