web-dev-qa-db-fra.com

Comment remplacer vector <uint8_t> :: const_iterator dans une API?

On m'a confié la tâche de peaufiner l'interface d'une bibliothèque de codecs. Nous utilisons C++ 17, et je ne peux utiliser que la bibliothèque standard (c'est-à-dire pas de Boost). Actuellement, il existe une classe Decoder qui ressemble à peu près à ceci:

class Decoder : public Codec {

public:

    struct Result {
        vector<uint8_t>::const_iterator new_buffer_begin;
        optional<Metadata>              metadata;
        optional<Packet>                packet;
    };

    Result decode(vector<uint8_t>::const_iterator buffer_begin,
                  vector<uint8_t>::const_iterator buffer_end);

private:
    // irrelevant details
};

L'appelant instancie un Decoder, puis envoie un flux de données au décodeur en

  1. Lire un bloc de données d'un fichier (mais il pourrait y avoir d'autres sources à l'avenir) et l'ajouter à un vector<uint8_t>.

  2. Appel de la fonction decode, passage des itérateurs pour leur vecteur.

  3. Si le Result retourné new_buffer_begin est identique à buffer_begin qui a été passé à decode, cela signifie qu'il n'y avait pas assez de données dans le tampon pour décoder quoi que ce soit, et l'appelant devrait revenir à l'étape 1. Sinon, l'appelant consomme le Metadata ou Packet objet qui a été décodé et revient à l'étape 2, en utilisant new_buffer_begin pour la prochaine passe.

Les choses que je n'aime pas à propos de cette interface et ont besoin d'aide pour s'améliorer:

  • En utilisant vector<uint8_t>::const_iterator semble trop spécifique. Existe-t-il une approche plus générique qui ne force pas l'appelant à utiliser vector? Je pensais simplement utiliser une interface de style C; une uint8_t * et une longueur. Existe-t-il une alternative C++ assez générique?

  • S'il y avait suffisamment de données pour décoder quelque chose, seul metadataopacket aura une valeur. Je pense std::variant ou 2 rappels (un pour chaque type) rendraient ce code plus auto-documenté. Je ne sais pas ce qui est plus idiomatique cependant. Quels sont les avantages et les inconvénients de chacun et existe-t-il une approche encore meilleure?

17
splicer

Je suis d'accord que mandater vector est inapproprié et je salue vos tentatives pour rendre l'interface plus utile.

Si decode attend une séquence contiguë de uint8_t, la solution éprouvée (et la plus flexible) consiste simplement à prendre un const uint8_t* et un std::size_t (ou alternativement deux pointeurs, mais le pointeur et la longueur sont plus idiomatiques).

À partir de C++ 20, vous pouvez le faire avec un argument de type std::span<const uint8_t> . Ou pour en revenir aux pointeurs, si vous voulez vraiment utiliser des outils de bibliothèque modernes pour le plaisir, vous pouvez confondre les gens avec std::experimental::observer_ptr .

Vous pouvez également envisager de faire de decode un modèle qui accepte n'importe quelle paire d'itérateurs et (si la contiguïté est nécessaire), même si ce n'est que par la documentation, que les itérateurs reflètent un contigu séquence. Mais faire de tout un modèle n'est pas toujours ce que vous voulez, et ce n'est pas toujours utile.

18

En plus de la suggestion valide de @ Justin de spans :

  • Vous pouvez également envisager d'utiliser std::byte au lieu de uint8_t, alors:
    Result decode(std::span<const std::byte> buffer);
    
    ou si vous êtes en C++ 17, utilisez l'implémentation span de la bibliothèque de prise en charge des directives C++ :
    #include <gsl/span>
    // etc.
    Result decode(gsl::span<const std::byte> buffer);
    
  • Si vous souhaitez prendre en charge le décodage à partir de conteneurs autres que la mémoire brute, utilisez des itérateurs arbitraires (en C++ 17 et versions antérieures) ou éventuellement des plages (en C++ 20). La version itérateur:

    template <typename InputIt>
    Result decode(InputIt start, InputIt end) { /* etc. */ }
    
  • Il est louche qu'un Decoder hérite d'un Codec plutôt que l'inverse.

  • La question de savoir si les rappels sont un bon choix ou non est quelque chose qui est difficile (pour moi) de répondre sans voir le code. Mais utilisez en effet un std::variant pour exprimer le fait que vous avez un paquet ou des métadonnées; vous pouvez également "combiner" des alternatives si, au lieu de rappels, vous utilisez des variantes ' std::visit .
15
einpoklum

C++ 20 aura std::span , qui fait ce que vous voulez:

    Result decode(std::span<uint8_t const> buffer);

std::span<T> est sémantiquement équivalent à un T* buffer, size_t size.


En C++ 17, certaines implémentations de type span sont équivalentes à std::span, comme les GSLgsl::span. Voir Qu'est-ce qu'un "span" et quand dois-je en utiliser un? .

Si vous ne pouvez utiliser aucune bibliothèque externe, pensez à écrire votre propre type span, sinon uint8_t const* buffer_begin, uint8_t const* buffer_end peut marcher.

5
Justin