Грокаем C++
5.98K subscribers
23 photos
3 files
312 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Extern template

Дисклеймер: здесь и далее в этой серии статей я буду называть "специализацией" шаблона его инстанциацию с конкретным шаблонным параметром.

Помните, как мы явно инстанцировали шаблон в файле реализации? Раз мы может сказать компилятору, чтобы он инстанцировал нужную специализацию в нужном нам файле, то очень удобно потом обращаться за кодом этой специализации конкретно в эту единицу трансляции. Потому что, как мы уже знаем, код одной и той же специализации может генерироваться во всех единицах трансляции, не вызывая при этом конфликтов и нарушения ODR(вектор интов инстанцируется много где, но никогда эти специализации не конфликтуют при линковке). Тут аналогия с inline сущностями.

В общем, хочется иметь один образцово-показательный код в одной TU и обращаться за нужными символами туда.

И такой инструмент есть!

Называется extern template declaration. Этот пост, а также несколько предыдущих и следующих, посвящается нашему олду @PyXiion и его просьбе разобрать этот вопрос.

Идея очень похожа на extern объявления других сущностей. С его помощью обеспечивается внешнее связывание и оно помогает компилятору понять, что такая сущность в программе есть, просто она описана в другой единице трансляции. Это значит, что на этапе линковки компилятор может посмотреть в эту самую другую TU и найти там все, что нужно для генерации полноценного кода.

Синтаксис следующий:

extern template class-key template-name <argument-list>;

Все то же самое, что и при явной инстанциации, только спереди добавляем extern. Фича из С++11 кстати и скорее всего средний разработчик даже не представляет о ее существовании.

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

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

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

Learn new things. Stay cool.

#cppcore #cpp11 #template #compiler
Когда НЕ стоит использовать extern template

Если вы будете гуглить инфу по этой теме, то непременно нарветесь на неправильное понимание принципов работы фичи. Стопроцентов вы наткнетесь на такое объяснение:

extern template в связке с явным инстанцированием шаблона помогает предотвратить дублирование кода в TU и уменьшить время компиляции. Выглядит это так:

// ship.hpp
#pragma once
template<typename T>
struct Ship
{
void TurnShip(T command);
};

// ship.cpp
#include "ship.hpp"

template <class T>
void Ship<T>::TurnShip(T command) {}

template struct Ship<int>; // explicit instantiation definition

// main.cpp
#include "ship.hpp"

extern template struct Ship<int>; // explicit instantiation declaration

int main() {
Ship<int> ship;
ship.TurnShip(5);
}

Типа вот мы в ship.cpp добавили явную конкретизацию шаблона, а в мэйне объявили, что возьмем информацию о специализации в другом месте.

Но дело в том, что в этом случае extern template - лишний! В нем нет никакого смысла и вот почему.

Если мы уберем extern template из файла main, то ничего не изменится. Так как в этой единице трансляции и так никогда бы не была конкретизирована специализация Ship<int>. Потому что компилятору на момент компиляции файла main.cpp видно только объявление шаблона из файла ship.hpp и у него недостаточно информации для инстанцирования. И только при линковке линковщик найдет все символы в единице трансляции, соответствующей ship.cpp, и сгенерирует рабочую программу.

Так что запомните: если вы используете явную инстанциацию после определения шаблона в цппшнике и подключаете хэдэр с его объявлением, то вам НЕ НУЖНО использовать extern template.

Это кстати отличная защита от тех самых проблем при работе с шаблонами. Так что выносить определенения шаблонов в цппшники и делать в них явную инстанциацию - полезная вещь.

Также без подключения хэдэра эта вещь вообще не работает, в отличии например от глобальных переменных. Все-таки контекст extern здесь не совсем совпадает.

А вот когда это нужно использовать. Только тогда, когда у вас есть несколько единиц трансляции, где компилятор сам неявно может инстанцировать одинаковые специализации. Например, когда вы полностью определяете шаблон в хэдэре и везде его распространяете таким образом. Тогда получается, что без использования extern template в каждой из этих единиц трансляций, подключивших хэдэр с шаблоном и использующих одинаковую специализацию, эта специализация будет инстанцирована. Это значит, что код для нее будет присутствовать во всех объектниках. Это приводит к его дублированию и увеличению времени компиляции.

Теперь мы во всех TU, кроме одной, используем extern template и в этой оставшейся делаем явную специализацию. Получается, что для всех, кроме одной, TU компилятору будет запрещено самостоятельно инстанцировать эту специализацию. И все они будет обращаться в тот единственный объектник, в котором есть код для специализации. Именно за счет этого и не происходит раздувания итогового бинарника. Все просто полагаются на одну копию.

Rely on original information. Stay cool.

#cpp11 #cppcore #template #compiler
Линковочная природа шаблонов

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

Думаю, что не будет грубым обобщением сказать, что каждый из нас пользовался шаблонным классом std::vector. И это хорошо, все имеют опыт с ним и всем будет проще понимать, о чем сейчас пойдет речь.

Представим, что мы разрабатываем какое-то приложение или отдельный сервис. Этот сервис состоит из отдельных кодовых модулей, которые отдельно компилируются и линкуются вместе для получения готового бинарника. Так вот очень легко допустить, что во многих модулях одного и того же сервиса используется вектор интов. std::vector<int>. Обычно мы просто инклюдим в эти модули хэдэр вектора и после используем его. Таким образом происходит неявная инстанциация. То есть компилятор на этапе компиляции модулей сам инстанциацирует интовую специализацию вектора в каждой единице трансляции и использует ее в коде текущего юнита.

Однако, погодите-ка. То есть у нас в нескольких единицах трансляции есть одна и та же скомпилированная сущность и при линковке это не вызывает никаких проблем. Как так?

Вот, что стандарт говорит по поводу этого:

There can be more than one definition in a program of each of the following: 
class type, enumeration type, inline function, inline variable(since C++17), 
templated entity(template or member of template, but not full template specialization),
as long as all of the following is true[...]


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

И кстати, утверждение, что шаблоны неявно помечены inline - неверно. Но об этом позже.

За счет чего допускается возможность наличия нескольких определений сущности в программе? Если вы читали гайд по inline(можете найти в закрепе), то, наверняка, знаете ответ. За счет слабых символов.

Скомпилируем гццшкой самую простенькую функцию:

void foo() {
std::vector<int> vec(10);
vec[0] = 1;
}


А утилитка nm покажет нам природу символов в бинаре. Там будет генерироваться оч много функций и символов связанных с вектором, поэтому разберем только один пример с конструктором, символ которого выглядит так:

