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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Member initialization. Best practices
#новичкам

Пост по запросу подписчика. Вот его вопрос.

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

Здесь я буду приводить какое-то общие и распространенные принципы. К каждому можно придраться и сказать "а у нас в проекте по-другому!". Исключения и другие подходы есть везде. Если хотите высказать свои варианты - комменты открыты.

Начну с того, что нужно предпочитать инициализировать поля либо с помощью списка инициализации конструктора, либо с помощью default member initializer. Дело в том, что все поля на самом деле инициализируются до входа в конструктор! Если списком инициализации или default member initializer'ом не установлено, как поле должно инициализироваться, то в конструктор оно попадет инициализированным по умолчанию. Именно поэтому, например, не можете в конструкторе инициализировать объект класса, у которого нет конструктора по умолчанию. Будет ошибка компиляции и у вас потребуют дефолтный конструктор. Запомните: конструктор нужен для нетривиальных вещей. С простой иницализацией справятся ctor initialization list и инициализатор по умолчанию.

Далее. Остается 2 способа, как инициализировать. Какой из них выбрать и в какой пропорции смешивать?

CppCoreGuidelies говорят нам: "Prefer default member initializers to member initializers in constructors for constant initializers".

То есть, если инициализатор константный, то используйте default member initializer.

Причина: inplace инициализатор делает явным то, что именно эти дефолтовые значения будут использоваться во всех конструкторах. Пример:

class X { // BAD 
int i;
string s;
int j;
public:
X() :i{666}, s{"qqq"} { } // j is uninitialized
X(int ii) :i{ii} {} // s is "" and j is uninitialized
// ...
};


Как в этом случае читатель кода поймет, была ли инициализация j специально пропущена(что скорее всего не очень гуд) или было ли для s намеренным выставление его значения в "qqq" в первом случае и в пустую строку во втором случае(почти стопроцентный баг)? Все эти ошибки могут появиться при добавлении новых полей в класс. По классике: добавили новое поле, использовали его в методах, но вот в одном месте упустили инициализацию. Кейс настолько жизненный, что мое почтение.

Более адекватный вариант:
class X2 { 
int i {666};
string s {"qqq"};
int j {0};
public:
X2() = default; // all members are initialized to their defaults
X2(int ii) :i{ii} {} // s and j initialized to their defaults
// ...
};


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

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

Используйте default member initializer и будет вам счастье!

Stay happy. Stay cool.

#cpp11 #cppcore #goodpractice
Директивы ifdef, ifndef, if
#новичкам

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

Этот способ - использование директив препроцессора #ifdef, #ifndef, #if. Все три - условные конструкции. Первая смотрит, определен ли в коде какой-то макрос. Если да, то делаем одни действия, если нет - другие. Второй наоборот, входит в первую ветку условия, если макрос не определен, и входит во вторую, если определен. Директива #if проверяет какое-то условие, ничего необычного. Все три директивы могут иметь как полные формы(с веткой в случае если условие ложно), так и неполные(без "else").

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

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

int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
int result = 0;
#if CPU_TYPE == 0
// mmx|sse|avx code
#elif CPU_TYPE == 1
// arm neon code
#else
static_assert(0, "NO CPU_TYPE IS SPECIFIED");
#endif
return result;
}


Если каждое значение CPU_TYPE включает нужную ветку кода и убирает из текста программы все остальные.

Если мы хотим оптимизировать только под интеловские процессоры, то можем написать чуть проще:

int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
int result = 0;
#ifdef OPTIMIZATION_ON
// mmx|sse|avx code
#else
for (int i = 0; i < vec1.size(); ++i)
result += vec1[i] * vec2[i];
#endif
return result;
}


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

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

Широко известно, что такой способ не только устарел, но еще и опасен. Завтра посмотрим, чем конкретно.

Choose the right path. Stay cool.

#compiler
​​Правила константности
#новичкам

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

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

