C-Compiler-Asserts – wie implementieren?

Lesezeit: 8 Minuten

Benutzeravatar von NickB
Nick B

Ich möchte ein “Assert” implementieren, das die Kompilierung verhindert, anstatt im Fehlerfall zur Laufzeit fehlzuschlagen.

Ich habe derzeit eine solche definiert, 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)?

  • Ich glaube wirklich nicht, dass es möglich ist, ein statisches Assert in einfachem C zu erstellen, würde es aber gerne wissen!

    – Robert Gould

    30. April 2009 um 14:42 Uhr

  • Duplizieren mit mehreren guten Antworten: stackoverflow.com/questions/174356/…

    – Michael Burr

    30. April 2009 um 15:07 Uhr

  • Da diese Frage relativ alt ist: _Static_assert und das zugehörige Makro static_assert sind ab C11 standardisiert. Dies ist jetzt in die Sprache integriert.

    – Leuschenko

    22. November 2015 um 2:00 Uhr

  • Sicherlich kann der Optimierer leere Schalter wegwerfen.

    – Trevor Hickey

    17. Februar 2017 um 19:13 Uhr

Ein Assertion zur Kompilierzeit in reinem Standard-C ist möglich, und ein wenig Präprozessor-Trickserei lässt seine Verwendung genauso sauber aussehen wie die Verwendung von zur Laufzeit 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 darf. Die Verwendung einer Typedef verhindert die Zuweisung von Speicherplatz bei Erfolg und behält den Fehler bei einem Fehler bei.

Die Fehlermeldung selbst bezieht sich kryptisch auf die Deklaration einer negativen Größe (GCC sagt “Größe des Arrays foo ist negativ”), also sollten Sie einen Namen für den Array-Typ wählen, der darauf hinweist, dass dieser Fehler wirklich eine Behauptungsprüfung ist.

Ein weiteres zu behandelndes Problem ist, dass es nur möglich ist typedef einen bestimmten Typnamen einmal in einer Kompilationseinheit. Das Makro muss also dafür sorgen, dass jede Verwendung einen eindeutigen Typnamen zum Deklarieren erhält.

Meine übliche Lösung bestand darin, zu verlangen, dass das Makro zwei Parameter hat. Die erste ist die zu bestätigende Bedingung, und die zweite ist Teil des Typnamens, der hinter den Kulissen deklariert wird. Die Antwort von Sockel deutet auf die Verwendung von Token-Einfügen und der __LINE__ vordefiniertes Makro, um einen eindeutigen Namen zu bilden, möglicherweise ohne ein zusätzliches Argument zu benötigen.

Befindet sich die Assertion-Prüfung in einer eingebundenen Datei, kann sie leider immer noch mit einer Prüfung an derselben Zeilennummer in einer zweiten eingebundenen Datei oder an dieser Zeilennummer in der Hauptquelldatei kollidieren. Wir könnten das mit dem Makro überspielen __FILE__, aber es ist als String-Konstante definiert und es gibt keinen Präprozessor-Trick, der eine String-Konstante wieder in einen Teil eines Bezeichnernamens umwandeln kann; Ganz zu schweigen davon, dass legale Dateinamen Zeichen enthalten können, die keine legalen Teile eines Bezeichners sind.

Also würde ich 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 so aussehen:

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

In GCC würde ein Behauptungsfehler wie folgt aussehen:

