2010-10-17:

Rozwinięcie makra w C/C++

easy:g++:c++:c
Ostatnio pracuje nad pewnym kodem w C++ w którym (nad)używam sporo różnych ficzerów języka. A jeśli chce się, żeby tego typu kod na pewno działał, trzeba być pewnym tego jak zachowa się kompilator, co sprowadza się do poszukania czy dane zachowanie jest wynikiem zgodności ze standardem (tj. czy standard mówi, że dana rzecz dokładnie tak ma się zachować), zdefiniowanym zachowaniem kompilatora (tj. w dokumentacji kompilatora (w moim przypadku jest to GCC) jest zapisane, że dana rzecz dokładnie tak się zachowa, ale niekoniecznie jest to zdefiniowane w standardzie języka), czy też jest totalnym UB (czyli w żadnej oficjalnej dokumentacji nie jest dane zachowanie udokumentowane, czyli jedyne czego możemy być pewni, to to że dana wersja danego kompilatora w takim konkretnym miejscu się prawdopodobnie zachowa się tak jak zaobserwowaliśmy). Tak więc niniejszy post jest w zasadzie zapisem informacji które znalazłem o pewnym ficzerze (rozwijaniu makr przez preprocesor) i prawdopodobnie doświadczeni programiści mogą niniejszy post pominąć.

OK, zacznę od pytania, które na pewno (przynajmniej po polskiej stronie lustra) by się pojawiło:
Q: "Dlaczego cały czas korzystasz z preprocesora skoro w C++ są template'y?"
A: Ponieważ lubie preprocesory. No i wyjaśnijmy jedną rzecz: szablony nie zastępują w całości funkcjonalności preprocesora, ponieważ z założenia miały dostarczyć odrobinę inną funkcjonalność.

Wracamy do głównego tematu. Pytanie (które za chwilę postawie) jest związane z tym oto kawałkiem kodu:
#include <cstdio>

int
main(void)
{
#define C(a) printf("C1: %s\n", a);
#define X(a) C(a)


X("before redefining C");

#undef C
#define C(a) printf("C2: %s\n", a);


X("after redefining C");

return 0;
}

Pytanie brzmi: czy druga linia wypisana na stdout rozpocznie się od C1 czy od C2?
Zauważmy, że są dwa możliwe zachowania preprocesora w tym przypadku:

Pierwsze możliwe zachowanie: Lista zastąpień w makrze (eng. "replacement list", to ta część w definicji która następuje po nazwie makra ;>) jest rozwiązywana w momencie definicji, czyli w przypadku #define X(a) C(a) token C(a) zostanie od razu rozwiązany do printf("C1: %s\n", a), a następnie para [X(a) => printf("C1: %s\n", a)] zostanie umieszczona w tablicy tłumaczeń preprocesora (czy jak tam się ta lista makr wewnętrznie nazywa). Należy zauważyć, że to zachowanie jest szybkie, ponieważ przy rozwinięciu makra w miejscu jego użycia nastepuje tylko jedno rozwinięcie i nie dochodzi do rekursji.

Drugie możliwe zachowanie: Lista zastąpień jest zapisywana w tablicy tłumaczeń preprocesora w dokładnie takiej postaci w jakiej się pojawia w definicji, czyli [X(a) => C(a)]. Tak więc (?rekursywne/liniowe?) rozwinięcie makra odbędzie się w momencie napotkania makra w kodzie.  Co prawda, będzie to wolniejsze, szczególnie w przypadkach kilku-poziomowego zagłębienia w makrze, ale dostarcza pewien poziom elastyczności, ponieważ można podmienić głębiej użyte makro na coś innego, przy zachowaniu funkcjonalności makra z wyższego poziomu.

Osobiście skłaniałem się ku drugiemu zachowaniu, ze względu na wspomnianą elastyczność. Furio sądził tak samo, a przecież logicznie rzecz biorąc obaj nie możemy się mylić, nie? ;p

Sprawdźmy co tam kompilator (GCC) faktycznie wypisze:
$ g++-4.5.1 test.cpp
$ ./a.out
C1: before redefining C
C2: after redefining C

Taak, czyli GCC ma zaimplementowane drugie zachowanie.
Pytanie więc brzmi, czy mamy do czynienia z czystym przypadkiem (UB), czy może zdefiniowanym w dokumentacji kompilatora / standardzie języka zachowaniem?

Zacznijmy od dokumentacji preprocesora GCC (3.1 Object-like Macros):

When the preprocessor expands a macro name, the macro's expansion replaces the macro invocation, then the expansion is examined for more macros to expand.
A także...
If the expansion of a macro contains its own name, either directly or via intermediate macros, it is not expanded again when the expansion is examined for more macros. This prevents infinite recursion.

(oczywiście, cytaty dotyczą makr w stylu obiektów, a w moim przykładowym kodzie są makra w stylu funkcji, niemniej jednak sądzę że możemy (możemy?) założyć, że powyższe zasady dotyczą obu rodzajów makr)

Więc... to nie jest UB! Jest to, przynajmniej, zachowanie zdefiniowane w dokumentacji kompilatora.

