Articles tagués “exception

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 !


Things every Java developer must know about Exception handling

Je n’avais encore jamais reblogué, mais cet article est tellement bien écrit, il me semble être le candidat idéal pour un premier reblog !

10K-LOC

Exceptions are one of the most misunderstood (and misused) features of the Java programming language. This article describes the absolute minimum every Java developer must know about exceptions. It assumes that the reader is somewhat familiar with Java.

Historical Perspective

Back in the heyday of the « C » programming language, it was customary to return values such as -1 or NULL from functions to indicate errors. You can easily see why this isn’t a great idea – developers had to check and track possible return values and their meanings: a return value of 2 might indicate « host is down » error in library A, whereas in library B, it could mean « illegal filename ».

Attempts were made to standardize error checking by expecting functions to set a global variable with a defined value.

deleteme

James Gosling and other designers of the language felt that this approach would go against the design goals of Java. They wanted:

  1. a cleaner, robust and portable approach
  2. built in language support for error checking…

Voir l’article original 1 470 mots de plus


Java : try / catch / finally et return

Voici un code simple en Java :

package com.gradot.blog;

public class ExceptionAndReturn {
	public static final int NORMAL = 0;
	public static final int EXCEPTION = 42;
	public static final int FINALLY = 66;

	public static void main(String[] args) {
		System.out.println("foo returned " + foo());
	}

	public static int foo() {
		try {
			System.out.println("Try");
			Object o = null;
			o.getClass();
			return NORMAL;
		} catch (NullPointerException e) {
			System.out.println("Catch");
			return EXCEPTION;
		} finally {
			System.out.println("Finally");
			return FINALLY;
		}
	}
}

De manière tout à fait évidente, l’appel à o.getClass(); va provoquer une exception et on va entrer dans le bloc catch. Que va alors afficher ce code ? Quelle valeur est renvoyée ? Exécutons et regardons :

Try
Catch
Finally
foo returned 66

Le passage dans les 3 blocs de gestion d’exception est attendu. Le retour de la valeur 66 est peut-être moins évident mais en même temps c’est logique : on termine par un return FINALLY;.

Modifions légèrement la méthode foo() pour que ne survienne pas l’exception :

public static int foo() {
	try {
		System.out.println("Try");
		Object o = new Object();
		o.getClass();
		return NORMAL;
	} catch (NullPointerException e) {
		System.out.println("Catch");
		return EXCEPTION;
	} finally {
		System.out.println("Finally");
		return FINALLY;
	}
}

On ne rentre plus dans le bloc catch mais on a bien sûr le même problème : la valeur retournée dans le bloc finally écrase celle donnée par un éventuel précédent return. On obtient en console :
Try
Finally
foo returned 66

Revenons à une version du code provoquant une exception mais essayons de la relancer dans le catch plutôt que de retourner un code d’erreur :

public static int foo() {
	try {
		System.out.println("Try");
		Object o = null;
		o.getClass();
		return NORMAL;
	} catch (NullPointerException e) {
		System.out.println("Catch");
		throw e;
	} finally {
		System.out.println("Finally");
		return FINALLY;
	}
}

Quel affichage d’après vous ? Et bien, de manière un peu surprenante (bien que vous deviez sentir le piège), le return du bloc finally annule et remplace le throw du bloc catch. En console :

Try
Catch
Finally
foo returned 66

Tentons carrément de supprimer le bloc catch :

public static int foo() {
	try {
		System.out.println("Try");
		Object o = null;
		o.getClass();
		return NORMAL;
	} finally {
		System.out.println("Finally");
		return FINALLY;
	}
}

catch ou pas, le résultat est le même, le finally est exécuté et l’exception n’est pas remontée à l’appelant.

La conclusion est toute simple : ne mettez pas de return dans un finally. Vous masqueriez les précédents returns et vous bloqueriez la levée d’exception ! Vous avez eu chaud ? Pas tant que ça : si vous mettez les codes ci-dessus dans Eclipse (ou si vous le compiler manuellement), vous verrez un warning sur l’ensemble du bloc finally :

finally block does not complete normally