0000000000000000 W std::vector<int, std::allocator<int> >::vector(unsigned long, std::allocator<int> const&)

Вот эта буковка W говорит, что этот символ - слабый. А слабые символы могут быть перезаписаны во время линковки. Линковщик просто сам выберет одно понравившееся ему определение из всех существующих в программе и перезапишет им остальные. Таким образом в программе останется всего одно определение шаблона и все будут ссылаться на него. Однако все равно до линковки во всех единицах трансляции будет своя копия интовой инстанциации шаблона.

Use your weaknesses to solve your problems. Stay cool.

#compiler #cppcore #template
Шаблоны не подразумевают inline

Дисклеймер: в этом посте слово "специализация" будет значить конкретную программную сущность, объявленную через template<> с пустыми треугольными скобками, которая переопределяет поведения шаблона для конкретного типа.

В прошлом посте кратко коснулись этого. Сегодня разберемся в этом подробнее.

Мы уже знаем, что в программе может быть больше одного определения шаблона и это нормально. Ровно также может быть больше одного определения inline сущности. Так есть ли между этими утверждениями связь?

Очевидно, классы не могут быть inline. Разговор здесь пойдет только про inline функции и переменные(с С++14).

Во-первых, стандарт ничего не говорит по поводу того, что шаблоны по умолчанию inline. Хотя, например, для constexpr функций и статических полей класса это явно описано.

Во-вторых, в нем есть пара слов про явные специализации

Whether an explicit specialization of a function or variable template is inline, 
constexpr, constinit, or consteval is determined by the explicit specialization and
is independent of those properties of the template. Similarly, attributes appearing
in the declaration of a template have no effect on an explicit specialization of that
template...


Эта строчка говорит нам о том, что спецификаторы, которыми помечены явные специализации, могут не совпадать со спецификаторами самих шаблонов. Значит, что шаблоны имеет смысл помечать inline и мы даже может можем изменить это поведение в явной специализации. А значит, шаблоны не подразумевают inline. Их поведение только лишь схоже с inline сущностими в плане обхода ODR. Пример из стандарта:

template<class T> void f(T) { /* ... */ } 
template<class T> inline T g(T) { /* ... */ }

template<> inline void f<>(int) { /* ... */ } // OK, inline
template<> int g<>(int) { /* ... */ } // OK, not inline


Здесь нужно быть аккуратным, потому что на явные специализации распространяется ODR. Явные специализации - уже не шаблоны, поэтому, если вы хотите поместить их в хэдэр, то нужно помечать их inline, чтобы линковщик не ругался.

Если инлайн в нынешнее время в основном используется для обхода ODR, то есть ли смысл помечать шаблонные функции этим ключевым словом?

Особого смысла нет(помимо явных специализаций). Темплейты и так не подвержены ODR. А в остальном инлайн только лишь указывает компилятору, чтобы он сделал проверку на возможность inline expansion. Но он в принципе и так это делает для всех функций.

Differentiate things apart. Stay cool.

#template #cppcore #cpp14 #compiler
Когда стоит использовать explicit template declaration

Мы поговорили о случае, в котором бесполезно использовать explicit template declaration. Теперь поговорим о наиболее уместном и логичном способе использования этой фичи.

Главная функция extern template - запретить компилятору неявную инстанциацию. Значит, для адекватного использования этой конструкции компилятору необходимо иметь возможность выполнить эту неявную инстанциацию. Единственным подходящим ситуации вариантом здесь будет нахождение полного определения шаблона в хэдэре, чтобы его могли видеть все заинтересованные лица(пофантизируйте в комментариях, как могло бы выглядеть лицо у единицы трансляции).

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

Ну и последний момент. Если есть явное объявление инстанциации, должно быть и ее явное определение. Причем это ВАЖНО. Нельзя при использовании extern template полагаться на неявную инстанциацию. В нашем случае это уже невозможно, потому что мы добавили в хэдэр с шаблоном запрет на неявную инстанциацию, но я все равно хочу на это обратить ваше внимание. Компилятор может ее оптимизировать, так что для нее больше не останется отдельно скомпилированной сущности и все вызовы просто встроятся. Тогда компановщик не сможет разрезолвить символы и будет undefined reference. Чуть позже расскажу об этом в отдельном посте.
Итак, explicit template instantiation. Мы помещаем явные определения всех нужных нам неявных специализаций в отдельный цппшник. И вот к коду в этой TU будет обращаться линкер, чтобы подставить адреса нужных вызовов. А в других TU не будет сгенерировано ничего связанного с шаблоном.

Продемонстрирую на примере:

// ship.hpp

#pragma once
#include <string>
template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command);
};

template <class T>
void Ship<T>::TurnShip(T command) {/* do stuff using command */}

extern template class Ship<std::string>; // text command
extern template class Ship<int>; // turn certain number of degrees clockwise

// ship.cpp

#include "ship.hpp"

template class Ship<std::string>;
template class Ship<int>;

// main.cpp

#include "ship.hpp"
#include <string>

int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
Ship<int> ship1; // i know it's silly to instantiate 2 version of
// ship just to have a different style of turning,
// but stick to the goodold example
ship1.TurnShip(36'000); // just trying to make a giant whirlpool
}


Если мы отдельно скомпилируем main.cpp и посмотрим на символы объектника, то там будет только то, что связано с std::basic_string, но не с Ship. Как и было задумано.

Подводя итог: нам нужен хэдэр с полным определением шаблона и явными объявлениями extern template и сорец с явными определениями этих инстанциаций. Теперь мы можем везде тыкать наш хэдэр и ожидать уменьшения времени компиляции и меньшего размера объектников.

Choose the right way. Stay cool.

#template #compiler #cppcore
Встраивание шаблонов

Небольшое предисловие: один из админов и автор последующих 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
Не всегда инстанциация шаблона нужна для работы программы

Возьмем пример из прошлого поста, объединим в хэдэре объявление шаблона с его определением и выкинем ship.cpp. И попробуем скомпилировать только main.cpp.

// ship.hpp
#pragma once

template<typename T>
struct Ship
{
int i = 0;
void TurnShip(T command) {i++;}
};

// main.cpp

#include "ship.hpp"
#include <string>
#include <iostream>

extern template class Ship<std::string>;

int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
std::cout << ship.i << std::endl;
}


И неожиданно, все компилируется и выводится единичка. Почему так? Мы ведь почти ничего не поменяли даже просто нагло и беспардонно выкинули так нам необходимую единицу трансляции с явным инстанированием. Как это работает?