Idąc za ciosem zajrzyjmy do jakiegoś w miarę nowego szkicu standardu języka C++:
16.3.4 Rescanning and further replacement [cpp.rescan]
1. After all parameters in the replacement list have been substituted and # and ## processing has taken place, all placemarker preprocessing tokens are removed. Then the resulting preprocessing token sequence is rescanned, along with all subsequent preprocessing tokens of the source file, for more macro names to replace.
2. If the name of the macro being replaced is found during this scan of the replacement list (not including the rest of the source file’s preprocessing tokens), it is not replaced. Furthermore, if any nested replacements encounter the name of the macro being replaced, it is not replaced. These nonreplaced macro name preprocessing tokens are no longer available for further replacement even if they are later (re)examined in contexts in which that macro name preprocessing token would otherwise have been replaced.

Oraz (16.3.1 Argument substitution)...
A parameter in the replacement list, unless preceded by a # or ## preprocessing token or followed by a ## preprocessing token (see below), is replaced by the corresponding argument after all macros contained therein have been expanded. Before being substituted, each argument’s preprocessing tokens are completely macro replaced as if they formed the rest of the preprocessing file;

Wygląda na to, że drugie zachowanie jest zachowaniem opisanym w cytowanym standardzie. Q.E.D.

Ciekawa natomiast jest część dotycząca # i ##, mianowicie wykonanie # i ## ma miejsce przed reskanowaniem (w poszukiwaniu kolejnych makr) ale już po podstawieniu argumentów (co jest oczywiste), tak więc korzystając np. z ## możemy tworzyć nazwy makr podczas rozwijania makra, np:
#define A _my_name_is_A_
#define B _my_name_is_B_
#define AB _my_name_is_AB_
#define C(a,b) a##b
#define D(a,b) C(a,b)
C(A,B)
D(A,B)

W tym przypadku C(A,B) oraz D(A,B) zostaną rozwinięte do różnych stringów, ponieważ C(A,B) będzie rozwijane w niniejszy sposób:

C(A,B)
=> A##B (podstawienie argumentów)
=> AB (wykonanie ##)
=> _my_name_is_AB_ (reskan i podmiana makra)
=> koniec

Natomiast, D(A,B) będzie rozwinięte w trochę inny sposób:

D(A,B)
=> C(A,B) (podstawienie argumentów)
=> C(_my_name_is_A_, _my_name_is_B_) (reskan argumentów w makrze funkcyjnym)
=> _my_name_is_A_##_my_name_is_B_ (reskan makr)
=> _my_name_is_A__my_name_is_B_ (wykonanie ##)
=> koniec

Więc, w pierwszym przypadku reskan musiał "poczekać" aż ## nie zostanie wykonane. W drugim przypadku nie było (w pierwszym zagłębieniu) żadnego ##, więc reskan miał miejsce od razu.

Sprawdźmy czy g++ -E naprawdę pokazuje to co przed chwilą opisałem:
$ g++-4.5.1 test2.cpp -E
# 1 "test2.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "test2.cpp"
_my_name_is_AB_
_my_name_is_A__my_name_is_B_


I w sumie tyle. Zachęcam do przejrzenia podlinkowanego wyżej standardu C++ (jeśli jeszcze tego nie robiliście, a piszecie w C++), można znaleźć sporo ciekawostek a także kilka ciekawych UB, również w preprocesorze. Może warto by posprawdzać jak różne kompilatory reagują na takie UB? Ale to już sprawa na osobny post...

Comments:

2010-10-17 19:11:18 = Gynvael Coldwind
{
Jeszcze jedna sprawa:
Kilka osób pytało się mnie tu i tam czy to, że kilka postów pojawiło się po stronie angielskiej wcześniej niż po stronie polskiej oznacza, że rezygnuje z postowania po Polsku. Spieszę więc uspokoić, że nie rezygnuje z pisania po Polsku, a wybór języka w którym post pojawi się pierwszy jest w sporej części przypadków losowy :)
}
2010-10-17 19:14:56 = Nism0
{
Imo to dobry pomysł, bo wzrośnie wartość intelektualna posta dla tych którzy angielskiego nie ogarniają tak do końca :>
}
2010-10-17 19:37:25 = arix
{
Templaty nie służą do zastępowania makr
do zastępowania makr pp służą funkcje inline
nie różnią się w działaniu niczym poza kontrolą typów (przynajmniej tak brzmi wersja oficjalna)
Osobiście preferuje stosowanie PP do definiowania "stałych"
nie ma nic złego w stosowaniu go do definiowania makr funkcji
po za tym że nie jest to zgodne z głównymi założeniami cepa
i może powodować wiele trudnych do łatwego zdiagnozowania
problemów
to generalnie rzecz gustu męczą mnie te święte wojny
o nic jakie często się właśnie po "polskiej stronie" zdarzają
}
2010-10-18 00:20:54 = Amarok
{
Unikajmy słowa "funkcjonalność"! (Dotyczy tylko pisania po polsku! ;) )
}

Add a comment:

Nick:
URL (optional):
Math captcha: 5 ∗ 5 + 4 =