Не будем долго задерживаться над константными методами. Константные объекты могут вызвать только константные методы. Все. Синтаксис такой:

void Class::Method(Type1 param1, Type2 param2) const {}

Теперь и константные, и неконстантные объекты могут вызывать метод Method.

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

Константный объект можно объявить двумя способами:

const T obj;
// Либо
T const obj;


Эти записи абсолютно эквивалентны! Это очень важно запомнить, потому что при разговоре о ссылках и указателях это играет большую роль.

Собственно также есть 2 нотации объявления массивов констант:

const T arr[5];
// либо
T const arr[5];


И 2 нотации определения ссылок:

const T& ref;
// либо
T const & ref;


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

Обычно при таком объявлении ссылку называют константной. Это не совсем верно. Ссылка при любых обстоятельствах сама по себе является константной. Как только вы забиндили ссылку на объект, она всегда будет смотреть на этот объект и изменять его. Более подробно про особенности ссылок посмотреть тут. При новом присваивании ссылки вызовется оператор присваивания и изменится существующий объект.

struct Type {
Type& operator=(const Type& other) {
std::cout << "copy assign" << std::endl;
return *this;
}
};

Type a{};
Type& b = a;
b = Type{};
// OUTPUT:
// copy assign


Когда говорят "константная ссылка" имеют ввиду ссылку на константу. И при любом виде объявления const T& ref или T const & ref она также будет ссылкой на константу.

Теперь указатели. Наверное самое сложное из всего перечисленного. Указатели, в отличии от ссылок, сами могут быть константными, да еще и указывать на константные объекты. А еще могут быть многоуровненые указатели. В общем сложно. Но есть правило: при объявлении указателя каждое появление ключевого слова const относится к тому уровню вложенности, который находится слева от этого слова. Вы просто читаете объявление справа налево и получаете правильное понимание объявления. Примеры:

// Читаем справа налево
int * const ptr; // ptr - это константный указатель на инт
int * * const ptr; // ptr - это константный указатель на указатель на инт
int * const * const ptr; // ptr - это константный указатель на константный указатель на инт

// Самый низкий уровень, который относится к самому объекту,
// можно писать двумя способами, о которых мы говорили выше

int const * const * * const ptr; // ptr - это константный указатель на указатель
// на константный указатель на интовую константу
const int * const * * const ptr; // Тоже самое


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

Rely on fixed thing in your life. Stay cool.

#cppcore

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
nullptr
#новичкам

Вероятно, каждый, кто писал код на C++03, имел удовольствие использовать NULL и постоянно ударяться мизинцем ноги об этот острый уголок тумбочки. Дело в том, что NULL использовался, как обозначение нулевого указателя, который никуда не указывает. Но если он для этого и использовался - это не значит, что он таковым являлся. Да и являлся он котом в мешке. Это макрос, который мог быть определен как 0 aka int zero или 0L aka zero long int, но всегда это вариация интегрального нуля. И уже эти чиселки могли быть приведены к типу указателя.

Вот в этом-то и вся проблема. NULL очень явно хочет себя видеть в роли указателя, но по факту в зеркале видит число. Допустим, у нас есть 2 перегрузки одной функции: одна для инта, вторая для указателя:

class Spell { };

void castSpell(Spell* theSpell);
void castSpell(int spellID);

int main() {
castSpell(NULL);
}


Намерения ясны: мы хотим вызвать перегрузку для указателя. Но это гарантировано не произойдет! В произойдет один из двух сценариев: если NULL определен как 0, то просто без объявления войны в 4 часа утра 22 июня вызовется вторая перегрузка. Если как 0L, то компилятор поругается на неоднозначный вызов: 0L может быть одинаково хорошо сконвертирован и в инт, и в указатель.

Проблему можно решить енамами, принимать вместо его вместо инта и передавать для нулевого spellID что-то типа NoSpell. Но надо опять городить огород. Почему все не работает из коробки?!

С приходом С++11 начало работать из коробки. Надо только забыть про NULL и использовать nullptr.

