GCC, Template Instantiation
Jan. 8th, 2012 10:09 pmУ пості про систему плагінів для C++ було піднято питання про template instantiation. Що це таке і з чим його їдять?
Як сказано у цьому документі - "7.5 Where's the Template?" - історично склались дві системи template instantiation: від Borland і від Cfront. Перед розробниками компілятора була поставлена задача: не допустити дублікатів коду у вихідному файлі. Задача не така проста як здається на перший погляд. Якщо з inline-функціями (і методами шаблонних класів) все більш-менш зрозуміло, то питання про розміщення коду звичайних шаблонних функцій і методів у об’єктних файлах і результуючому бінарнику потребує детального аналізу.
Розглянемо простий проект, що складається із чотирьох файлів: main.cpp, foo.cpp, bar.cpp і print.h.
main.cpp:
Головний файл простий і питань не викликає.
foo.cpp:
У цьому файлі міститься реалізація функції foo. Вона складається із ініціалізації стекової змінної типу size_t значенням 15 і виклику невідомої функції print із змінною у якості аргументу. Функція print задекларована у файлі print.h.
bar.cpp:
Файл bar.cpp містить реалізацію функції bar яка примітна тим що функція print у нії викликається для аргументів різного типу. А це означає що функція поліморфна по своєму аргументу. Оскільки function overloading ми зараз не розглядаємо, очевидно що функція print шаблонна.
print.h:
Виникає питання: де розмістити код функції print? Оскільки inline-версія нам зараз не цікава, згадаємо два підхода про які я писав на початку. Так от, template instantiation від Borland вимагає наявності коду під час, власне, template instantiation - тобто у точках використання шаблонної функції. А значить реалізацію треба покласти у файл з декларацією - безпосередньо чи шляхом підключення директивою include. З цього варіанту і почнемо:
Скомпілюємо окремі модулі в об’єктні файли і скомпонуємо їх у цільну програму:
Стійте-стійте! Як же так? А як же One Definition Rule?! Чому компоновщик не каже нам що символ уже був визначений раніше?
Подивимось уважно на вивід nm. У файлі foo.o спеціалізація для print позначена як W. Те саме і у файлі bar.o. Документація на nm каже що:
Компоновщик допускає існування кількох символів такого типу і у готовий файл підставляє тільки один із них (зазвичай це визначається порядком слідування об’єктних файлів у командному рядку компоновщика). Таким чином, компілятор для модуля генерує код для кожного template instantiation, а компоновщик потім зайвий код відкидає. Звісно це не кращим чином позначається на часі компіляції проекту і розмірах окремих об’єктних файлів. Повноцінна підтримка Borland'івського підходу до template instantiation реалізується у gcc за допомогою ключа -frepo. Якщо його вказати то для кожного об’єктного файлу буде згенеровано файл з розширенням .rpo, що містить використовувані у ньому шаблонні функції і їх типи. Перед компоновкою запускається утиліта collect2 (наприклад, /usr/libexec/gcc/x86_64-pc-linux-gnu/4.5.3/collect2) яка вміє працювати з цими файлами. Використовуючи інформацію із .rpo-файлів вона виконає додатковий сеанс компіляції і додасть код шаблонних функцій у ті об’єктні файли де він потрібен. Дивіться самі:
Зверніть увагу - символи шаблонної функції більше не позначені як weak, вони тепер повноцінні символи на які компоновщик уже буде лаятись за наявності дублікатів. Але дублікатів більше немає, collect2 розрулив ситуацію. Цікаво прослідкувати за змінами що відбуваються у об’єкних та .rpo-файлах при проході collect2:
Після першого проходу компілятора об’єктний файл не містить символів спеціалізованих шаблонних функцій, зате інформація про них є у .rpo-файлі. А після проходу collect2 ситуація змінюється:
Цікавий той факт що порядок слідування об’єктних файлів у командному рядку компоновщика впливає на те у якому об’єктному файлі буде розміщено код:
Я розмістив bar.o першим і код обох функцій потрапив у нього, бо він використовує обидві. А у foo.o цей символ позначено як undefined. Таким чином -frepo лише зменшує час компіляції і розміри об’єктних файлів за рахунок двохпроходової компіляції.
Тепер що стосується підходу Cfront. Він не вимагає доступності коду шаблонної функції у точці template instantiation, але вимагає явного вказування які саме типи будуть використовуватись. Шаблонний код написаний у стилі Cfront схожий на extern templates: він розділений на заголовочний файл з declaration і cpp-файл з definition функцій. Модифікуємо наш код, розділивши print.h на два файли.
print.h:
print.cpp:
Зверніть увагу на останні рядки файлу - це називається explicit template instantiation. Саме вони вказують компілятору згенерувати код функції для вказаних типів. Результат, в принципі, очікуваний:
Компілятор згенерував код функцій лише у тому файлі де він мав до нього доступ. При чому завдяки explicit template instantiation код було згенеровано саме для тих типів які потрібні.
Є ще один тонкий момент. Згадайте першу версію коду - компілятор, маючи у точці використання шаблонної функції доступ до її коду генерував weak symbol для неї. Так от, можна залишити код шаблонної функції у заголовочному файлі і використовувати explicit template instantiation. Щоб компілятор при цьому не генерував зайвий код використовується ключ -fno-implicit-templates. Виглядає це так:
print.h:
print.cpp:
Як бачите, код шаблонної функції із файлу зник, тут залишились тільки директиви explicit template instantiation. Якщо просто зібрати прогрраму як і раніше, не вказуючи додаткових ключів, то у об’єктних файлах foo.o і bar.o буде міститись код функцій print<size_t> і print<std::string>. Точно такий же код міститиме файл print.o. Щоб уникнути зайвої компіляції використовуємо -fno-implicit-templates.
Як написано у "7.5 Where's the Template?", gcc на більшості платформ використовує підхід Borland до template instantiation. Він працює і без використання -frepo, але при цьому один і той-же код генерується кілька разів збільшуючи час компіляції. Це твердження вірне для звичайних, не-inline функцій. До речі, розрулювання inline-функцій відбувається саме завдяки weak symbols (адже компілятор сам визначає, чи inline-ити код функції чи ні).
Підсумовуючи всю цю писанину скажу, що в кінці кінців, які б не були складні конструкції на вході і які б вони не мали типи - все зводиться до імен символів.
PS: довбаний редактор LJ все-таки схавав всю підсвітку коду. Ідіотська блогоплатформа!
Як сказано у цьому документі - "7.5 Where's the Template?" - історично склались дві системи template instantiation: від Borland і від Cfront. Перед розробниками компілятора була поставлена задача: не допустити дублікатів коду у вихідному файлі. Задача не така проста як здається на перший погляд. Якщо з inline-функціями (і методами шаблонних класів) все більш-менш зрозуміло, то питання про розміщення коду звичайних шаблонних функцій і методів у об’єктних файлах і результуючому бінарнику потребує детального аналізу.
Розглянемо простий проект, що складається із чотирьох файлів: main.cpp, foo.cpp, bar.cpp і print.h.
main.cpp:
#include <cstdlib>
void foo();
void bar();
int main(int, char **)
{
foo();
bar();
return EXIT_SUCCESS;
}
|
| _Winnie C++ Colorizer |
Головний файл простий і питань не викликає.
foo.cpp:
#include <cstddef> // size_t
#include "print.h"
void foo()
{
size_t v = 15;
print(v);
}
|
| _Winnie C++ Colorizer |
У цьому файлі міститься реалізація функції foo. Вона складається із ініціалізації стекової змінної типу size_t значенням 15 і виклику невідомої функції print із змінною у якості аргументу. Функція print задекларована у файлі print.h.
bar.cpp:
#include <cstddef>
#include <string>
#include "print.h"
void bar()
{
std::string v1("abc");
size_t v2 = 13;
print(v1);
print(v2);
}
|
| _Winnie C++ Colorizer |
Файл bar.cpp містить реалізацію функції bar яка примітна тим що функція print у нії викликається для аргументів різного типу. А це означає що функція поліморфна по своєму аргументу. Оскільки function overloading ми зараз не розглядаємо, очевидно що функція print шаблонна.
print.h:
#ifndef __PRINT_H__ #define __PRINT_H__ #include <iostream> template <typename T> void print(const T & value); #endif |
| _Winnie C++ Colorizer |
Виникає питання: де розмістити код функції print? Оскільки inline-версія нам зараз не цікава, згадаємо два підхода про які я писав на початку. Так от, template instantiation від Borland вимагає наявності коду під час, власне, template instantiation - тобто у точках використання шаблонної функції. А значить реалізацію треба покласти у файл з декларацією - безпосередньо чи шляхом підключення директивою include. З цього варіанту і почнемо:
#ifndef __PRINT_H__
#define __PRINT_H__
#include <iostream>
template <typename T>
void print(const T & value);
template <typename T>
void print(const T & value)
{
std::cout << value << std::endl;
}
#endif
|
| _Winnie C++ Colorizer |
Скомпілюємо окремі модулі в об’єктні файли і скомпонуємо їх у цільну програму:
$ make
g++ -c main.cpp -o main.o
g++ -c foo.cpp -o foo.o
g++ -c bar.cpp -o bar.o
g++ main.o foo.o bar.o -o test
$ nm main.o | c++filt
U bar()
U foo()
0000000000000000 T main
$ nm foo.o | c++filt
000000000000005e t global constructors keyed to foo()
0000000000000000 T foo()
000000000000001e t __static_initialization_and_destruction_0(int, int)
0000000000000000 W void print<unsigned long>(unsigned long const&)
U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
U std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned long)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U __cxa_atexit
U __dso_handle
$ nm bar.o | c++filt
00000000000000dd t global constructors keyed to bar()
U _Unwind_Resume
0000000000000000 T bar()
000000000000009d t __static_initialization_and_destruction_0(int, int)
0000000000000000 W void print<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
0000000000000000 W void print<unsigned long>(unsigned long const&)
U std::allocator<char>::allocator()
...
U __gxx_personality_v0
w pthread_cancel
$ ./test
15
abc
13
Стійте-стійте! Як же так? А як же One Definition Rule?! Чому компоновщик не каже нам що символ уже був визначений раніше?
Подивимось уважно на вивід nm. У файлі foo.o спеціалізація для print
"W"
"w" The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. When a weak defined symbol is linked with a normal defined symbol, the normal defined symbol is used with no
error. When a weak undefined symbol is linked and the symbol is not defined, the value of the symbol is determined in a system-specific manner without error. On some systems, uppercase indicates
that a default value has been specified.
Компоновщик допускає існування кількох символів такого типу і у готовий файл підставляє тільки один із них (зазвичай це визначається порядком слідування об’єктних файлів у командному рядку компоновщика). Таким чином, компілятор для модуля генерує код для кожного template instantiation, а компоновщик потім зайвий код відкидає. Звісно це не кращим чином позначається на часі компіляції проекту і розмірах окремих об’єктних файлів. Повноцінна підтримка Borland'івського підходу до template instantiation реалізується у gcc за допомогою ключа -frepo. Якщо його вказати то для кожного об’єктного файлу буде згенеровано файл з розширенням .rpo, що містить використовувані у ньому шаблонні функції і їх типи. Перед компоновкою запускається утиліта collect2 (наприклад, /usr/libexec/gcc/x86_64-pc-linux-gnu/4.5.3/collect2) яка вміє працювати з цими файлами. Використовуючи інформацію із .rpo-файлів вона виконає додатковий сеанс компіляції і додасть код шаблонних функцій у ті об’єктні файли де він потрібен. Дивіться самі:
$ CXXFLAGS=-frepo make
g++ -frepo -c main.cpp -o main.o
g++ -frepo -c foo.cpp -o foo.o
g++ -frepo -c bar.cpp -o bar.o
g++ main.o foo.o bar.o -o test
collect: recompiling bar.cpp
collect: recompiling foo.cpp
collect: relinking
$ nm main.o | c++filt
U bar()
U foo()
0000000000000000 T main
$ nm foo.o | c++filt
000000000000008d t global constructors keyed to foo()
0000000000000000 T foo()
000000000000004d t __static_initialization_and_destruction_0(int, int)
000000000000001e T void print<unsigned long>(unsigned long const&)
U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
U std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned long)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U __cxa_atexit
U __dso_handle
$ nm bar.o | c++filt
0000000000000109 t global constructors keyed to bar()
U _Unwind_Resume
0000000000000000 T bar()
00000000000000c9 t __static_initialization_and_destruction_0(int, int)
000000000000009d T void print<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
U void print<unsigned long>(unsigned long const&)
U std::allocator<char>::allocator()
...
U __gxx_personality_v0
w pthread_cancel
$ ./test
15
abc
13
Зверніть увагу - символи шаблонної функції більше не позначені як weak, вони тепер повноцінні символи на які компоновщик уже буде лаятись за наявності дублікатів. Але дублікатів більше немає, collect2 розрулив ситуацію. Цікаво прослідкувати за змінами що відбуваються у об’єкних та .rpo-файлах при проході collect2:
$ CXXFLAGS=-frepo make foo.o
g++ -frepo -c foo.cpp -o foo.o
faust@hammer ~/temp/ti $ cat foo.rpo | c++filt
M foo.cpp
D /home/faust/temp/ti
A '-frepo' '-c' '-o' 'foo.o' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-frandom-seed=0xbe92b50f'
O typeinfo for std::collate<char>
O typeinfo for std::collate<wchar_t>
...
O typeinfo for std::basic_iostream<wchar_t, std::char_traits<wchar_t> >
O vtable for std::collate<char>
...
O construction vtable for std::basic_istream<wchar_t, std::char_traits<wchar_t> >-in-std::basic_iostream<wchar_t, std::char_traits<wchar_t> >
O VTT for std::basic_iostream<wchar_t, std::char_traits<wchar_t> >
O vtable for std::basic_iostream<wchar_t, std::char_traits<wchar_t> >
O void print<unsigned long>(unsigned long const&)
O __gnu_cxx::__numeric_traits_integer<long>::__min
...
$ nm foo.o | c++filt
000000000000005e t global constructors keyed to foo()
0000000000000000 T foo()
000000000000001e t __static_initialization_and_destruction_0(int, int)
U void print<unsigned long>(unsigned long const&)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 b std::__ioinit
U __cxa_atexit
U __dso_handle
Після першого проходу компілятора об’єктний файл не містить символів спеціалізованих шаблонних функцій, зате інформація про них є у .rpo-файлі. А після проходу collect2 ситуація змінюється:
$ CXXFLAGS=-frepo make
g++ -frepo -c main.cpp -o main.o
g++ -frepo -c bar.cpp -o bar.o
g++ main.o foo.o bar.o -o test
collect: recompiling bar.cpp
collect: recompiling foo.cpp
collect: relinking
$ diff foo.rpo.pre foo.rpo | c++filt
3c3
< A '-frepo' '-c' '-o' 'foo.o' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-frandom-seed=0xbe92b50f'
---
> A '-frepo' '-c' '-o' 'foo.o' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-frandom-seed=0xbe92b50f' '-shared-libgcc'
60c60,61
< O void print<unsigned long>(unsigned long const&)
---
> O std::ctype<char> const& std::__check_facet<std::ctype<char> >(std::ctype<char> const*)
> C void print<unsigned long>(unsigned long const&)
Цікавий той факт що порядок слідування об’єктних файлів у командному рядку компоновщика впливає на те у якому об’єктному файлі буде розміщено код:
$ CXXFLAGS=-frepo make obj
g++ -frepo -c main.cpp -o main.o
g++ -frepo -c foo.cpp -o foo.o
g++ -frepo -c bar.cpp -o bar.o
$ g++ main.o bar.o foo.o -o test
collect: recompiling bar.cpp
collect: relinking
$ nm foo.o | c++filt
000000000000005e t global constructors keyed to foo()
0000000000000000 T foo()
000000000000001e t __static_initialization_and_destruction_0(int, int)
U void print<unsigned long>(unsigned long const&)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 b std::__ioinit
U __cxa_atexit
U __dso_handle
$ nm bar.o | c++filt
0000000000000138 t global constructors keyed to bar()
U _Unwind_Resume
0000000000000000 T bar()
00000000000000f8 t __static_initialization_and_destruction_0(int, int)
000000000000009d T void print<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
00000000000000c9 T void print<unsigned long>(unsigned long const&)
U std::allocator<char>::allocator()
...
w pthread_cancel
$ ./test
15
abc
13
Я розмістив bar.o першим і код обох функцій потрапив у нього, бо він використовує обидві. А у foo.o цей символ позначено як undefined. Таким чином -frepo лише зменшує час компіляції і розміри об’єктних файлів за рахунок двохпроходової компіляції.
Тепер що стосується підходу Cfront. Він не вимагає доступності коду шаблонної функції у точці template instantiation, але вимагає явного вказування які саме типи будуть використовуватись. Шаблонний код написаний у стилі Cfront схожий на extern templates: він розділений на заголовочний файл з declaration і cpp-файл з definition функцій. Модифікуємо наш код, розділивши print.h на два файли.
print.h:
#ifndef __PRINT_H__ #define __PRINT_H__ template <typename T> void print(const T & value); #endif |
| _Winnie C++ Colorizer |
print.cpp:
#include <cstddef>
#include <iostream>
#include <string>
#include "print.h"
template <typename T>
void print(const T & value)
{
std::cout << value << std::endl;
}
template
void print<size_t>(const size_t & value);
template
void print<std::string>(const std::string & value);
|
| _Winnie C++ Colorizer |
Зверніть увагу на останні рядки файлу - це називається explicit template instantiation. Саме вони вказують компілятору згенерувати код функції для вказаних типів. Результат, в принципі, очікуваний:
$ make
g++ -c main.cpp -o main.o
g++ -c foo.cpp -o foo.o
g++ -c bar.cpp -o bar.o
g++ -c print.cpp -o print.o
g++ main.o foo.o bar.o print.o -o test
$ nm foo.o | c++filt
0000000000000000 T foo()
U void print<unsigned long>(unsigned long const&)
$ nm print.o | c++filt
0000000000000040 t global constructors keyed to print.cpp
0000000000000000 t __static_initialization_and_destruction_0(int, int)
0000000000000000 W void print<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
0000000000000000 W void print<unsigned long>(unsigned long const&)
U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
U std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned long)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
U __cxa_atexit
U __dso_handle
$ ./test
15
abc
13
Компілятор згенерував код функцій лише у тому файлі де він мав до нього доступ. При чому завдяки explicit template instantiation код було згенеровано саме для тих типів які потрібні.
Є ще один тонкий момент. Згадайте першу версію коду - компілятор, маючи у точці використання шаблонної функції доступ до її коду генерував weak symbol для неї. Так от, можна залишити код шаблонної функції у заголовочному файлі і використовувати explicit template instantiation. Щоб компілятор при цьому не генерував зайвий код використовується ключ -fno-implicit-templates. Виглядає це так:
print.h:
#ifndef __PRINT_H__
#define __PRINT_H__
#include <iostream>
template <typename T>
void print(const T & value);
template <typename T>
void print(const T & value)
{
std::cout << value << std::endl;
}
#endif
|
| _Winnie C++ Colorizer |
print.cpp:
#include <cstddef> #include <string> #include "print.h" template void print<size_t>(const size_t & value); template void print<std::string>(const std::string & value); |
| _Winnie C++ Colorizer |
Як бачите, код шаблонної функції із файлу зник, тут залишились тільки директиви explicit template instantiation. Якщо просто зібрати прогрраму як і раніше, не вказуючи додаткових ключів, то у об’єктних файлах foo.o і bar.o буде міститись код функцій print<size_t> і print<std::string>. Точно такий же код міститиме файл print.o. Щоб уникнути зайвої компіляції використовуємо -fno-implicit-templates.
$ CXXFLAGS=-fno-implicit-templates make
g++ -fno-implicit-templates -c main.cpp -o main.o
g++ -fno-implicit-templates -c foo.cpp -o foo.o
g++ -fno-implicit-templates -c bar.cpp -o bar.o
g++ -fno-implicit-templates -c print.cpp -o print.o
g++ main.o foo.o bar.o print.o -o test
$ nm foo.o | c++filt
000000000000005e t global constructors keyed to foo()
0000000000000000 T foo()
000000000000001e t __static_initialization_and_destruction_0(int, int)
U void print<unsigned long>(unsigned long const&)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
0000000000000000 b std::__ioinit
U __cxa_atexit
U __dso_handle
$ nm print.o | c++filt
0000000000000040 t global constructors keyed to print.cpp
0000000000000000 t __static_initialization_and_destruction_0(int, int)
0000000000000000 W void print<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
0000000000000000 W void print<unsigned long>(unsigned long const&)
U std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
U std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned long)
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
U __cxa_atexit
U __dso_handle
$ ./test
15
abc
13
Як написано у "7.5 Where's the Template?", gcc на більшості платформ використовує підхід Borland до template instantiation. Він працює і без використання -frepo, але при цьому один і той-же код генерується кілька разів збільшуючи час компіляції. Це твердження вірне для звичайних, не-inline функцій. До речі, розрулювання inline-функцій відбувається саме завдяки weak symbols (адже компілятор сам визначає, чи inline-ити код функції чи ні).
Підсумовуючи всю цю писанину скажу, що в кінці кінців, які б не були складні конструкції на вході і які б вони не мали типи - все зводиться до імен символів.
PS: довбаний редактор LJ все-таки схавав всю підсвітку коду. Ідіотська блогоплатформа!