webentwicklung-frage-antwort-db.com.de

C-Compiler behauptet - wie zu implementieren?

Ich möchte ein "assert" implementieren, das im Fehlerfall die Kompilierung verhindert, anstatt zur Laufzeit fehlzuschlagen.

Ich habe derzeit eine solche Definition, die großartig funktioniert, aber die Größe der Binärdateien erhöht.

#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}

Beispielcode (der nicht kompiliert werden kann).

#define DEFINE_A 1
#define DEFINE_B 1
MY_COMPILER_ASSERT(DEFINE_A == DEFINE_B);

Wie kann ich dies implementieren, damit kein Code generiert wird (um die Größe der generierten Binärdateien zu minimieren)?

39
NickB

Eine Kompilierzeit-Assertion im reinen Standard-C ist möglich, und ein bisschen Pre-Prozessor-Trickery lässt die Verwendung genauso sauber aussehen wie die Laufzeitverwendung von assert().

Der Schlüsseltrick besteht darin, ein Konstrukt zu finden, das zur Kompilierzeit ausgewertet werden kann und bei einigen Werten einen Fehler verursachen kann. Eine Antwort ist, dass die Deklaration eines Arrays keine negative Größe haben kann. Die Verwendung einer Typedef verhindert die Zuteilung von Speicherplatz bei Erfolg und behält den Fehler bei einem Fehler bei.

Die Fehlernachricht selbst verweist kryptisch auf die Deklaration einer negativen Größe (GCC sagt "Größe des Arrays foo ist negativ"). Daher sollten Sie einen Namen für den Array-Typ auswählen, der darauf hinweist, dass es sich bei diesem Fehler tatsächlich um eine Assertionsprüfung handelt.

Ein weiterer zu behandelnder Punkt ist, dass es nur in einer beliebigen Übersetzungseinheit möglich ist, einen bestimmten Typnamen einmal typedef zu verwenden. Das Makro muss also für jede Verwendung eine Anordnung veranlassen, um einen eindeutigen Typnamen zu deklarieren.

Meine übliche Lösung bestand darin, dass das Makro zwei Parameter hat. Die erste ist die zu bestätigende Bedingung, und die zweite ist Teil des hinter den Kulissen angegebenen Typnamens. Die Antwort nach Plinthe weist auf die Verwendung des Token-Einfügens und des vordefinierten Makros __LINE__ hin, um einen eindeutigen Namen zu bilden, möglicherweise ohne ein zusätzliches Argument.

Wenn sich die Assertionsprüfung in einer eingeschlossenen Datei befindet, kann sie leider immer noch mit einer Prüfung an derselben Zeilennummer in einer zweiten eingeschlossenen Datei oder an dieser Zeilennummer in der Hauptquelldatei kollidieren. Wir könnten dies mit dem Makro __FILE__ überarbeiten, aber es ist als String-Konstante definiert, und es gibt keinen Präprozessor-Trick, der eine String-Konstante in einen Teil des Bezeichners zurückverwandeln kann. Es darf nicht erwähnt werden, dass legale Dateinamen Zeichen enthalten können, die keine rechtlichen Bestandteile eines Bezeichners sind.

Ich würde also folgendes Codefragment vorschlagen:

/** 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];

Eine typische Verwendung könnte etwa sein:

#include "CAssert.h"
...
struct foo { 
    ...  /* 76 bytes of members */
};
CASSERT(sizeof(struct foo) == 76, demo_c);