Дело в том, что любой метод, определенный внутри описания класса, неявно помечается inline. А на инлайн сущности не работает эффект подавления неявной специализации. Стандарт вот что говорит об этом:

Except for inline functions and class template specializations, 
explicit instantiation declarations have the effect of suppressing
the implicit instantiation of the entity to which they refer.


Кажется, тут можно такую цепочку мыслей провести: компилятору запрещается делать неявную инстанциацию строкового корабля. Но он ее может и не делать, а просто встроить вызов метода этой инстанциации внутрь функции main и дело в шляпе! И ничего не нарушили и все работает.

Естественно, на это полагаться нельзя, потому что не любой метод может быть встроен, а значит компилятору придется проводить неявную инстанциацию. А мы как раз и добивались, чтобы этого не было. И правило "на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации" по-прежнему работает.

Просто интересно было показать, как такое небольшое изменение может развернуть ситуацию на 180. И кстати, если все-таки держать отдельно описание класса и его определение, но пометить метод inline, то будет тот же эффект, который я описал выше.

Pay attention to small details. Stay cool.

#template #compiler #cppcore
Преимущества использования extern template

C++ известен долгой компиляцией программ. И одной из причин такого поведения является наличие шаблонов в языке. За счет того, что стандарт допускает больше одной конкретной инстанциации шаблона в программе, люди сильно расслабляются. Мало кто знает, как правильно организовывать и использовать шаблонный код. В большинстве случаев он просто находится в хэдэре и подключается во всевозможные места. Соответсвенно, в каждой TU, куда подключается хэдэр, будет своя копия инстанциации. Но это же не просто сама копия, на компиляцию всех копий тратится время.

А если используются какие-нибудь кодогенераторы, типа grpc-шного или soap-ного, то там реально может быть очень много единиц трансляции. И в каждой будет своя копия какого-нибудь вектора или опшинала.

И хотя для STL-ных сущностей extern template ничем не поможет(об этом в другом посте), для самописных шаблонов, расположенных в хэдэрах - подойдет. Адекватную организацию кода при использовании extern template, мы осветили тут https://t.me/grokaemcpp/226.

Но какие конкретно преимущества дает такой способ организации шаблонного кода?

1) Уменьшение размеров объектных файлов скомпилированных единиц трансляции. При сборке больших проектов у вас скорее всего сгенерируются сотни, если не тысячи объектных файлов или либок, которые за счет загромождения инстанциациями могут весить десятки и сотни мегабайт. Не каждый себе может позволить на машинке такой билд, который помимо полезных исполняемых файлов будет содержать огромное количество объектников с дублирующимся миллион раз кодом. extern template запрещает неявную инстанциацию шаблонов, а значит среди всех объектников будет только один, который и будет содержать нужную инстанциацию.

2) Уменьшение времени компиляции. Компилятору запретили генерировать код, а значит много лишних действий убирается и сокращается время компиляции.

3) Сокращение времени линковки. Вот это не прям очевидный пункт. Чем объемнее объектные файлы - тем больше линкеру работы. А учитывая, что для дедуплицирования инстанциаций нужно найти эти одинаковые дубли, сопоставить их, выбрать один и убрать все остальные, то задача уже не кажется такой простой.

4) Любой код, который подключит ваш заголовочник, сможет инстанцировать или найти любой explicit template instantiation, который захочет. Через extern template мы запретим компилятору самостоятельно генерировать эти инстанциации. Но если для какого-то шаблонного параметра не будет прописан extern template, то компилятор сможет сам неявно конкретизировать шаблон с этим параметром. Поэтому в этом плане, это очень гибкий инструмент.

Из-за последнего пункта такой способ организации кода подойдет для частоиспользуемых шаблонов, которые инстанцируются с большим многообразием параметров. Просто в цппшнике можно предоставить самые популярные варианты инстанциаций, которые будут давать большой импакт в увеличении времени компиляции и наиболее часто будут дублироваться. Остальное компилятор сможет сам неявно инстанцировать.
Если внешняя библиотека завязана на ваш шаблон, то это единственный способ адекватно предоставить ей доступ к коду.

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

Find benefits of using different things in various situations. Stay cool.

#cppcore #template #compiler
Другой способ организации шаблонного кода

В чем недостаток способа из предыдущего поста? В том, что при любом изменении шаблона, придется перекомпилировать все единицы трансляции, которые его включают. Весь код находится в хэдэре, значит TU будет в себе содержать полное определение сущности. Значит, любое незначительное изменение реализации приводит к перекомпиляции.

Не зря люди придумали разделение кода на объявление сущности в хэдэре и ее реализацию в цппшнике. Внешний интерфейс класса/сигнатура функции меняются не так часто. А вот изменение деталей реализации при разработке - дело само собой разумеющееся и это происходит на каждой итерации билда при отладке кода.

При использовании подхода с разделением на хэдэр и сорец, при изменении реализации мы перекомпилируем только сорец. И все остальные TU не будут нуждаться в перекомпилировании. А при линковке они просто будут обращаться за нужными символами в обновленную TU исходников сущности.

Таким образом, мы хоть и не увеличиваем изначальную скорость компиляции, но увеличиваем ее при перекомпиляции исходников во время отладки. Я считаю, что это даже больший плюс. Потому что при отладке кода, мы его мильён раз изменяем и на каждую попытку ждать несколько минут - это зашквар. Так программисты и спиваются. Надо же что-то делать пока билд собирается?

Плюс из-за помещения определения в хэдэр, вы не можете распространять свои исходники без раскрытия деталей реализации. Код - бедная, тонкая, голая и стесняющаяся чужих людских глаз натура. Зачем вы его показываете на общее обозрение? Уважьте малыша. Спрячьте его.

Еще один плюс - удобство чтения кода и его организации. Очень часто нам нужно узнать только публичный интерфейс класса, без погружения в детали. Заголовочник с одним объявлением сущности сильно помогает в этом.

Ну и в мире коммерческой разработки на С++ в принципе принято разделение на сорцы и хэдэры. Поэтому, зачастую, инфраструктура проекта завязвна на этой негласной договоренности. И часто бывает непонятно, куда запихать обособленный заголовочник.

Единственный способ, при котором мы можем использовать разделение объявления и определения по разным файлам с шаблонами - мы должны точно знать, с какими параметрами будет инстанцироваться наш шаблон и в цппшнике предоставить явное его инстанцирование с этими параметрами.

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

И не нужно нигде писать никакой extern! Компилятор из объявления сам ничего не может инстанцировать, поэтому главная задача extern template решается автоматически.

