madf: (Default)
[personal profile] madf
У пості про систему плагінів для C++ було піднято питання про template instantiation. Що це таке і з чим його їдять?

Як сказано у цьому документі - "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. Те саме і у файлі bar.o. Документація на nm каже що:
           "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 все-таки схавав всю підсвітку коду. Ідіотська блогоплатформа!

Profile

madf: (Default)
madf

April 2018

S M T W T F S
1234567
891011121314
15161718192021
22232425262728
2930     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 6th, 2026 06:28 am
Powered by Dreamwidth Studios