2009-04-30 12 views
31

Chciałbym zaimplementować "asert", który zapobiega kompilacji, a nie awarii w czasie wykonywania, w przypadku błędu.C kompilator zapewnia - jak zaimplementować?

Obecnie mam taki zdefiniowany, który działa świetnie, ale który zwiększa rozmiar plików binarnych.

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

Przykładowy kod (którego nie można skompilować).

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

Jak mogę wdrożyć to tak, że nie generuje żadnego kodu (w celu zminimalizowania wielkości plików binarnych generowanych)?

+0

Naprawdę nie sądzę, że możliwe jest utworzenie statycznego dowodu w zwykłym C, ale chcielibyśmy wiedzieć! –

+0

Powiel z kilkoma dobrymi odpowiedziami: http://stackoverflow.com/questions/174356/ways-to-assert-expressions-at-build-time-in-c –

+3

Ponieważ to pytanie jest stosunkowo stare: '_Static_assert' i powiązane z nim makro 'static_assert' jest standaryzowane jak w C11. To jest teraz wbudowane w język. – Leushenko

Odpowiedz

35

Możliwe jest wykonanie kompilacji w czystym standardzie C, a odrobina preprocesora sprawia, że ​​jego użycie wygląda tak samo czysto, jak użycie środowiska wykonawczego assert().

Kluczową sztuczką jest znalezienie konstruktu, który może być oceniany w czasie kompilacji i może powodować błąd w przypadku niektórych wartości. Jedną z odpowiedzi jest deklaracja tablicy, która nie może mieć ujemnego rozmiaru. Użycie parametru typedef zapobiega przydzieleniu miejsca na sukces i zapobiega błędom w przypadku niepowodzenia.

Sam komunikat o błędzie kryptycznie odwoła się do deklaracji o rozmiarze ujemnym (GCC mówi "rozmiar tablicy foo jest ujemny"), więc powinieneś wybrać nazwę typu tablicy, która podpowiada, że ​​ten błąd naprawdę jest sprawdzaniem asercji .

Kolejną kwestią do rozwiązania jest to, że możliwe jest tylko jednoznaczne wpisanie nazwy pewnego typu w dowolnej jednostce kompilacji. Tak więc makro musi zorganizować każde użycie, aby uzyskać unikalną nazwę do deklaracji.

Moim zwykłym rozwiązaniem było wymaganie, aby makro miało dwa parametry. Pierwszym z nich jest warunek potwierdzenia, a drugi jest częścią nazwy typu zadeklarowanej za kulisami. Odpowiedź cokołu sugeruje użycie wklejonego tokena i predefiniowanego makra, aby utworzyć niepowtarzalną nazwę, bez konieczności dodatkowego argumentu.

Niestety, jeśli kontrola asercji znajduje się w dołączonym pliku, nadal może kolidować z zaznaczeniem na tym samym numerze linii w drugim dołączonym pliku lub na tym numerze linii w głównym pliku źródłowym. Moglibyśmy napisać o tym za pomocą makra __FILE__, ale jest on zdefiniowany jako stała łańcuchowa i nie istnieje preprocesorowa sztuczka, która może zamienić ciąg z powrotem na część nazwy identyfikatora; nie wspominając, że legalne nazwy plików mogą zawierać znaki, które nie są legalnymi częściami identyfikatora.

Tak, chciałbym zaproponować następujący fragment kodu:

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

Typowe wykorzystanie może być coś takiego:

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

W GCC, awaria twierdzenie wyglądałby następująco:

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

Jeśli nie zależy Ci na przenośności, w GCC \ _ \ _ COUNTER \ _ \ _ można użyć do nadania unikalnego identyfikatora do wklejenia na nazwę typedef. Został dodany całkiem niedawno (4.3). –

+0

Byłem zaskoczony, że preprocesor C nigdy nie miał czegoś takiego jak __COUNTER__. Asemblery makro mają podobne konstrukcje, o ile istnieją makrobloki asemblerowe, nie mówiąc już o możliwości zdefiniowania jednego makra, które może być użyte do zbudowania dowolnego unikalnego symbolu, który może być potrzebny. Na nieszczęście dla mnie projekty systemów wbudowanych generalnie są zablokowane gcc w wersji 3.4.5, jeśli nie jest to jakiś zastrzeżony kompilator, który (głównie) jest zgodny z C89. – RBerteig

+0

@RBerteig Witam, chciałbym użyć tego kodu do projektu Open Source. Na co byś ją licencjonował? –

2

Podczas kompilacji końcowych plików binarnych zdefiniuj wartość MY_COMPILER_ASSERT jako pustą, aby jej wynik nie został uwzględniony w wyniku. Zdefiniuj go tylko tak, jak masz to do debugowania.

Ale tak naprawdę, nie będziesz w stanie uchwycić każdego twierdzenia w ten sposób. Niektóre po prostu nie mają sensu w czasie kompilacji (jak twierdzenie, że wartość nie jest zerowa). Wszystko, co możesz zrobić, to sprawdzić wartości innych #defines. Nie jestem do końca pewien, dlaczego chciałbyś to zrobić.

