Je voudrais implémenter un "assert" qui empêche la compilation, plutôt que d'échouer au moment de l'exécution, dans le cas d'erreur.
J'ai actuellement une définition comme celle-ci, qui fonctionne très bien, mais qui augmente la taille des fichiers binaires.
#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}
Exemple de code (qui ne compile pas).
#define DEFINE_A 1
#define DEFINE_B 1
MY_COMPILER_ASSERT(DEFINE_A == DEFINE_B);
Comment puis-je implémenter cela afin qu'il ne génère aucun code (afin de minimiser la taille des fichiers binaires générés)?
Une assertion à la compilation en C standard pur est possible, et un peu de ruse du préprocesseur rend son utilisation aussi nette que celle de l'exécution assert()
.
L'astuce consiste à trouver une construction pouvant être évaluée au moment de la compilation et pouvant générer une erreur pour certaines valeurs. Une réponse est la déclaration d'un tableau ne peut pas avoir une taille négative. L'utilisation d'un typedef empêche l'allocation d'espace en cas de succès et préserve l'erreur en cas d'échec.
Le message d'erreur lui-même fera référence de manière cryptique à la déclaration d'une taille négative (GCC dit "la taille du tableau foo est négative"), vous devez donc choisir un nom pour le type de tableau qui laisse entendre que cette erreur est vraiment une vérification d'assertion.
Un autre problème à gérer est qu’il n’est possible de typedef
qu’un nom de type particulier une fois dans une unité de compilation. La macro doit donc organiser pour chaque utilisation un nom de type unique à déclarer.
Ma solution habituelle a été d'exiger que la macro ait deux paramètres. Le premier est la condition pour affirmer est vraie, et le second fait partie du nom de type déclaré dans les coulisses. La réponse par plinthe suggère l'utilisation du collage de jetons et de la macro prédéfinie __LINE__
pour former un nom unique, éventuellement sans nécessiter d'argument supplémentaire.
Malheureusement, si la vérification d'assertion est dans un fichier inclus, elle peut toujours entrer en collision avec une vérification au même numéro de ligne dans un deuxième fichier inclus ou à ce numéro de ligne dans le fichier source principal. Nous pourrions écrire sur cela en utilisant la macro __FILE__
, mais il est défini comme une constante de chaîne et il n'y a aucune astuce de préprocesseur qui puisse transformer une constante de chaîne en partie d'un nom d'identifiant; sans oublier que les noms de fichiers légaux peuvent contenir des caractères qui ne font pas partie légale d'un identifiant.
Donc, je proposerais le fragment de code suivant:
/** A compile time assertion check.
*
* Validate at compile time that the predicate is true without
* generating code. This can be used at any point in a source file
* where typedef is legal.
*
* On success, compilation proceeds normally.
*
* On failure, attempts to typedef an array type of negative size. The
* offending line will look like
* typedef assertion_failed_file_h_42[-1]
* where file is the content of the second parameter which should
* typically be related in some obvious way to the containing file
* name, 42 is the line number in the file on which the assertion
* appears, and -1 is the result of a calculation based on the
* predicate failing.
*
* \param predicate The predicate to test. It must evaluate to
* something that can be coerced to a normal C boolean.
*
* \param file A sequence of legal identifier characters that should
* uniquely identify the source file in which this condition appears.
*/
#define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)
#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line, file) \
typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];
Un usage typique pourrait être quelque chose comme:
#include "CAssert.h"
...
struct foo {
... /* 76 bytes of members */
};
CASSERT(sizeof(struct foo) == 76, demo_c);
Dans GCC, un échec d'assertion ressemblerait à ceci:
$ gcc -c demo.c demo.c: 32: erreur: la taille du tableau `assertion_failed_demo_c_32 'est négative $
La macro COMPILER_VERIFY(exp)
suivante fonctionne assez bien.
// combine les arguments (après avoir développé les arguments) # define GLUE (a, b) __GLUE (a, b) # define __GLUE (a, b) a ## b #define CVERIFY (expr, msg) typedef char GLUE (compiler_verify_, msg) [(expr)? (+1): (-1)] # Define COMPILER_VERIFY (exp) CVERIFY (exp, __LINE __)
Cela fonctionne à la fois en C et en C++ et peut être utilisé partout où un typedef serait autorisé. Si l'expression est vraie, elle génère un typedef pour un tableau de 1 caractère (ce qui est sans danger). Si l'expression est false, elle génère une typedef pour un tableau de -1 caractères, ce qui entraîne généralement un message d'erreur. L'expression donnée en tant qu'arugment peut être n'importe quoi qui se traduit par une constante de compilation (les expressions impliquant sizeof () fonctionnent donc bien). Cela le rend beaucoup plus flexible que
# if (expr) # error # endif
où vous êtes limité aux expressions pouvant être évaluées par le préprocesseur.
Si votre compilateur définit une macro de préprocesseur telle que DEBUG ou NDEBUG, vous pouvez créer quelque chose comme ceci (sinon vous pouvez le définir dans un Makefile):
#ifdef DEBUG
#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}
#else
#define MY_COMPILER_ASSERT(EXPRESSION)
#endif
Ensuite, votre compilateur affirme uniquement pour les générations de débogage.
La meilleure écriture que je puisse trouver sur les assertions statiques en C est à pixelbeat . Notez que des assertions statiques sont ajoutées à C++ 0X et peuvent être intégrées à C1X, mais cela ne va pas durer longtemps. Je ne sais pas si les macros du lien que j'ai donné augmenteront la taille de vos fichiers binaires. J'imagine qu'ils ne le feraient pas, du moins si vous compilez à un niveau d'optimisation raisonnable, mais votre kilométrage peut varier.
Je sais que vous êtes intéressé par le C, mais jetez un coup d'œil au C++ static_assert de boost. (Incidemment, cela deviendra probablement disponible en C++ 1x.)
Nous avons fait quelque chose de similaire, encore une fois pour C++:
# define COMPILER_ASSERT (expr) enum {ARG_JOIN (CompilerAssertAtLine, __LINE__) = sizeof (char [(expr)? +1: -1])}
Cela ne fonctionne apparemment qu'en C++. Cet article discute d'un moyen de le modifier pour l'utiliser en C.
Lorsque vous compilez vos fichiers binaires finaux, définissez MY_COMPILER_ASSERT comme vide, afin que sa sortie ne soit pas incluse dans le résultat. Définissez-le uniquement de la manière dont vous l'avez utilisé pour le débogage.
Mais en réalité, vous ne pourrez pas saisir chaque affirmation de cette façon. Certains n’ont tout simplement pas de sens au moment de la compilation (comme l’affirmation selon laquelle une valeur n’est pas nulle). Tout ce que vous pouvez faire est de vérifier les valeurs des autres #defines. Je ne sais pas vraiment pourquoi tu voudrais faire ça.
Utiliser '#error' est une définition de préprocesseur valide qui provoque l'arrêt de la compilation sur la plupart des compilateurs. Vous pouvez simplement le faire comme ceci, par exemple, pour empêcher la compilation dans le débogage:
#ifdef DEBUG
#error Please don't compile now
#endif
J'ai trouvé que cela donnait le message d'erreur le moins déroutant pour GCC. Tout le reste avait un suffixe sur une taille négative ou une autre chose déroutante:
#define STATIC_ASSERT(expr, msg) \
typedef char ______Assertion_Failed_____##msg[1]; __unused \
typedef char ______Assertion_Failed_____##msg[(expr)?1:2] __unused
exemple d'utilisation:
unsigned char testvar;
STATIC_ASSERT(sizeof(testvar) >= 8, testvar_is_too_small);
Et le message d'erreur dans gcc (Compilateur ARM/GNU C: 6.3.1):
conflicting types for '______Assertion_Failed_____testvar_is_too_small'