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
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>
.
Appel de la fonction decode
, passage des itérateurs pour leur vecteur.
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 metadata
opacket
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?
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.
En plus de la suggestion valide de @ Justin de spans :
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.
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
.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.