+4

Jest to przydatne, ponieważ wszystko, co można zweryfikować podczas kompilacji, nie wymagało testu w czasie wykonywania. Podczas budowania implementacji protokołu przenośnego warto zweryfikować założenia dotyczące rozmiaru i układu struktury. Ponieważ rozmiary i przesunięcia są znane podczas kompilacji, preferowane jest ich testowanie. Ponadto implementacja asserta podczas kompilacji bez generowania kodu oznacza, że ​​nie ma powodu, aby go wyrzucać z wersji wydań. W najgorszym przypadku przykrywa tablicę symboli nazwami sierocych typów. – RBerteig

3

Jeśli kompilator ustawia makra preprocesora jak debug lub NDEBUG można zrobić coś takiego (inaczej można to ustawić w Makefile):

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

Następnie kompilator stwierdza tylko dla debugowania buduje.

1

Użycie "#error" jest poprawną definicją preprocesora, która powoduje zatrzymanie kompilacji na większości kompilatorów. Można po prostu zrobić to w ten sposób, na przykład, aby zapobiec kompilację w debug:


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

Niestety to przerywa na poziomie preprocesora, więc nie jest w stanie obsłużyć takich rzeczy jak 'assert (sizeof (long) == sizeof (void *))'. – ephemient

4

Najlepszym writeup że udało mi się znaleźć na twierdzeń statycznych w C jest pixelbeat. Zauważ, że asercje statyczne są dodawane do C++ 0X i mogą być wprowadzane do C1X, ale to nie potrwa długo. Nie wiem, czy makra w linku podałem zwiększy rozmiar plików binarnych. Podejrzewam, że tak by się nie stało, przynajmniej jeśli kompilujesz na rozsądnym poziomie optymalizacji, ale twój przebieg może się różnić.

-1

Cóż, możesz użyć static asserts in the boost library.

To, w co wierzę, że tam robią, polega na zdefiniowaniu tablicy.

#define MY_COMPILER_ASSERT(EXPRESSION) char x[(EXPRESSION)]; 

Jeśli wyrażenie jest prawdziwe, to definiuje char x[1];, który jest OK. Jeśli wartość jest nieprawidłowa, definiuje ona wartość char x[0];, która jest nielegalna.

+0

Ale w C89, które mogłyby przerwać kompilację, niezależnie od tego, ponieważ zmienne mogą być zadeklarowane tylko na górze zakresu. Może może to być zawijanie w nawiasy klamrowe? Również tablice o zerowym rozmiarze są w moim odczuciu tylko zabronione. Otrzymasz także mnóstwo ostrzeżeń o wielu deklaracjach i nieużywanych zmiennych, jeśli nie we własnym zakresie. – dreamlax

+1

'#define MY_COMPILER_ASSERT (EXPRESSION) zrobić {char x [(EXPRESSION)? 1: -1];} while (0)' byłoby lepiej: wewnątrz nawiasów klamrowych, więc deklaracja jest legalna i ma zasięg, rozmiary 1 i -1 są wyraźnie poprawne/nieważne we wszystkich wersjach C i zmusza do "MY_COMPILER_ASSERT (...);" z końcowym średnikiem, dla wizualnej spójności z wszystkimi innymi funkcyjnymi elementami w C. – ephemient

+0

Deklaracja, że ​​typedef jest lepszy niż deleting nieużywanej zmiennej . Nieużywany typedef jest nieszkodliwy, ale niewykorzystana zmienna może sama generować ostrzeżenie dla wielu kompilatorów. –

3

Wiem, że jesteś zainteresowany C, ale spójrz na boost C++ static_assert. (Nawiasem mówiąc, jest to prawdopodobne stają się dostępne w C++ 1x).

Robiliśmy coś podobnego, znowu dla C++:

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

ta działa tylko w języku C++, widocznie. This article omawia sposób modyfikowania go do użycia w C.

7

Następujące makro COMPILER_VERIFY(exp) działa całkiem dobrze.

 
// 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__) 

Działa zarówno w językach C, jak i C++ i może być używany wszędzie tam, gdzie dozwolony byłby typedef. Jeśli wyrażenie jest prawdziwe, generuje typedef dla tablicy 1 char (która jest nieszkodliwa). Jeśli wyrażenie jest fałszywe, generuje on typedef dla tablicy -1 znaków, co zwykle skutkuje komunikatem o błędzie. Wyrażenie podane jako przerost może być dowolną wartością określającą stałą czasu kompilacji (więc wyrażenia obejmujące sizeof() działają poprawnie). To sprawia, że ​​jest on znacznie bardziej elastyczny niż w przypadku ograniczania się do wyrażeń, które mogą zostać ocenione przez preprocesor.

2

Jak powiedział Leander, do C++ 11 dodawane są asercje statyczne, a teraz mają.

static_assert(exp, message)

Na przykład

#include "myfile.hpp" 

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

void doStuff(MyClass object) { } 

Zobacz cppreference page na nim.

+2

Pytanie dotyczy C nie C++. –