Вы мне скажете, что нужно будет постоянно следить за новопоявившимися параметрами и добавлять явные инстанциации шаблона с ними в цппшник. И я вам отвечу: это правда. Просто в этом случае следить очень просто: у вас перестает компилироваться код)

Однако это лучше, чем в подходе в extern. Если здесь не уследить за новым шаблонным параметров, то компилятор сам сможет неявно с ним инстанцировать шаблон и тогда будут проявляться все негативные эффекты, которых мы и хотели избежать, использовав extern template.

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

Choose the proper tool. Stay cool.

#template #compiler #cppcore
extern template с шаблонами STL

Обещал рассказать, почему вы не сможете подавить неявную инстанциацию для STL сущностей. Это будет хорошим завершением серии, потому что включает множество обсужденных концепций и особенностей.

Оговорюсь, что буду говорить за реализацию стандартной библиотеки от gcc.

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

Далее. Единственным способом запретить неявное инстанцирование при распространении кода с помощью заголовочников - с помощью extern template. Все конкретизации мы не можем запретить, но самые популярные - можем попробовать. В случае успеха это будет давать большой импакт к уменьшению времени компиляции и размеру объектников.

А в недавнем посте "Не всегда инстанциация шаблона нужна для работы программы" мы поговорили о том, что методы, определенные внутри описания класса, неявно помечаются inline. А для inline функций запрет на неявное инстанцирование не работает!

Поэтому для шаблонов STL(под этим акронимом я понимаю все шаблонные классы стандартной библиотеки) вы просто не сможете получить никакого профита от extern template. Стандарт это явно запрещает.

Получается, что все разговоры про запрет или ограничение неявного инстанцирования касаются лишь только кастомного кода. Ограничения на стандартные инструменты наложить не получится, как и поиметь соотвествующих плюшек. Sad, but true. Об этой особенности очень мало говорят даже в англоязычном пространстве. Поэтому вы теперь элита и эксперты в правильной организации шаблонного кода.

Use your tools in proper places. Stay cool.

#cpcore #template #compiler
​​Результаты ревью

Круто вчера постарались, столько проблем нашли в этом маленьком кусочке кода. Больше всего проблем нашли два подписчика со скрина: я не смог выбрать из них одного, так как их пункты хоть и пересекаются, но все же дополняют друг друга различными мыслями. Давайте похлопаем нашим героям!👏👏👏👏👏👏

А теперь скомпануем все воедино. Напомню, что код был такой:

template<typename T, template<class,class...> class C, class... Args>
std::ostream& operator <<(std::ostream& os, C<T,Args...> objs)
{
os << PRETTY_FUNCTION << '\n';
for (auto obj : objs)
os << obj << ' ';
return os;
}


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

❗️ Второй аргумент оператора принимается по значению, что ведет с излишним копированиям. Лучше использовать константную ссылку, так как мы не собираемся изменять значения контейнера.

❗️ В цикле тоже будут копирования, так как obj - объект, а не ссылка. Лучше использовать const auto &.

❗️ В первой строчке смешиваются class и typename. Это путает читателя, заставляя задумываться о тайном замысле использования разных ключевых слов. Лучше везде использовать class, так как Артем отметил , что до С++17 в шаблон-шаблонных параметрах нельзя было использовать typename.

❗️ Не очень выразительное название для аргумента, которым предполагается быть контейнеру. Хотя бы полностью написать Container.

❗️ Вывод элементов очень странный и кривой. Как минимум после последнего элемента будет ставиться пробел. Решить проблему можно с помощью стандартного алгоритма std::copy и интересного экспериментального итератора std::experimental::make_ostream_joiner, который может выводить элементы последовательности через разделитель, не записывая разделитель в конце! Выглядит это так:

std::copy(vec.begin(), vec.end(),
std::experimental::make_ostream_joiner(std::cout, ", "));


На этом очевидные недостатки, которые мог выделить даже не очень разбирающийся в шаблонах читатель, заканчиваются.

Посмотрим чуть поглужбе. Функция используется для отладочных и учебных целей. Это понятно по использованию макроса PRETTY_FUNCTION. Он позволяет посмотреть полную сигнатуру функции с расшифровкой всех шаблонных параметров. Он довольно сильно помогает при обучении. Но к сожалению, этот макрос определен только под gcc/clang. Давайте уж не будем сильно внимание заострять на кроссплатформенности и целесообразности использования этой конструкции. В прод функция явно не пойдет. Д. А более интересные и кроссплатформенные варианты вывода сигнатуры функции можно посмотреть тут.

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

🔞 Для класса std::string уже определен оператор вывода на поток, поэтому при наличии этого куска в общем коде мы просто не сможем выводить строку, так как компилятор найдет 2 подходящие перегрузки и не сможет из них выбрать лучшую. Можно ограничить тип контейнера с помощью sfinae/концептов.

🔞 Перегрузка не будет работать для мап. У них элементы - пары, которые не имеют собственной реализации вывода на поток. Да и вообще: если элементы "контейнера" не умеют выводиться на поток, то будет ошибка. Выход - поставить sfinae/концепт на существовании перегрузки на поток вывода для типа Т.

🔞 В предыдущем пункте я взял слово контейнер в кавычки. Все потому что сигнатура функции способна принимать любую шаблонную тварь, даже какой-нибудь std::shared_ptr. А для него уже перегружен оператор вывода. Опять компилятор не сможет выбрать из двух одинаковых перегрузок. Поэтому было бы неплохо поставить ограничение на существование методов begin() и end().

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Fix your flaws. Stay cool.

#template #STL #cppcore #cpp17 #cpp20
​​Template type deduction
#новичкам

Пользователи 98-го стандарта недоумевали, почему они обязаны при наличии инициализатора указывать полный тип переменной при ее определении. "Если я еще раз напишу полный тип итератора, то я устрою Роскомнадзор", "Вы что, хотите, чтобы я пальцы стёр?!" и тд. У многих были такие мысли. И это, вообще говоря, было очень странно, потому что компилятор уже на тот момент мог сам выводить тип на основе типа другого выражения!

В шаблонах.

Вам же не обязательно писать в шаблонных функциях тип шаблонного аргумента. В некоторых случаях компилятор за вас это может сделать.

template <class Container>
size_t my_size(const Container& container)
{
return container.size();
}

std::cout << my_size(std::vector<int>(10, 0)) << std::endl;


Здесь выведется 10 и, как вы видите, для функции my_size мы не указывали явным образом шаблонный тип.

