Ответ
Компилятор C++ выполняет инстанцирование шаблона, генерируя отдельную, конкретную версию функции (или класса) для каждого уникального набора шаблонных параметров, с которым она используется. Эти сгенерированные функции являются обычным машинным кодом.
Процесс:
- Компилятор видит вызов шаблонной функции с конкретными типами.
- Он подставляет эти типы вместо параметра шаблона
T. - Генерирует новую функцию с «зашитым» типом и компилирует ее.
Пример:
// Шаблонная функция (исходный код)
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
auto r1 = add(5, 10); // Вызов с int
auto r2 = add(3.14, 2.71); // Вызов с double
auto r3 = add('A', 1); // Ошибка компиляции: типы 'char' и 'int' разные
// Явное указание типа решает проблему:
auto r4 = add<int>('A', 1); // char неявно приводится к int
}
После компиляции условно будут сгенерированы две независимые функции (манглинг имен упрощен):
// Сгенерированный код (пример)
int add_int(int a, int b) { // Инстанцирование для T = int
return a + b;
}
double add_double(double a, double b) { // Инстанцирование для T = double
return a + b;
}
Важные следствия:
- Раздувание бинарного кода (code bloat): Каждая инстанцированная версия увеличивает размер исполняемого файла.
- Компиляция только используемых инстанциаций: Если
addни разу не вызвана дляstd::string, эта версия сгенерирована не будет. - Определение в заголовочном файле: Поскольку инстанцирование происходит на этапе компиляции, тело шаблонной функции должно быть видно в каждой единице трансляции, где она используется. Поэтому шаблоны почти всегда определяются полностью в
.hppфайлах.
Ответ 18+ 🔞
А, шаблоны в C++! Ну это ж классика, ёпта. Сейчас объясню на пальцах, только не бей.
Представь, что шаблон — это как чертёж для станка, а не сам готовый болт. Компилятор — это такой токарь-алкаш, который сидит и ждёт: "Ну чё, какие болты-то нужны?". Только когда ты в коде пишешь add(5, 10), он такой: "А, бля, целочисленные болты!" — и начинает на своём станке по чертежу вытачивать конкретную функцию для int. Это и называется инстанцирование — превращение абстрактного шаблона в реальный, жирный машинный код.
Вот смотри, как это выглядит в деле:
// Это просто чертёж, понимаешь? Функции ещё нет.
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
auto r1 = add(5, 10); // Токарь видит: о, целые числа! Делаем версию для int!
auto r2 = add(3.14, 2.71); // Опа, дробные! Делаем ещё одну, отдельную версию для double!
auto r3 = add('A', 1); // А тут ёперный театр: 'char' и 'int' — разные типы! Станок встаёт. Ошибка компиляции.
// Но можно ткнуть токаря мордой в чертёж:
auto r4 = add<int>('A', 1); // Говорим явно: делай для int! И char послушно превратится в int.
}
И после всей этой движухи компилятор-токарь нагенерит под капотом обычные функции, как будто ты их сам написал. Условно, конечно:
// Вот что на самом деле оказывается в бинарнике
int add_int(int a, int b) { // Инстанцирование для T = int
return a + b;
}
double add_double(double a, double b) { // Инстанцирование для T = double
return a + b;
}
А теперь главные подводные камни, о которые все бьются:
- Раздувание кода (code bloat): Это пиздец, чувак. Вызвал шаблон для
int,double,long long— получил три независимые функции в бинарнике. Жрёт место, как не в себя. Но зато каждая — быстрая, оптимизированная под свой тип. - Генерируется только то, что используют: Логично же. Если ты ни разу не сложил
std::string, то версии для строк и не будет. Компилятор не дурак, зря время тратить. - Определение — только в заголовочном файле: Вот это самое важное, бля! Поскольку токарю-компилятору нужно видеть полный чертёж в момент, когда он вытачивает болт, весь код шаблона (и объявление, и тело!) должен быть в
.hppфайле. Иначе другой.cppфайл придёт и скажет: "а сделай-ка мнеaddдляfloat", а как — непонятно, чертежа-то нет. Поэтому шаблоны — это всегда головная боль с инклудами, их в.cppне спрячешь.
Короче, мощный инструмент, но если использовать бездумно — получишь бинарник размером с хороший порнофильм, который и компилироваться будет овердохуища.