Articles tagués “variant

std::variant en C++17

Il y a peu, j’ai lu des trucs sur std::variant, une des nouvelles fonctionnalités de C++17. std::variant est vendu comme étant une union type-safe. Vous n’êtes pas sans ignorer les risques liés à l’utilisation des unions : vous avez une union entre un entier et d’une string, vous affectez un entier à votre union, vous essayez de lire la string, ça vous renvoie n’importe quoi. Sauf que vous n’avez aucun moyen de le savoir… std::variant vient régler ces problèmes. Ce template de classes permet de tester le membre actif et lance des exceptions si on tente d’accéder à un autre membre.

Le lendemain de mes lectures, j’ai eu besoin de faire évoluer du code et je me suis dit que std::variant pourrait répondre à mon besoin. Oui, j’ai la chance de coder en C++17 au boulot ! Le code en question est concerne la gestion d’un bus SPI. Un thread est chargé de protéger l’accès au bus, de le reconfigurer si besoin, de sélectionner / désélectionner les périphériques, de faire des statistiques d’utilisation. Quand un autre thread a besoin de faire des lectures et des écritures sur le bus, il poste une requête qui est traité de manière asynchrone. Ça fonctionne très bien mais la manière dont est implémenté la classe Request n’est pas parfaite. Voici à quoi elle ressemble :

enum class RequestType {
	READ, WRITE, WRITE_READ
};

struct Request {
	RequestType type;
	
	std::uint8_t* const readData;
	const std::size_t readLength;

	const std::uint8_t* const writeData;
	const std::size_t writeLength;
};

Voici des exemples de création de requêtes :

	std::array<std::uint8_t, 15> readBuffer;
	std::array<std::uint8_t, 15> writeBuffer;

	Request read{RequestType::READ, readBuffer.data(), readBuffer.size(),
                                nullptr, 0};

	Request writeRead{RequestType::WRITE_READ, readBuffer.data(), readBuffer.size(),
                                writeBuffer.data(), writeBuffer.size()};

	Request bad{RequestType::WRITE, readBuffer.data(), readBuffer.size(),
                                nullptr, 0};

On voit plusieurs défauts :

  • des paramètres qui ne servent pas (pour read),
  • des paramètres dans un ordre pas forcément logique (pour writeRead, il faut donner les paramètres lecture puis d’écriture alors qu’on s’attendrait à l’inverse),
  • des paramètres qui peuvent être non consistants (pour bad mais un constructeur avec des assertions pourrait contrecarrer de telles erreurs).

Enfin, le plus gros défaut est le bazar induit par l’ajout d’un nouveau type de requête. J’ai par exemple besoin de rajouter d’un type de requête pour faire deux écritures consécutives. Soit je réutilise les membres readData et readLength mais c’est dégueulasse ; soit je rajoute deux membres à la structure et je dois retoucher toutes les créations de requêtes existantes…

C’est là que je me suis dit que j’allais testé std::variant pour améliorer la situation. L’idée est de définir 4 structures, une pour chaque type de requête, puis d’instancier std::variant avec ces 4 types. Se pose ensuite la question de traiter les requêtes dans la classe SpiBus. Le switch/case sur le champ Request::type (qui n’existe plus) peut se remplacer par des appels à std::variant::index() et à std::get(), mais il y a mieux. En effet, std::variant vient avec son copain std::visit. Cette fonction prend en paramètre un visiteur et un std::variant, détermine le membre actif et appelle l’opérateur du visiteur prenant en paramètre le type du membre actif.

Mes expérimentations m’ont amenés à ce code :

Rajouter la nouvelle requête DoubleWrite a été simple : j’ai créé ma nouvelle structure, je l’ai ajouté à la liste des types de Request et j’ai implémenté un nouvel opérateur dans RequestProcessor. Si on oublie cette dernière étape, le code ne compile pas donc on est certain que le dispatch se fera bien si le code compile. La création de requête plus simple et moins sujette à erreur. Le traitement est aussi simplifié : chaque type de requête est traité dans sa propre fonction dans laquelle on ne voit pas les membres qui ne servent à rien. Comprendre : je ne peux plus utiliser readData alors que je traite une requête de type WRITE par exemple.

Il se pose encore la question de la consommation mémoire de tout ce beau monde, parce que dans mon cas, ça doit tourner dans un Cortex-M4. Encore et toujours, Compiler Explorer est là. Il s’avère que ça génère pas mal de code… Trop pour que je puisse en faire une capture d’écran. Cliquez-ici et regarder ce que ça donne. Ça pique un peu mais c’est logique, il y a des vérifications pour lancer des exceptions en cas de mauvaise utilisation de l’union, même si ici, grâce à std::visit, on ne risque rien normalement. Il s’avère que les exceptions sont désactivées dans mon projet, donc je peux l’option -fno-exceptions et voir ce que ça donne (cliquez pour agrandir) :

Yeah ! C’est bon ça !

Au final, j’ai implémenté tout ça dans le vrai code et j’ai a gagné un peu de place en flash puisque j’ai viré du code de vérification lors de la création de requêtes. Et le même jour j’ai rajouté très facilement un cinquième type de requête… Bref, une bonne amélioration !

Bonne gestion des variantes !