Ну и раз уже есть наработанная схема, к которой разработчики уже привыкли, то почему бы именно ее не использовать в качестве основы вывода типов для auto? Этим риторическим вопросом задались контрибьютеры в 11-й стандарт и теперь у нас действительно есть ключевое слово auto, для которого вывод типов практически ничем не отличается от вывода типов для шаблонных функций!

Поэтому надо понимать, что это за зверь такой, чтобы осознанно использовать auto.

Чтобы мы все понимали, о чем конкретно будем говорить, посмотрим на следующий псевдокод:

template <class T>
void func(ParamType param) {...}

func(expression);


Все будем разбирать на примере подобной шаблонной функции. Так вот процесс вывода типов ParamType и Т на основании типа выражения expression - это и есть вывод шаблонных типов.

Небольшой пример:

template <class T>
size_t my_size(const std::vector<T>& vec) {...}

template <class T>
void fun(const T& param) {...}

my_size(std::vector<int>(10, 0));
int i = 42;
fun(i)


В случае c my_size ParamType - const std::vector<T>&, а тип T - int. В случае с fun ParamType принимает вид типа Т, обвешанного побрякушками, типа const- и ссылочного квалификаторов. Здесь ParamType = const T&, а Т = int.

То есть ParamType - все то, что стоит слева от имени шаблонного параметра, и на основе выведенного ParamType уже принимается решение о типе Т. Поэтому очень важно понимать не только, какой тип имеет expression, но и какой вид принимает ParamType. Есть всего 3 мажорных варианта:

1) ParamType - указатель или ссылка, но не универсальная ссылка.

2) ParamType - универсальная ссылка.

3) ParamType - ни указатель, ни ссылка.

Все это в следующих постах будем раскрывать подробнее.

Use deduction. Stay cool.

#cppcore #template
​​Небольшой пролог для вывода типов
#новичкам

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

А сейчас я объясню некую концепцию, которая у меня сложилась в голове по ходу изучения нюансов вывод типов. Когда-то мне она помогла выровнять и законнектить все знания по этой теме, потому что примеров в интернете мало, иногда вкинут пару фраз про определенный кейс и все. А ты сиди и голову ломай, как этот черный ящик работает.

Статей с таким количеством примеров, как в следующих постах, исчезающе мало, поэтому контент уникальный. Надеюсь, вам понравится)

Коротко напомню контекст. ParamType - тип выражения-параметра функции. T - шаблонный тип функции :

template <class T>
void func(ParamType param) {...}

func(expression);


Введу такую концепцию, как слои вложенности типов. Слои есть не у всех типов, а только у тех, у кого есть вложенность)
Вложенность актуальна в основном для указателей и шаблонных типов. Тип Т вложен в указатель(указетель - отдельный тип). Тип Т вложен в шаблонный контейнер(например).
Слои могут быть у типа expression и у типа param. Так вот одна из задач вывода - правильно заматчить слои этих типов друг на друга.

Представьте, что тип expression - это капуста и листы этой капусты - слои вложенности. Чтобы из типа expression грубо получить тип Т, нужно оторвать от капусты столько слоев, сколько есть в типе ParamType. И оставшаяся качерышка - и есть выведенный тип Т. Приведу примеры.

Простой одинокий шаблонный параметр.
template <class T>
void func(T param) {...}

Здесь нулевая вложенность типа параметра(нет слоев). Какую бы кракозябру вы бы туда не засунули, тип Т будет отличаться от типа expression разве что константностью и ссылочность. От капусты ни одного листа не отрываем и в выводе типа будут участвовать все слои expression.

Засунем туда переменную типа RandomType без вложенности - в выводе T будет полностью участвовать этот тип и по итогу Т будет равен RandomType.
Если засунем шаблонный тип std::set<int> с двумя слоями вложенности: внешним для std::set и внутренним для int, то в выводе будут участвовать оба слоя и Т будет иметь такой же тип std::set<int>. Снова ни одна капуста не пострадала.

Дальше ссылка
template <class T>
void func(T& param) {...}

Казалось бы ссылка - это уже индирекция(под капотом лежит указатель). Однако с помощью ссылки вы только непосредственно объектом можете управлять! Поэтому в этом смысле никакой индирекции нет и тут также нулевая вложенность и рассуждения, как для предыдущего примера.

template <class T>
void func(T * param) {...}

template <class T>
void func1(std::vector<T> param) {...}

Указатель или вектор - уже появляется вложенность: наружный тип(указатель или шаблонный вектор) и внутренний тип. Так и получается, что у нас есть внутренний и внешний слой. И за счет того, что мы определили внешний слой(сказали, что наш параметр - указатель/вектор), в выводе параметра Т участвует только внутренний слой типа expression и все что в него вложено. Передам в func указатель на инт - от этой капусты отрываем внешний листок и остается тип инт, в который и выводится Т.
Если передам двойной указатель на инт int **, то мы убираем внешний слой указателя и от типа expression остается уже одинарный указатель на int *. И соответственно Т выведется в int *.

В чем проблема и зачем это все вводить и объяснять - большинство примеров в интернете ограничиваются лишь интами и ссылками на инты. Я же хочу разобрать чуть более сложные кейсы, которые не укладываются в стандартные объяснения происходящего.

Типы шаблонных параметров приблизительно так и выводятся, как описано в этом посте. Однако есть нюансы с константностью и ссылочностью, которые придется разобрать в следующих постах.

Поддержите пост лайками, если хотите подробного разбора этой темы.

Support hardcore stuff. Stay cool.

#cppcore #template
​​Как посмотреть шаблонный тип
#новичкам

Вчера Антон сделал важное замечание, что неплохо бы показать, как самому посмотреть, во что выводится тип Т в каждом конкретном случае. Собсна, погнали.

В С++ стандартными средствами конечно можно это сделать, но решение будет довольно громоздкое и некрасивое с точки зрения пользователя.

Хотелось бы что-то очень простое, желательно вообще однострочное. Обычно таких решений в плюсах нет и надо городить огород, но не в этом случае. Благодаря обширным возможностям препроцессора компиляторы зачастую определяют свои макросы, которые раскрываются в сигнатуру функции. В случае же с шаблонной функцией, они показывают и правильный выведенный шаблонный тип.

Для шланга и гцц этот макрос называется __PRETTY_FUNCTION__, а для msvc - __FUNCSIG__. Пользоваться ими можно примерно так:

#if defined __clang__ || __GNUC__
#define FUNCTION_SIGNATURE __PRETTY_FUNCTION__
#elif defined __FUNCSIG__
#define FUNCTION_SIGNATURE __FUNCSIG__
#endif

template<class T>
void func(const T& param) {
std::cout << FUNCTION_SIGNATURE << std::endl;
}

func(std::vector<int>{});


Для кланга вывод будет такой:
void func(const T &) [T = std::vector<int>]


Для msvc:
void __cdecl func<class std::vector<int,class std::allocator<int> >>(const class std::vector<int,class std::allocator<int> > &)


Тут на мой взгляд msvc предоставляет несколько более полный и понятный функционал, но кому как удобно.

Можете поиграться в годболте.

See through things. Stay cool.

#compiler #template
ParamType - не cv-квалифицированная ссылка
#новичкам

Список постов по теме , Пост про слои

Пойдем по порядку мажорных вариантов. Первым в очереди случай, когда ParamType - не cv-квалифицированная(без пометок const и volatile), не универсальная ссылка. Дальше только про константность буду говорить в контексте cv-квалификаторов, а на volatile забью(впрочем, ему не привыкать).

Первый вариант может быть такой:

template <class T>
void func(T& param) {...}
// | |
// ParamType

func(expression);
// decltype(expression) - expression's type


Тут порядок такой: берете полный тип expression -> откидывайте от него внешнюю ссылочность, если она есть -> все, что осталось, запихиваете в Т. Так как ParamType не имеет слоев вложенности, то мы ничего не отрываем от типа expression. Таким образом параметр Т никак не может быть ссылочным типом, а тип ParamType - всегда ссылка. Пример:

template <class T>
void func(T& param) {...}

int x = 42; // x is an int
const int const_x = x; // const_x is a const int
const int& const_ref_x = x; // const_ref_x is a reference to x as a const int
std::list<double> lst;

func(x); // T is int, ParamType is int&
func(const_x); // T is const int, ParamTypeis const int&
func(const_ref_x); // T is const int, ParamType is const int&
func(lst); // T is std::list<double>, ParamType is std::list<double>&


Пойдем по порядку. С переменной x все сильно очевидно: тип param - int&, тип Т - int. Как и с lst: тип param - std::list<double>&, тип Т - std::list<double>
Теперь добавим щепотку константности. У const_x нет ссылочности, поэтому запихивает полный ее тип в Т, который выведется в const int.
Для const_ref_x сначала откидываем ссылочность и все оставшееся пихаем в Т, который выведется в const int.

Давайте очень важную особенность проследим. Каждый раз, когда мы объявляем константу или константную ссылку и передаем их в шаблон, ParamType которого T&, тип Т оказывается тоже константой. Это очень важный момент для обобщенного программирования: в функцию кто-то может передать константу. И он очень естественно ожидает, что значение его переменной не изменится. Ну может и не ожидает(в плюсах нужно настроиться ожидать что угодно), но очень хочет, чтобы оно не менялось. Иначе БУМ! И вот такой механизм сохранения константности шаблонного типа и позволяет шаблонным функциям, принимающим неконстантную ссылку вида Т&, спокойно принимать в себя константные объекты и не изменять их(так как сам тип неизменяемый).

А что если ParamType будет вложенным типом?
template <class T>
void func(std::vector<T>& param) {...}
// | |
// ParamType

std::vector<int> vec(10, 0);
const std::vector<int> const_vec(10, 0);
std::vector<int>& ref_vec = vec;
int a = 0;
int b = 1;
std::vector<std::reference_wrapper<int>> vec_of_ref{a, b};

func(vec); // T is int, ParamType is std::vector<int>&
func(const_vec); // ERROR!
func(ref_vec); // T is int, ParamType is std::vector<int>&
func(vec_of_ref); // T is std::reference_wrapper<int>, ParamType is std::vector<std::reference_wrapper<int>>&


В этом случае мы явно сказали, что хотим принимать какой-то вектор. Мы просто отрываем этот слой вместе со ссылочностью и оставшееся - наш тип Т.

Но! Так как тип внешнего слоя полностью специфицирован, то у нас никак не получится передать в функцию константный вектор или ссылку на константный вектор. То есть тут сама сигнатура функции подразумевает изменяемый внешний слой, а мы пытаемся туда передать неизменяемый объект. Компилятор запрещает нам такие мувы делать.

Почему в одном случае можно передать константую ссылку, а в другом нет? Все из-за волшебного типа Т, который может быть кем угодно. Ссылка Т& может быть ссылкой на любой тип, в том числе и константный. Можете прям так и читать: ссылка на что угодно. Этот Т как бы вбирает в себя все особенности типа.
А для такого выражения std::vector<T>& мы читаем: ссылка на вектор от чего-угодно. Внешний слой зафиксирован, а внутренний может содержать в себе что-угодно.

Protect your invariants. Stay cool.

#cppcore #template
ParamType - не cv-квалифицированный указатель
#новичкам

Список постов по теме , Пост про слои

Переходим к выводу параметров для указателей. Давайте просто накидаем много указателей разных типов и посмотрим, какой тип шаблонного параметра выведется для них.

template <class T>
void func(T* param) {...}
// | |
// ParamType

int x = 42;
int * p_x = &x;
const int * p_const_x = &x; // p_const_x is a ptr to const int
int * const const_p_x = &x; // const_p_x is a const ptr to int
const int * const const_p_const_x = &x; // const_p_const_x is a const ptr to const int
int ** p_p_x = &p_x; // p_p_x is a ptr to a ptr to x as int
const int * const * const const_p_const_p_const_x = &const_p_const_x; // const_p_const_p_const_x is a const ptr to a const ptr to const int

func(p_x); // T is int, param's type is int*
func(p_const_x); // T is const int, param's type is const int*
func(const_p_x); // T is int, param's type is int *
func(const_p_const_x); // T is const int, param's type is const int *
func(p_p_x); // T is int *, param's type is int **
func(const_p_const_p_const_x); // T is const int * const, param's type is const int * const *


В комментах к строчкам все расписано, что и во что выводится. Я же обращу внимание на принцип. Снова возвращаемся к капусте. ParamType - указатель, а значит от типа выражения, переданного в функцию, мы должны этот указатель и оторвать и получим тип Т. Так и происходит во всех случаях. Если передаем константный указатель - снимаем этот слой вместе с константностью.

Здесь все просто, работает также как и со ссылками. Почти. Семантика сохранения константности шаблонного типа повторяется. То есть если указатель указывает на константный инт, то тип Т тоже будет константным. Однако, если константной ссылки не может быть(то что в народе называют константной ссылкой - это на самом деле ссылка на константный объект: сама по себе ссылка неизменяема, она просто может указывать на другой объект), то указатель может быть константным. То есть здесь уже играют роль слои вложенности. В этом случае, константность внутреннего слоя(который ближе к самому объекту) непосредственно отражается на шаблонном параметре Т, а константность внешнего слоя к типу Т не будет иметь отношения. Примерами здесь являются const_p_x, const_p_const_x, const_p_const_p_const_x.

template <class T>
void func(std::list<T> * param) {...}
// | |
// ParamType

std::list<double> lst;
std::list<std::unique_ptr<const double>> lst_of_const;
std::list<std::vector<std::unique_ptr<const int>>> lst_vec_of_const;
std::list<std::vector<std::unique_ptr<const int>>> * const const_p_lst_vec_of_const = &lst_vec_of_const;

func(&lst); // T is double, param's type is std::list<double>
func(&lst_of_const); // T is std::unique_ptr<const double>, param's type is std::list<std::unique_ptr<const double>>*
func(&lst_vec_of_const); // T is std::vector<std::unique_ptr<const int>>, param's type is std::list<std::vector<std::unique_ptr<const int>>>*
func(const_p_lst_vec_of_const); // T is std::vector<std::unique_ptr<const int>>, param's type is std::list<std::vector<std::unique_ptr<const int>>>*


В этом примере у типа param аж 2 слоя вложенности определены: 1 на указатель и 2 на контейнер. От типа аргумента в начале отрезаем указатель вместе с константностью, а далее и слой с std::list. По итогу тип Т выводится в то, что стоит в треугольных скобках у листа.

Есть одна интересная деталь: сигнатура функции подразумевает, что сам указатель не будет константным, то есть его можно изменять. И если вы передадите в нее константный указатель, то эта константность очень неожиданно пропадает и расплывается в пучине правил вывода типов. Так происходит с переменными const_p_x, const_p_const_x, const_p_const_p_const_x и const_p_lst_vec_of_const Если для нешаблонной функции с параметром неконстантного указателя при передаче в нее константного указателя была бы ошибка компиляции, то здесь эта штука проходит фэйс-контроль. Помните об этой об этой особенности и потенциальной опасности.

Dig deeper. Stay cool.

#template #cppcore
template <class T>
void func(T * const param) {...}
// | |
// ParamType

int x = 42;
int * p_x = &x; // p_x is a ptr to x as int
int ** p_p_x = &p_x; // p_p_x is a ptr to a ptr to x as int
const int * p_const_x = &x; // p_const_x is a ptr to x as a const int
int * const const_p_x = &x; // const_p_x is a const ptr to x as int
const int * const const_p_const_x = &x; // const_p_const_x is a const ptr to x as const int

func(p_x); // T is int, param's type is int * const
func(p_const_x); // T is const int, param's type is const int * const
func(const_p_x); // T is int, param's type is int * const
func(const_p_const_x); // T is const int, param's type is const int * const
func(p_p_x); // T is int *, param's type is int * * const


Здесь все еще проще. param - всегда константный указатель, что бы вы в функцию не передали. А для вывода Т просто снимаем внешний слой указательности со всем квалификаторами и вуаля - ваш тип Т готов.

Самый душный блок из вывода типов готов, дальше будет по-веселее.

Believe in good future. Stay cool.

#cppcore #template
​​ParamType - универсальная ссылка
#опытным

Один и самых интересных, сложных, непонятных и противоречивых кейсов в выводе шаблонных параметров. Да и интересный он скорее из-за всего остального.

Только при такой сигнатуре шаблонной функции можно считать ее параметр универсальной ссылкой:

template <class T>
void func(T&& param) {...}

func(expression);


То есть это rvalue reference на cv-неквалифицированный тип. Только в таком виде тип param называется универсальной ссылкой. Как говорят в школе:
И ни в каком другом виде!

Ни
template <class T>
void func(std::vector<T>&& param) {...}

Это просто rvalue reference.
Ни
template <class T>
void func(const T&& param) {...}
Это тоже просто rvalue reference! Только константный.
И к последним двум кейсам применяются правила
отсюда
 и 
отсюда.


Когда expression - rvalue reference, то Т выводится безссылочным типом, чтобы тип ParamType был rvalue reference of T. Если тип expression - lvalue, то Т выводится в тип lvalue reference. Самое интересное, что это единственный кейс, когда тип Т выводится в ссылку.

Есть такое правило, что & + && = &. То есть при использовании универсальной ссылки в параметре шаблонной функции при передаче туда lvalue|lvalue reference, этот параметр выводится в lvalue reference. Это происходит именно за счет того, что шаблонный тип выводится в тип lvalue reference. Условно: функция принимает Т && , T выводится в int&, подставляем Т в параметр функции и получаем int& &&. Но такого синтаксиса нет и 2 ссылки коллапсируют в одну левую ссылку int&.


template<typename T> void f(T&& param); // param is a universal reference

int x = 27;
const int cx = x;
const int& lrx = x;
int&& rrx = 42;

f(x); // x is lvalue, so T is int&, param's type is also int&
f(cx); // cx is lvalue, so T is const int&, param's type is also const int&
f(lrx); // lrx is lvalue, so T is const int&, param's type is also const int&
f(27); // 27 is prvalue, so T is int, param's type is therefore int&&
f(std::move(rrx)); // rrx is xvalue, so T is int, param's type is therefore int&&


Обратите внимание на первые 3 кейса. Там Т выводится в lvalue reference тип. В двух последних Т - просто int безо всяких ссылок.

Мы на самом деле уже обсуждали универсальные ссылки в рамках серии статей про категории выражения. Вот ссылочка на эту статью с более полным описанием процессов.

В этой статье я просто хотел подсветить самые важные моменты в этой теме, которые касаются именно вывода типов.

Stay universal. Stay cool.

#cppcore #cpp11 #template
​​Примеры использования const T&&
#опытным

В прошлый раз мы поговорили о том, что можно использовать константную правую ссылку для того, чтобы запретить принимать любые rvalue reference в функцию.

Для чего это может быть нужно?

Допустим, мы храним в поле класса в каком-то виде ссылку на объект. И нам бы очень не хотелось принимать в конструкторе rvalue reference, потому что возможно сразу же после выхода из конструктора для объектов вызовется деструктор и хана этим объектам. И встречаем UB из-за хранения битой ссылки.

Есть такой стандартный класс std::reference_wrapper и его функции помощники std::ref() и std::cref(). Поскольку std::reference_wrapper предполагает хранение ссылки только для lvalue, то стандарт удалил перегрузки std::ref() и std::cref(), которые принимают const T&&.

template <class T> void ref(const T&&) = delete;
template <class T> void cref(const T&&) = delete;


По той же самой причине такая перегрузка удалена у функции std::as_const, которая формирует левую ссылку на константный тип из аргумента.

template< class T >  
constexpr std::add_const_t<T>& as_const( T& t ) noexcept;


Также константные правые ссылки используются в более сложных штуках, типа std::optional, когда нужно вернуть из него значение.

constexpr const T&& operator*() const&&;
constexpr const T&& value() const &&;


С этой же целью оно используется, например, и в std::get.

template< std::size_t I, class... Types >  
constexpr const std::variant_alternative_t<I, std::variant<Types...>>&&
    get( const std::variant<Types...>&& v );

template< class T, class... Types >
constexpr const T&& get( const std::variant<Types...>&& v );


В таких случаях использование const T&& оправдано передачей информации и о ссылочности типа, и о его константности. Это важно в обобщенном программировании, потому что никто не знает с каким типом будет работать шаблонная сущность. Вы вполне можете получить константный временный объект std::optional(да и любого другого объекта), это синтаксически корректно. И чтобы геттер его внутреннего значение отражал свойства обертки, приходится перегружать эти геттеры для любых возможных параметров. Так вот например методы std::optional упомянутые выше вызовутся только для временных константных объектов. И эти свойства отображаются в возвращаемом значении.

Также не стоит забывать, что константность объекта не накладывает ультимативных ограничений на использование объекта. Есть мутабельные и статические поля, которые можно изменять, и плевать они хотели на вашу константность. А также указатели. Мы не можем менять сам указатель, но можем изменить объект, на который он указывает. Это немного расширяет спектр возможностей использования константных правых ссылок, но не прям существенно. В голову пришел очевидный пример - pimpl idiom. Согласно этой идиоме класс хранит указатель на реализацию, в которой может лежать все, что угодно. Все операции, которые как-то изменяют состояние объекта, влияют на данные внутри указателя. Поэтому снаружи кажется, что объект и не изменился. Да и старый объект можно будет использовать. Непонятно только, зачем менять привычные традиции использования правых ссылок, но все же.

// MyClass.hpp
class MyClass {
public:
MyClass();
MyClass(int g_meat);
MyClass(const MyClass &&other); // const rvalue reference!
~MyClass();
int GetMeat() const;
private:
class Pimpl;
Pimpl *impl {};
};

// MyClass.cpp
class MyClass::Pimpl {
public:
int meat {42};
};

MyClass::MyClass() : impl {new Pimpl} { }

MyClass::MyClass(int g_meat) : MyClass() {
impl->meat = g_meat;
}

MyClass::MyClass(const MyClass &&other) : MyClass()
{
impl->meat = other.impl->meat;
other.impl->meat = 0;
}

MyClass::~MyClass() { delete impl; }

int MyClass::GetMeat() const {
return impl->meat;
}

// main.cpp
int main() {
const MyClass a {100500};
MyClass b (std::move(a)); // moving from const!
std::cout << a.GetMeat() << "\n"; // returns 0, b/c a is moved-from
std::cout << b.GetMeat() << "\n"; // returns 100500
}


Stay useful even if nobody understands you. Stay cool.

#template #cpp11 #STL
​​ParamType - не ссылка и не указатель
#новичкам

Список постов по теме

template <class T>
void func(T param) {...}

func(expression);


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

Это значит, что param - совершенно новый объект типа Т, который создается из аргумента, переданного функции. После создания он живет своей жизнь и никак не зависит от исходного объекта.

Есть кстати распространенное заблуждение или недоговаривание, что при передаче объекта в функцию по значению происходит копирование. Это не совсем правда. Вызывается конструктор объекта на основе переданного параметра. А вот какой именно конструктор вызовется - copy или move - определяется типом ссылочности аргумента. Передадут lvalue - вызовется copy ctor, передадут rvalue reference - вызовется move ctor. Короткий пример:

struct Test {
Test() = default;
Test(const Test& other) {
std::cout << "Copy ctor" << std::endl;
}
Test(Test&& other) {
std::cout << "Move ctor" << std::endl;
}
};

template<class T>
void fun(T t) {
std::cout << "Hello, subscribers!!!" << std::endl;
}

int main () {
Test t;
fun(t);
fun(std::move(t))
}


Copy ctor
Hello, subscribers!!!
Move ctor
Hello, subscribers!!!


Как видим, при передаче аргумента через std::move происходит вызов мув конструктора.

Кстати, недавно дошел до очень простого объяснения мув-семантики и всего, что вокруг нее вертиться. Буквально за один пост все поймут всё про нее. Если хотите такой пост - жмакайте кита)

Вернемся к шаблонам

Как в этой ситуации выводится шаблонный тип?

Если у типа expression есть верхняя ссылочность/константность/волатильность - все в мусорку, оставшееся - тип Т.

int x = 42;
const int cx = x;
const int& rx = x;
const int * const px = &x;

func(x); // T's and param's types are both int
func(cx); // T's and param's types are again both int
func(rx); // T's and param's types are still both int
func(px); // T's and param's types are const int*


Обратите внимание: хотя cx и rx представляют константные значения, param не является константой. В этом есть смысл. param — это объект, полностью независимый от cx и rx, копия cx или rx. Тот факт, что cx и rx не могут быть изменены, ничего не говорит о том, можно ли изменять param. Вот почему константность expression игнорируется при определении типа параметра: то, что expression не может быть изменено, не означает, что его копия не может быть изменена.

С указателем похожая история: он как бы копируется в функцию. В функции уже другой указатель - param - и он не обязан быть сам по себе константным. И согласно правилам вывода типов, таковым и не является.

Если шаблонный параметр немного сложнее - например, тип Т вложен в другой шаблонный тип, то возвращаемся к нашей капусте. Снимаем столько слоев с типа expression, сколько явно определено для типа param - и это будет тип Т. Аргумент передается также по значению.

template <class T>
void func(std::vector<T> param) {...}

std::vector<int> vec;
const std::vector<int> const_vec;

func(vec); // T is int, param's type is std::vector<int>
func(const_vec); // T is int, param's type is std::vector<int>


На этом мы заканчиваем рассматривать мажорные кейсы видов шаблонных параметров функций. Дальше будем какие-то менее встречающиеся примеры рассматривать.

Be independent subject. Stay cool.

#cppcore #template