Грокаем 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
​​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
Один плюс решает все

Вчера мы рассматривали такой код и он фейлился при линковке:

#include <algorithm>

struct foo {
static const int qwerty = 100;
};

int main() {
std::cout << std::max(0, foo::qwerty) << std::endl;
return 0;
}


Но стоит нам добавить всего лишь + к имени переменной foo::qwerty и код сразу же начнет компилироваться и выдавать ожидаемый результат.

int main() {
std::cout << std::max(0, +foo::qwerty) << std::endl;
return 0;
}


Почему?

Для интов определено унарный оперетор +, который возвращает временное значение. Он не реализован в рамках обычных функций С++ и компилятор может как угодно его оптимизировать, но главное, что нам нужно знать - компилятор рассматривает это как новое rvalue значение. Которое может кастится к константной ссылке и эта операция не требует наличия определенного адреса объекта. А так как оригинальная переменная foo::qwerty теперь не odr-used(от нее больше не берут ссылку), то и компилятору не нужно больше определение. Он прекрасно видит значение инициализатора и может просто подставить на место foo::qwerty значение его инициализатора.

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

Focus on positive. Stay cool.

#cppcore
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 rvalue reference
#опытным

В прошлом посте мельком упомянул эту конструкцию, а в этом решил раскрыть по-подробнее.

Правые ссылки были введены в С++11 и с тех пор помогают в реализации семантики перемещения. С помощью таких ссылок мы можем убрать ненужное глубокое копирование объектов и внедрить "перемещение" одного объекта в другой. Достигается это с помощью специальных методов: конструктора перемещения и перемещающего оператора присваивания. Выглядит это так:

struct Movable {
Movable(int i) : num{new int(i)} {}
Movable(Movable&& other) {
num = other.num;
other.num = nullptr;
std::cout << "Don't have to copy in ctor\n";
}
Movable& operator=(Movable&& other) {
if (this != &other) {
delete num;
num = other.num;
other.num = nullptr;
std::cout << "Don't have to copy in assignment\n";
}
return *this;
}
~Movable() { delete num;}
int * num = nullptr;
}

Movable obj1{5};
Movable obj2{7};
Movable&& rvalue_ref = std::move(obj1);
Movable obj3{std::move(rvalue_ref)};
obj2 = std::move(obj3);

// OUTPUT
// Don't have to copy in ctor
// Don't have to copy in assignment


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

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

Так за каким хреном нам тогда нужны константный правые ссылки? Чтобы что? С первого взгляда это выглядит так: мы принимаем правые все правые ссылки в перегрузку(неконстантные ссылки биндятся к константным), но все равно копируем объект, потому что ничего другого сделать не можем. Звучит, как бред.

Но все же есть применение у этой конструкции.

Дело в том, что T&& могут кастится к const T&, T&& и const T&&. Наиболее подходящей перегрузкой будет T&&, дальше const T&& и, наконец, const T&. А вот левые ссылки к правым вообще не могут преобразовываться.

Соотвественно, если мы хотим принимать только lvalue в функцию и никак не пропускать правые ссылки, то "Хьюстон, у нас проблема!". Если мы просто определим перегрузку для const T&, то rvalue reference все равно смогут попадать в эту перегрузку. Что нас сильно огорчает.

Однако мы можем совершить ход конем: пометить перегрузку const T&& удаленной. Так как удаленные функции все еще участвуют в разрешении перегрузок, то для T&& более подходящим выбором будет const T&&, нежели const T&. Но мы намеренно удаляем эту перегрузку и тогда компилятор выдает ошибку из-за того, что не может найти подходящий вариант функции.

struct T{};

void f(T&) { std::cout << "lvalue ref\n"; }
void f(const T&) { std::cout << "const lvalue ref\n"; }
void f(const T&&) = delete; //{ std::cout << "const rvalue ref\n"; }

const T g() {
return T{};
}

int main() {
f(g()); // error: use of deleted function 'void f(const T&&)'
f(T{}); // error: use of deleted function 'void f(const T&&)'
}


Это применение и еще парочку других мы рассмотрим на реальных примерах в следующий раз.

Remove obstructing things from your life. Stay cool.

#cppcore #cpp11
​​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
​​Правильный swap двух объектов Ч1

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

В принципе нам нужна функциональность, которая сможет обменять данных двух объектов местами. Она может называться как угодно, никто не запрещает назвать вам swap функцию как TheMostTrickyFunction.

Но так делать не очень удобно. Все привыкли использовать std::swap для обмена значений. Поэтому логично как минимум назвать функцию swap.

Дальше будем рассматривать по очереди возможные варианты.

Можно определить метод swap внутри класса:

struct my_type
{
void swap(my_type&) { /* swap members / }
};


И хоть это будет работать в пользовательском коде just fine, но мы не сможем для такого типа например использовать std::sort, которая вызывает свободную функцию swap.

Но мы можем найти примеры такого дизайна даже в стандартной библиотеке. Например std::vector имеем метод swap, который обменивает данные двух векторов. Но тут важен контекст: до появления мув-семантики обмен векторов через std::swap приводило бы к нежелательным копированиям. Поэтому в те времена многие объекты имели свою оптимизированную версию в виде метода.

Сейчас вы можете использовать std::swap на двух векторах и не парится по поводу перфоманса. Так что просто swap метод класса нам не подходит.

Раз мы хотим использовать std::swap, то может тогда просто специализируем эту функцию для нашего типа в скоупе std? Давайте попробуем.

namespace std
{
template <>
void swap(my_type& one, my_type& two)
{
one.swap(two);
}
}


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

Попробуем определение свободной функции swap в неймспейсе класса

namespace my_ns {

struct my_type
{
void swap(my_type&) { / swap members */ }
};

void swap( my_type<T> & lhs, my_type<T> & rhs ) noexcept
{
lhs.swap(rhs);
}

}


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

Сделаем эту свободную функцию дружественной нашему классу! Тогда можно выкинуть ненужный метод и оставить просто функцию.

struct my_type
{
friend void swap(my_type& first, my_type& second) noexcept {
// swap
}
};


Так-то лучше. Все работает и выглядит культурно. Далее поговорим про то, что должно быть внутри функции.

Be nice. Stay cool.

#template #cppcore #cpp20