$ gcc -c demo.c
demo.c:32: error: size of array `assertion_failed_demo_c_32' is negative
$

  • Wenn Sie sich nicht um Portabilität kümmern, kann in GCC __COUNTER__ verwendet werden, um eine eindeutige Kennung anzugeben, die in den Typedef-Namen eingefügt werden kann. Es wurde vor kurzem hinzugefügt (4.3)

    – Greg Rogers

    17. Juli 2009 um 15:52 Uhr

  • Ich war erstaunt, dass der C-Präprozessor so etwas noch nie hatte ZÄHLER. Makro-Assembler hatten ähnliche Konstrukte, solange es Makro-Assembler gab, ganz zu schweigen von der Fähigkeit eines Makros, ein anderes zu definieren, das verwendet werden kann, um jede Art von einzigartigem Symbol zu erstellen, das man möglicherweise benötigt. Unglücklicherweise bleiben meine Projekte für eingebettete Systeme im Allgemeinen bei gcc 3.4.5 oder so hängen, wenn nicht bei einem proprietären Compiler, der (meistens) mit C89 kompatibel ist.

    – RBerteig

    17. Juli 2009 um 20:03 Uhr

  • @RBerteig Hallo, ich möchte diesen Code für ein Open Source Projekt verwenden. Unter was würden Sie es lizenzieren?

    – Hannes Landholm

    19. August 2014 um 12:26 Uhr

  • @HannesLandeholm Wenn ich es selbst veröffentlichen würde, würde ich normalerweise die MIT-Lizenz anwenden. Da es hier ohne weitere Diskussion veröffentlicht wird, kann davon ausgegangen werden, dass die allgemeine Behauptung, dass Benutzerinhalte lizenziert sind, als CC-BY-SA gilt. Zu diesem Zweck könnten Sie einfach „CC-BY-SA 3.0 from stackoverflow.com/a/809465/68204“ zum Kommentarblock hinzufügen. Ich persönlich ziehe es vor, dem Code nicht die Einschränkung „Teilen unter gleichen Bedingungen“ aufzuerlegen, aber eine Namensnennung und ein Link hier sind immer angemessen.

    – RBerteig

    19. August 2014 um 20:14 Uhr

  • Dieser Ansatz löst nun unglücklicherweise GCC-Warnungen über unbenutzte Typedefs aus.

    – Kas

    4. März um 19:20 Uhr

Folgende COMPILER_VERIFY(exp) Makro funktioniert ziemlich gut.

// combine arguments (after expanding 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__)

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 wahr ist, generiert er eine Typedef für ein Array von 1 Zeichen (was harmlos ist). Wenn der Ausdruck falsch ist, generiert er eine Typedef für ein Array von -1 Zeichen, was im Allgemeinen zu einer Fehlermeldung führt. Der als Argument angegebene Ausdruck kann alles sein, was zu einer Konstante zur Kompilierzeit ausgewertet wird (Ausdrücke mit sizeof() funktionieren also gut). Dies macht es viel flexibler als

#if (expr)
#error
#endif

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

  • Das gefällt mir, aber es verursacht eine Warnung für eine nicht verwendete deklarierte Variable: main.c:22:47: warning: typedef ‘compiler_verify_39’ local defined but not used [-Wunused-local-typedefs]. Da mein Projekt Warnungen als Fehler behandelt, kann ich dies nicht verwenden.

    – Alex

    12. Juli 2019 um 10:44 Uhr


  • Die Dinge haben sich in den zehn Jahren, seit ich diese Antwort gepostet habe, weiterentwickelt. Wenn Ihr Compiler C11 unterstützt, können Sie das _Static_assert verwenden, das jetzt Teil der Sprache ist.

    – Stephen C. Stahl

    12. Juli 2019 um 21:50 Uhr

  • @Alex, diese Warnung kann mit stummgeschaltet werden (void)(sizeof(TYPEDEF_NAME));. Möglicherweise benötigen Sie eine zusätzliche Dereferenzierungsebene, um GLUE als Parameter an das CVERIFY()-Makro zu übergeben, damit Sie einen TYPEDEF_NAME haben, mit dem Sie arbeiten können. Und (void)ENTITY; ist eine allgemeine Methode, um „unbenutzte“ Warnungen zu verwerfen.

    – Jetski S-Typ

    8. November 2021 um 23:45 Uhr

Wie Leander sagte, werden statische Zusicherungen zu C++11 hinzugefügt, und jetzt haben sie es getan.

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 cpReferenzseite darauf.

  • Die Frage bezieht sich auf C, nicht auf C++.

    – Trevor Hickey

    17. Februar 2017 um 18:57 Uhr

  • C11 bietet die ähnlich benannten _Static_assert (siehe stackoverflow.com/a/7287341/446106). Zusätzlich, assert.h enthält ein static_assert Makro, das darauf abgebildet wird (en.cppreference.com/w/c/error/static_assert)

    – mwfearnley

    18. Februar 2019 um 23:05 Uhr

Wenn Ihr Compiler ein Präprozessor-Makro wie DEBUG oder NDEBUG setzt, können Sie so etwas machen (andernfalls 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 behauptet Ihr Compiler nur für Debug-Builds.

Die Verwendung von „#error“ ist eine gültige Präprozessordefinition, die dazu führt, dass die Kompilierung bei den meisten Compilern angehalten wird. Sie können es zum Beispiel einfach so machen, um das Kompilieren im Debug zu verhindern:


#ifdef DEBUG
#error Please don't compile now
#endif

  • Leider bricht dies auf der Präprozessorebene ab, sodass Dinge wie nicht verarbeitet werden können assert(sizeof(long) == sizeof(void *)).

    – vergänglich

    30. April 2009 um 15:17 Uhr

Benutzeravatar von ChrisInEdmonton
ChrisInEdmonton

Die beste Beschreibung, die ich zu statischen Behauptungen in C finden konnte, ist unter Pixelschlag. Beachten Sie, dass statische Zusicherungen zu C++ 0X hinzugefügt werden und es möglicherweise in C1X schaffen, aber das wird noch eine Weile dauern. Ich weiß nicht, ob die Makros in dem von mir angegebenen Link die Größe Ihrer Binärdateien erhöhen. Ich würde vermuten, dass sie dies nicht tun würden, zumindest wenn Sie auf einem angemessenen Optimierungsniveau kompilieren, aber Ihre Laufleistung kann variieren.

  • Leider bricht dies auf der Präprozessorebene ab, sodass Dinge wie nicht verarbeitet werden können assert(sizeof(long) == sizeof(void *)).

    – vergänglich

    30. April 2009 um 15:17 Uhr

Leanders Benutzeravatar
schlanker

Ich weiß, dass Sie an C interessiert sind, aber werfen Sie einen Blick auf C++ von boost static_assert. (Dies wird übrigens wahrscheinlich in C++1x verfügbar sein.)

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

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

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

1409470cookie-checkC-Compiler-Asserts – wie implementieren?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy