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 :

#include <cassert>
#include <cstdint>
#include <variant>
struct ReadRequest {
ReadRequest(std::uint8_t* data, std::size_t length) :
data(data),
length(length) {
assert(data != nullptr);
}
std::uint8_t* const data;
const std::size_t length;
};
struct WriteRequest {
WriteRequest(const std::uint8_t* data, std::size_t length) :
data(data),
length(length) {
assert(data != nullptr);
}
const std::uint8_t* const data;
const std::size_t length;
};
struct WriteReadRequest {
WriteReadRequest(const std::uint8_t* writeData, std::size_t writeLength,
std::uint8_t* readData, std::size_t readLength) :
readData(readData),
readLength(readLength),
writeData(writeData),
writeLength(writeLength) {
assert(readData != nullptr);
assert(writeData != nullptr);
}
std::uint8_t* const readData;
const std::size_t readLength;
const std::uint8_t* const writeData;
const std::size_t writeLength;
};
struct DoubleWriteRequest {
DoubleWriteRequest(const std::uint8_t* firstData, std::size_t firstLength,
const std::uint8_t* secondData, std::size_t secondLength) :
firstData(firstData),
firstLength(firstLength),
secondData(secondData),
secondLength(secondLength) {
assert(firstData != nullptr);
assert(secondData != nullptr);
}
const std::uint8_t* const firstData;
const std::size_t firstLength;
const std::uint8_t* const secondData;
const std::size_t secondLength;
};
using Request =std::variant<ReadRequest,
WriteRequest,
WriteReadRequest,
DoubleWriteRequest>;
//———————————————————————————
extern "C" {
void spi_driver_read(std::uint8_t* data, std::size_t length);
void spi_driver_write(const std::uint8_t* data, std::size_t length);
}
class SpiBus {
public:
void execute(const Request& request) {
RequestProcessor requestProcessor;
std::visit(requestProcessor, request);
}
private:
class RequestProcessor {
public:
void operator()(const ReadRequest& request) {
spi_driver_read(request.data, request.length);
}
void operator()(const WriteRequest& request) {
spi_driver_write(request.data, request.length);
}
void operator()(const WriteReadRequest& request) {
spi_driver_write(request.writeData, request.writeLength);
spi_driver_read(request.readData, request.readLength);
}
void operator()(const DoubleWriteRequest& request) {
spi_driver_write(request.firstData, request.firstLength);
spi_driver_write(request.secondData, request.secondLength);
}
};
};
//——————————————————————————–
#include <array>
int main() {
SpiBus bus;
std::array<std::uint8_t, 15> buffer;
ReadRequest rr{buffer.data(), buffer.size()};
bus.execute(rr);
//ReadRequest bad_rr{nullptr, 42}; // triggers assert()
WriteRequest wr{buffer.data(), buffer.size()};
bus.execute(wr);
WriteReadRequest wrr{buffer.data(), 2, buffer.data(), 13};
bus.execute(wrr);
DoubleWriteRequest dwr{buffer.data(), 10, buffer.data(), 5};
bus.execute(dwr);
}

view raw
main.cpp
hosted with ❤ by GitHub

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 !