Встраивание шаблонов



Небольшое предисловие: один из админов и автор последующих 5 постов уходит в отпуск на первые майские и будет недоступен. Выходить они будут отложенными публикациями. Так как мы особо не влияем на тексты друг друга, то второй админ может быть спокойно застан врасплох возможными неточностями постов и вашими комментариями. Так что прошу иметь это ввиду. Спасибо



Вот здесь мы поговорили о том, что методы класса - это по факту те же самые обычные функции, только для них первым параметром передается this. И если подумать 1.34 секунды, то можно понять, что взаимодействие класса с внешним миром происходит только за счет методов. А поля класса - это просто кусок памяти, из которого в разных ситуациях компилятор может достать ту или иную информацию. Получается, что низкоуровневый "код класса" - это набор низкоуровневого кода его методов(то есть обычных функций) и не более.



Получается, что возможна ситуация, когда компилятор встроит вызовы одного, нескольких или всех методов класса.



Шаблонные классы - хоть и неполноценные классы, но их инстанциации - да. Поэтому их методы также могут инлайниться, никаких исключений.



Обычные функции тоже могут встраиваться.



А константные шаблонные переменные после инстанциации могут не иметь имени, компилятор просто сразу подставит во все места использования конкретное значение.



Итого, получается, что у нас все шаблонные сущности могут быть встроены компилятором. Конечно же для этого должны быть включены оптимизации(но и без них может получиться).



Получается, что если мы в какой-то единице трансляции указываем явное объявление инстанциации с помощью extern template, и рассчитываем на неявную инстанциацию в другой единице трансляции, то мы спокойно можем нарваться на undefined reference.



Происходит это примерно так:



// ship.hpp

#pragma once



template<typename T>

struct Ship

{

int i = 0;

void TurnShip(T command);

};



template <class T>

void Ship<T>::TurnShip(T command) {i++;}



// ship.cpp



#include "ship.hpp"

#include <string>

#include <iostream>



void foo() {

Ship<std::string> ship{};

ship.TurnShip(std::string{"Turn upside down"});

std::cout << ship.i << std::endl;

}



// main.cpp



#include "ship.hpp"

#include <string>



extern template class Ship<std::string>;



int main() {

Ship<std::string> ship;

ship.TurnShip(std::string{"Turn upside down"});

}





Знакомый пример, только пара модификаций. В хэдэре только объявление и определение шаблона. В ship.cpp пытаемся неявно инстанцировать строковую специализацию. Чтобы компилятор полностью не убирал код внутри foo за ненадобностью(тогда и ничего инстанцировать не нужно будет), сделаем так, чтобы она влияла на внешний мир. Добавим в шаблон поле, в методе его будем инкрементировать, и в foo выведем поле после модификации. В мэйне будем полагаться на инстанциацию в другой единице трансляции за счет extern template.



Вот если это попытаться скомпилировать(с оптимизациями) и собрать, то на линковке произойдет undefined reference. Компилятор увидел, что метод TurnShip слишком простой и его спокойно можно встроить и не генерировать для него определение. Что и происходит. А линкер в свою очередь из-за этого и не смог найти определение метода.



А божественным избавлением от этой проказы будет использование явной инстанциации. Она заставляет компилятор сгенерировать определение символа. Вызовы по прежнему могут инлайниться, но определение будет и мы сможем к нему обращаться.



Так что помните простое правило: на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации(1 на всю программу на каждое конкретное определение).



Rely on explicitly stated things. Stay cool.



#cppcore #template #compiler