In GCC würde ein Assertionsfehler folgendermaßen aussehen:

 $ gcc -c demo.c 
 demo.c: 32: Fehler: Größe des Arrays `assertion_failed_demo_c_32 'ist negativ 
 $ 
40
RBerteig

Das folgende COMPILER_VERIFY(exp)-Makro funktioniert ziemlich gut.

 // Argumente kombinieren (nach dem Erweitern von Argumenten) 
 # definiere 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)] 
 
 # Definieren COMPILER_VERIFY (exp) CVERIFY (exp, __LINE __) 

Es funktioniert sowohl für C als auch für C++ und kann überall dort verwendet werden, wo ein Typedef erlaubt wäre. Wenn der Ausdruck "true" ist, wird ein Typedef für ein Array von 1 Zeichen generiert (was harmlos ist). Wenn der Ausdruck "false" ist, wird eine Typedef für ein Array von -1 Zeichen generiert, die im Allgemeinen zu einer Fehlermeldung führt. Der Ausdruck, der als Argument angegeben wird, kann alles sein, was eine Kompilierungszeitkonstante ergibt (also können Ausdrücke, die sizeof () verwenden, gut funktionieren). Dies macht es viel flexibler als

 # if (Ausdruck) 
 # Fehler 
 # Endif 

wo Sie auf Ausdrücke beschränkt sind, die vom Präprozessor ausgewertet werden können.

7

Wenn Ihr Compiler ein Präprozessor-Makro wie DEBUG oder NDEBUG einstellt, können Sie Folgendes machen (sonst könnten Sie dies in einem Makefile einrichten):

#ifdef DEBUG
#define MY_COMPILER_ASSERT(EXPRESSION)   switch (0) {case 0: case (EXPRESSION):;}
#else
#define MY_COMPILER_ASSERT(EXPRESSION)
#endif

Dann übernimmt Ihr Compiler nur für Debugbuilds.

4
dreamlax

Die beste Beschreibung, die ich bei statischen Assertions in C finden konnte, ist bei pixelbeat . Beachten Sie, dass statische Zusicherungen zu C++ 0X hinzugefügt werden und möglicherweise zu C1X passen, dies wird jedoch nicht für eine Weile der Fall sein. Ich weiß nicht, ob die Makros in dem Link, den ich angegeben habe, die Größe Ihrer Binärdateien erhöhen. Ich würde vermuten, dass dies nicht der Fall wäre, zumindest wenn Sie mit einem vernünftigen Optimierungsgrad kompilieren, aber Ihre Laufleistung kann variieren.

4
ChrisInEdmonton

Ich weiß, dass Sie sich für C interessieren, aber werfen Sie einen Blick auf C++ static_assert von boost. (Übrigens ist dies wahrscheinlich in C++ 1x verfügbar.)

Wir haben etwas Ähnliches für C++ gemacht:

 # define COMPILER_ASSERT (expr) Aufzählung {ARG_JOIN (CompilerAssertAtLine, __LINE__) = sizeof (char [(expr)? +1: -1])}} 

Dies funktioniert anscheinend nur in C++. Dieser Artikel beschreibt eine Möglichkeit, ihn für die Verwendung in C zu ändern.

4
leander

Wie Leander gesagt hat, werden statische Zusicherungen zu C++ 11 hinzugefügt, und jetzt haben sie dies.

static_assert(exp, message)

Zum Beispiel

#include "myfile.hpp"

static_assert(sizeof(MyClass) == 16, "MyClass is not 16 bytes!")

void doStuff(MyClass object) { }

Siehe die cppreferenzseite drauf.

3
Dylan

Definieren Sie beim Kompilieren der endgültigen Binärdateien MY_COMPILER_ASSERT als leer, damit die Ausgabe nicht im Ergebnis enthalten ist. Definieren Sie es nur so, wie Sie es zum Debuggen haben.

Aber wirklich, Sie werden nicht in der Lage sein, jede Behauptung auf diese Weise einzufangen. Einige machen zum Zeitpunkt der Kompilierung einfach keinen Sinn (wie die Behauptung, dass ein Wert nicht null ist). Sie können lediglich die Werte anderer #defines überprüfen. Ich bin nicht wirklich sicher, warum Sie das tun wollen.

2
Welbog

Die Verwendung von '#error' ist eine gültige Präprozessor-Definition, die die Kompilierung auf den meisten Compilern stoppt. Sie können dies einfach so tun, um beispielsweise die Kompilierung im Debugging zu verhindern:


#ifdef DEBUG
#error Please don't compile now
#endif
1
Steve Wranovsky

Ich fand dies, um die am wenigsten verwirrende Fehlermeldung für GCC zu geben. Alles andere hatte ein Suffix über eine negative Größe oder eine andere verwirrende Sache:

#define STATIC_ASSERT(expr, msg)   \
typedef char ______Assertion_Failed_____##msg[1];  __unused \
typedef char ______Assertion_Failed_____##msg[(expr)?1:2] __unused

verwendungsbeispiel:

 unsigned char testvar;
 STATIC_ASSERT(sizeof(testvar) >= 8, testvar_is_too_small);

Und die Fehlermeldung in gcc (ARM/GNU C-Compiler: 6.3.1):

conflicting types for '______Assertion_Failed_____testvar_is_too_small'
0
Joe