Ключевое слово nullptr обозначает литерал указателя. Это prvalue типа std::nullptr_t. И nullptr неявно приводится к нулевому значению указателя для любого типа указателя. Это объект отдельного типа, который теперь к простому инту не приводится.

Поэтому сейчас этот код отработает как надо:

class Spell {};

void castSpell(Spell* theSpell);
void castSpell(int spellID);

int main() {
castSpell(nullptr);
}



Так как nullptr - значение конкретного типа std::nullptr_t, то мы может принимать в функции непосредственно этот тип, а не общий тип указателя. Такая штука используется, например, в реализации std::function, конструктор которого имеет перегрузку для std::nullptr_t и делает тоже самое, что и конструктор без аргументов.

/*
* @brief Default construct creates an empty function call wrapper.
* @post !(bool)*this
*/
function() noexcept
: _Function_base() { }

/
* @brief Creates an empty function call wrapper.
* @post @c !(bool)*this
*/
function(nullptr_t) noexcept
: _Function_base() { }


По той же причине nullptr даже при возврате через функцию может быть приведен к типу указателя. А вот обычные null pointer константны не могут похвастаться таким свойством. Они могут приводиться к указателям только в виде литералов.

template<class T>
constexpr T clone(const T& t)
{
return t;
}
 
void g(int *)
{
std::cout << "Function g called\n";
}
 
int main()
{
g(nullptr); // Fine
g(NULL); // Fine
g(0); // Fine
 
g(clone(nullptr)); // Fine
// g(clone(NULL)); // ERROR: non-literal zero cannot be a null pointer constant
// g(clone(0)); // ERROR: non-literal zero cannot be a null pointer constant
}


clone(nullptr) вернет тот же nullptr и все будет работать гладко. А для 0 и NULL функция вернет просто int, который сам по себе неявно не конвертится в указатель.

Думаю, что вы все и так пользуете nullptr, но этот пост обязан быть на канале.

Как говорится "Use nullptr instead of NULL, 0 or any other null pointer constant, wherever you need a generic null pointer."

Be a separate subject. Stay cool.

#cppcore #cpp11
Вывод типов
#новичкам

С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "im::so::tired::of::typing::long<types>::iterator". Точнее никогда.

Да, есть алиасы и синонимы, это нужные и полезные вещи. Но не на все же гигадлинные типы их вводить.

Очень хочется, чтобы работу по "написанию" типов делал кто-то за нас. Ведь в конце концов, все сигнатуры функций и методов известны, нормальные пацаны используют явные плюсовые касты, инициализаторы обычно представляют из себя понятные типы. Да, есть всякие приколы с неявным приведением типов и неэксплисит конструкторами от одного аргумента. Но попробовать-то стоит?

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

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

std::unordered_map<std::string, std::vector<Customer>> data;


Так вот, чтобы по этой мапе проитерироваться раньше нужно было писать вот так:

for (std::unordered_map<std::string, std::vector<Customer> >::iterator it = data.begin(); it != data.end(); it++) {...}


Это конечно никуда не годится, выглядит ужасно, нечитаемо, да и код повторяется. Теперь подключаем 11-у плюсы и случается магия:

for (auto it = data.begin(); it != data.end(); it++) {...}


А добавив заклинание под называнием range-based-for, получим:
for (const auto& elem: data) {...}


Не идеально, это вам не питон. Но уже ощутимо приятнее и короче раза в 3.

Но тут встает вопрос: а как вообще эти типы-то выводятся? Есть наверное какие-то правила, алгоритм, по которому компилятор выводит тип?

Есть. Иначе это было бы магией(хотя грустновато без нее в нашем мире).

Его можно запомнить довольно легко. Поэтому в нескольких следующих постах мы будем разбирать эту тему.

А вообще знаете, что существует 3 вида вывода типов? Может и больше, но 3 точно есть, обещаю)

Delegate your work. Stay cool.

#cpp11
​​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