Po co używać pozornie bezsensownych instrukcji do-while I if-else w makrach?
W wielu makrach C / C++ widzę kod makra zawinięty w coś, co wydaje się być bezsensowną pętlą do while
. Oto przykłady.
#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else
Nie widzę, co robi do while
. Dlaczego nie napisać tego bez tego?
#define FOO(X) f(X); g(X)
10 answers
do ... while
i if ... else
są po to, aby
średnik po makrze zawsze oznacza to samo. Powiedzmy, że ty
miałem coś takiego jak twoje drugie makro.
#define BAR(X) f(x); g(x)
Teraz, jeśli miałbyś użyć BAR(X);
w if ... else
deklaracji, gdzie ciała deklaracji if nie były owinięte nawiasami klamrowymi, miałbyś złą niespodziankę.
if (corge)
BAR(corge);
else
gralt();
Powyższy kod rozszerzyłby się na
if (corge)
f(corge); g(corge);
else
gralt();
Co jest niepoprawne składniowo, ponieważ else nie jest już związane z if. Nie pomaga zawijanie rzeczy w nawiasach klamrowych wewnątrz makra, ponieważ średnik po nawiasach jest niepoprawny składniowo.
if (corge)
{f(corge); g(corge);};
else
gralt();
Istnieją dwa sposoby rozwiązania problemu. Pierwszym z nich jest użycie przecinka do sekwencji wyrażeń w makrze bez pozbawiania go zdolności do działania jak wyrażenie.
#define BAR(X) f(X), g(X)
Powyższa wersja bar BAR
rozszerza powyższy kod do tego, co następuje, co jest poprawne składniowo.
if (corge)
f(corge), g(corge);
else
gralt();
To nie działa, jeśli zamiast z f(X)
masz bardziej skomplikowany kod, który musi przejść we własnym bloku, na przykład do deklarowania zmiennych lokalnych. W najbardziej ogólnym przypadku rozwiązaniem jest użycie czegoś w rodzaju do ... while
, aby makro było pojedynczą instrukcją, która przyjmuje średnik bez nieporozumień.
#define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0)
Nie musisz używać do ... while
, Możesz również ugotować coś z if ... else
, chociaż gdy if ... else
rozszerza się wewnątrz if ... else
prowadzi do " zwisającego else ", co może sprawić, że istniejący problem zwisający jeszcze trudniej znaleźć, jak w poniższym kodzie.
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
Chodzi o użycie średnika w kontekstach, w których zwisający średnik jest błędny. Oczywiście w tym miejscu można (i prawdopodobnie powinno) argumentować, że lepiej zadeklarować BAR
jako rzeczywistą funkcję, a nie makro.
Podsumowując, do ... while
jest po to, aby obejść niedociągnięcia preprocesora C. Kiedy te podręczniki stylu C każą Ci zwolnić preprocesor C, właśnie o to się martwią.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-15 18:26:38
Makra są kopiowanymi / wklejanymi fragmentami tekstu, które pre-procesor umieści w oryginalnym kodzie; autor makra ma nadzieję, że zastąpienie stworzy poprawny kod.
Istnieją trzy dobre "wskazówki", aby odnieść sukces:
Pomóż makro zachowywać się jak prawdziwy kod
Zwykły kod jest zwykle zakończony dwukropkiem. Jeśli użytkownik przegląda kod, który go nie potrzebuje...
doSomething(1) ;
DO_SOMETHING_ELSE(2) // <== Hey? What's this?
doSomethingElseAgain(3) ;
Oznacza to, że użytkownik oczekuje, że kompilator wytworzy błąd, jeśli średnik jest nieobecny.
Ale naprawdę dobrym powodem jest to, że w pewnym momencie autor makra będzie musiał zastąpić makro prawdziwą funkcją(być może inlined). Więc makro powinno naprawdę zachowywać się jak jedno.
Więc powinniśmy mieć makro wymagające dwukropka.
Wygeneruj prawidłowy kod
Jak pokazano w odpowiedzi jfm3, czasami makro zawiera więcej niż jedną instrukcję. A jeśli makro zostanie użyte wewnątrz instrukcji if, będzie to problematyczne:
if(bIsOk)
MY_MACRO(42) ;
To makro można rozszerzyć jako:
#define MY_MACRO(x) f(x) ; g(x)
if(bIsOk)
f(42) ; g(42) ; // was MY_MACRO(42) ;
Funkcja g
zostanie wykonana niezależnie od wartości bIsOk
.
Oznacza to, że musimy dodać Zakres do makra:
#define MY_MACRO(x) { f(x) ; g(x) ; }
if(bIsOk)
{ f(42) ; g(42) ; } ; // was MY_MACRO(42) ;
Podaj prawidłowy kod 2
Jeśli makro jest takie jak:
#define MY_MACRO(x) int i = x + 1 ; f(i) ;
Możemy mieć inny problem w następującym kodzie:
void doSomething()
{
int i = 25 ;
MY_MACRO(32) ;
}
Ponieważ rozszerzyłoby się jako:
void doSomething()
{
int i = 25 ;
int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}
Ten kod nie będzie kompilowany, oczywiście. Więc, ponownie, rozwiązanie jest za pomocą zakresu:
#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }
void doSomething()
{
int i = 25 ;
{ int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}
Kod zachowuje się ponownie poprawnie.
Łączenie efektów półkropka + lunety?
Istnieje jeden idiom C / C++, który wywołuje ten efekt: pętla do/while:do
{
// code
}
while(false) ;
Do / while może utworzyć zakres, zamykając w ten sposób kod makra, a na końcu potrzebuje dwukropka, rozszerzając go do kodu potrzebującego.
Bonus?
Kompilator C++ zoptymalizuje pętlę do / while, ponieważ jego post-condition is false jest znany w czasie kompilacji. Oznacza to, że makro takie jak:
#define MY_MACRO(x) \
do \
{ \
const int i = x + 1 ; \
f(i) ; g(i) ; \
} \
while(false)
void doSomething(bool bIsOk)
{
int i = 25 ;
if(bIsOk)
MY_MACRO(42) ;
// Etc.
}
Rozwinie się poprawnie jako
void doSomething(bool bIsOk)
{
int i = 25 ;
if(bIsOk)
do
{
const int i = 42 + 1 ; // was MY_MACRO(42) ;
f(i) ; g(i) ;
}
while(false) ;
// Etc.
}
A następnie jest kompilowany i optymalizowany jako
void doSomething(bool bIsOk)
{
int i = 25 ;
if(bIsOk)
{
f(43) ; g(43) ;
}
// Etc.
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-13 08:31:21
@Jfm3 - masz miłą odpowiedź na pytanie. Możesz też dodać, że idiom makro zapobiega również prawdopodobnie bardziej niebezpiecznemu (ponieważ nie ma błędu) niezamierzonemu zachowaniu z prostymi instrukcjami "if":
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
Rozszerza się do:
if (test) f(baz); g(baz);
Który jest poprawny składniowo, więc nie ma błędu kompilatora, ale ma prawdopodobnie niezamierzoną konsekwencję, że g() będzie zawsze wywoływane.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-30 18:05:30
Powyższe odpowiedzi wyjaśniają znaczenie tych konstrukcji, ale istnieje znacząca różnica między nimi, która nie została wymieniona. W rzeczywistości istnieje powód, aby preferować do ... while
do if ... else
konstrukcji.
Problem konstrukcji if ... else
polega na tym, że nie zmusza ona do umieszczenia średnika. Jak w tym kodzie:
FOO(1)
printf("abc");
Chociaż pominęliśmy średnik (przez pomyłkę), kod rozszerzy się do
if (1) { f(X); g(X); } else
printf("abc");
I po cichu się skompiluje (chociaż niektóre Kompilatory mogą wystawić ostrzeżenie o nieosiągalnym kodzie). Ale printf
oświadczenie nigdy nie zostanie wykonane.
do ... while
construct nie ma takiego problemu, ponieważ jedynym poprawnym tokenem po while(0)
jest średnik.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-03 15:21:08
Chociaż oczekuje się, że Kompilatory optymalizują pętle do { ... } while(false);
, istnieje inne rozwiązanie, które nie wymagałoby tego konstruktu. Rozwiązaniem jest użycie operatora przecinka:
#define FOO(X) (f(X),g(X))
Lub jeszcze bardziej egzotycznie:
#define FOO(X) g((f(X),(X)))
Chociaż będzie to dobrze działać z oddzielnymi instrukcjami, nie będzie działać z przypadkami, w których zmienne są konstruowane i używane jako część #define
:
#define FOO(X) (int s=5,f((X)+s),g((X)+s))
Z tym trzeba by użyć konstruktu do/while.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-10 08:33:06
Biblioteka preprocesora Jensa Gustedta P99 (tak, fakt, że taka rzecz istnieje, rozwalił też mój umysł!) ulepsza konstrukcję {[1] } w sposób mały, ale znaczący, definiując:
#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP
Uzasadnieniem jest to, że w przeciwieństwie do konstrukcji do { ... } while(0)
, break
i continue
nadal działają wewnątrz danego bloku, ale ((void)0)
tworzy błąd składni, jeśli średnik zostanie pominięty po wywołaniu makra, co w przeciwnym razie pominie następny blok. (Właściwie nie ma problem "zwisania" tutaj, ponieważ else
wiąże się z najbliższym if
, czyli tym w makrze.)
Jeśli interesują Cię rzeczy, które można zrobić mniej lub bardziej bezpiecznie z preprocesorem C, sprawdź tę bibliotekę.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-19 16:19:45
Z pewnych powodów nie mogę skomentować pierwszej odpowiedzi...
Niektórzy z was pokazali makra ze zmiennymi lokalnymi, ale nikt nie wspomniał, że nie można używać żadnej nazwy w makrze! Kiedyś ugryzie użytkownika! Dlaczego? Ponieważ argumenty wejściowe są podstawione do szablonu makra. W przykładach makr użyłeś prawdopodobnie najczęściej używanej zmiennej i .
Na przykład, gdy następujące makro
#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)
Jest stosowany w następujących function
void some_func(void) {
int i;
for (i = 0; i < 10; ++i)
FOO(i);
}
Makro nie użyje zamierzonej zmiennej i, która jest zadeklarowana na początku some_func, ale zmiennej lokalnej, która jest zadeklarowana w do ... pętla while makra.
Dlatego nigdy nie używaj wspólnych nazw zmiennych w makrze!
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-21 18:42:56
Nie wydaje mi się, żeby to było wspomniane, więc rozważ to
while(i<100)
FOO(i++);
Zostanie przetłumaczone na
while(i<100)
do { f(i++); g(i++); } while (0)
Zauważ, jak i++
jest oceniane dwukrotnie przez makro. Może to prowadzić do ciekawych błędów.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-10-18 22:04:41
Uważam, że ta sztuczka jest bardzo pomocna w sytuacjach, w których musisz sekwencyjnie przetwarzać określoną wartość. Na każdym poziomie przetwarzania, jeśli wystąpi jakiś błąd lub nieprawidłowy warunek, można uniknąć dalszego przetwarzania i przerwać wcześnie. np.
#define CALL_AND_RETURN(x) if ( x() == false) break;
do {
CALL_AND_RETURN(process_first);
CALL_AND_RETURN(process_second);
CALL_AND_RETURN(process_third);
//(simply add other calls here)
} while (0);
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-18 04:34:54
Wyjaśnienie
do {} while (0)
i if (1) {} else
mają upewnić się, że makro jest rozszerzone do tylko 1 instrukcji. Inaczej:
if (something)
FOO(X);
Rozszerzy do:
if (something)
f(X); g(X);
I g(X)
będą wykonywane poza if
instrukcją kontrolną. Unika się tego podczas stosowania do {} while (0)
i if (1) {} else
.
Better alternative
Z wyrażeniem instrukcji GNU (nie jest częścią standardu C), masz lepszy sposób niż do {} while (0)
i if (1) {} else
, aby rozwiązać ten problem, po prostu używając ({})
:
#define FOO(X) ({f(X); g(X);})
I ta składnia jest zgodna z wartościami zwrotnymi (zauważ, że do {} while (0)
nie jest), jak w:
return FOO("X");
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-05 05:32:28