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

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

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

С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "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
ParamType - cv-квалифицированный параметр
#новичкам

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

Рассмотрим вариант, когда ParamType - cv-квалифицированная ссылка. Тогда для вывода типа верхний уровень константности аргумента остается только в типе параметра функции, а шаблонный тип выводится без этой константности на верхнем уровне. Любую ссылочность типа аргумента функции мы игнорируем.

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

int x = 42;
const int const_x = x;
const int& const_ref_x = x;
std::list<double> lst;

func(x); // T is int, ParamType is const int&
func(const_x); // T is int, ParamType is const int&
func(const_ref_x); // T is int, ParamType is const int&
func(lst); // T is std::list<double>, ParamType is const std::list<double>&
___________________________
template <class T>
void func1(const std::shared_ptr<T>& param) {...}
// | |
// ParamType

std::shared_ptr<double> ptr;
std::shared_ptr<const double> ptr_of_const;
const std::shared_ptr<const double>& const_ref_ptr_of_const;

func1(ptr); // T is double, param's type is const std::shared_ptr<double>&
func1(ptr_of_const); // T is const double, param's type is const std::shared_ptr<const double>&
func1(const_ref_ptr_of_const); // T is const double, param's type is const std::shared_ptr<const double>&


В случае func тип param всегда будет константной ссылкой, вопрос только на что. И это что-то и будет искомым шаблонным типом. И получается он путем отбрасывания константности и ссылочности от типа аргумента функции. Для const_x откидываем константность, для const_ref_x - и константность и ссылочность. Для x и lst типы выводятся без изменений.

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

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

Когда в статьях и книжках расписывают этот вариант, то очень часто отсылаются к такой форме параметра функции const T * param. И говорят, что в этом случае вывод типа шаблонного параметра мало отличается от случая cv-квалифицированных ссылок. И это действительно правда. С одним уточнением, что это указатель на константу, а не константный указатель.

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

template <class T>
void func(const T * 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 const int*
func(p_const_x); // T is int, param's type is const int*
func(const_p_x); // T is int, param's type is const 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 * const *


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

Теперь рассмотрим настоящие константные указатели.
​​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
​​Фактор загрузки std:unordered_map
#новичкам

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

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

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

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

У вектора есть поле - capacity, которое говорит о том, сколько элементов может вмещать внутренний буффер.

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

float load_factor() const;


Этот метод мапы возвращает ее фактор загрузки, который равен среднему числу элементов в одном бакете aka size() / bucket_count(). Эта та характеристика, которая определяет, когда мапа будет расширяться. Точнее не только она. Нужно же еще пороговое значение, при достижении которого произойдет расширение. А вот и оно.

float max_load_factor() const;
void max_load_factor( float ml );


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

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

Stay balanced. Stay cool.

#STL #cppcore
​​Виртуальный конструктор
#новичкам

Виртуальные методы - это основа полиморфизма, одного из важнейших концептов объектно-ориентированного программирования, который позволяет нам реализовывать сложные конструкции из классов и строить гибкую-расширяемую архитектуру. Но вот если на секунду задуматься: конструктор - это ведь тоже метод. Можем ли мы сделать конструктор класса виртуальным?

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

Ответ: нет, не можем. Логика тут довольно простая. Виртуальные функции подразумевают собой позднее связывание объекта и вызываемого метода в рантайме. То есть для них нужны объекты(точнее vptr, которых находится внутри них). А объекты создаются в рантайме. И для создания объектов нужны констукторы. Получается, если бы конструкторы были виртуальными, то собака постоянно гналась бы укусить себя за жёпу получился бы цикл и парадокс(фанатам Шарифова посвящается). Нет указателя на виртуальную таблицу - нет виртуальности.

Если более формально и официально, то вот комментарий самого Бъерна по этому вопросу:

A virtual call is a mechanism to get 
work done given partial information.
In particular, "virtual" allows us to
call a function knowing only an interfaces
and not the exact type of the object.
To create an object you need complete
information. In particular, you need to
know the exact type of what you want to
create. Consequently, a
"call to a constructor" cannot be virtual.


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

Однако, нам по сути этого и не нужно. Нам нужен механизм создания объекта, зависящий от типа полиморфного объекта. И у нас такой механизм есть! Называется он фабричным методом. В ту же степь идет и паттерн "метод clone()".

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

Метод clone позволяет создавать объекты именно того класса, который на самом деле лежит под данным указателем или ссылкой.

Выглядит это так:

class Shape {
public:
virtual ~Shape() { } // A virtual destructor
// ...
virtual std::unique_ptr<Shape> clone() const = 0; // Uses the copy constructor
virtual std::unique_ptr<Shape> create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
std::unique_ptr<Shape> clone() const override;
std::unique_ptr<Shape> create() const override;
// ...
};
std::unique_ptr<Shape> Circle::clone() const { return new Circle(*this); }
std::unique_ptr<Shape> Circle::create() const { return new Circle(); }


У класса Фигура есть метод clone, который позволяет скопировать текущий объект в новый объект. Метод create позволяет дефолтно создать объект того же класса.

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

std::unique_ptr<Shape> ptr = std::make_unique<Circle>();

auto new_obj = ptr->create();

auto copy_obj = ptr->copy();


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

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

Use well-known tools for your task. Stay cool.

#interview #cppcore #pattern
​​Зачем может понадобиться делать деструктор приватным?
#новичкам

Недавно мы узнали, что можно делать деструктор приватным. Те, кто не слышал о этой технике, скорее всего подумали: "Сомнительно........но.....окэй". Потому что с первого взгляда не очень понятно, для чего вообще нужно заниматься этими непристойностями in the first place. Чтобы запретить пользователю создавать объекты на стеке и в статической области?

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

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

int MyRefCountedObject::Release() 
{
_refCount--;
if ( 0 == _refCount )
{
delete this;
return 0;
}
return _refCount;
}


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

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

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

Также приватные конструктор и деструктор могут идти в комплекте с двумя статическими методами: Create() и Destroy().

Например, из Create возвращать умный указатель и указывать в качестве его делитера приватный статический метод Destroy. Таким образом вы можете реализовать довольно сложную логику создания и удаления объекта. Когда, например, у вас есть конструктор и метод init, то для ограничения работы с созданным, но не инициализированным объектом, используется статический метод Create. По аналогии для деструктора и finalize мы используем Destroy. Мы никого не ограничиваем в правильном создании и удалении объектов. Просто хотим, чтобы только через эти два метода проходило создание и удаление объектов.

class ControlLifeCycle
{
public:
static std::unique_ptr<ControlLifeCycle, void()(ControlLifeCycle)> Create() // Factory
{
auto deleter = [](ControlLifeCycle * obj)
{
ControlLifeCycle::Destroy(obj);
};
std::cout << "I was born!" << std::endl;
return std::unique_ptr<ControlLifeCycle, decltype(deleter)>(new ControlLifeCycle, deleter);
}
private:
static void Destroy(ControlLifeCycle* ptr)
{
std::cout << "I've been destroyed!" << std::endl;
delete ptr;
}
ControlLifeCycle() {} // Private CTOR and DTOR
~ControlLifeCycle() {}
};

ControlLifeCycle global_var; // error: ctor and dtor are private

int main ()
{
ControlLifeCycle stack_var; // error: ctor and dtor are private
ControlLifeCycle* dynamic_var = new ControlLifeCycle; // error: private ctor
auto smart_var = ControlLifeCycle::Create(); // OK
}


Можно из метода Create возвращать просто указатель и оставить метод Destroy публичным. Тогда только мы своими очумелыми ручками сможем напечатать в нужном месте кода "Class::Destroy(ptr);" и только тогда произойдет освобождение ресурсов и ни в каком другом случае.

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

Control cycle of your life. Stay cool.
​​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 часа утра вызовется вторая перегрузка. Если как 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
Please open Telegram to view this post
VIEW IN TELEGRAM
Опасности автоматического вывода типов
#новичкам

C++17 дал нам замечательную фичу CTAD. Это автоматический вывод шаблонных параметров класса по инициализатору.

Теперь, если вы хотите создать например пару строки и числа, то вместо этого:

std::pair<std::string, int> pair{"Hello there!", 1};


Можно писать так:
std::pair pair{"Hello there!", 1};


Удобно? Безусловно! Только вот один вопросик есть.

Что будет, если я попытаюсь достать размер строки?

size_t size = pair.first.size();


А будет ошибка

error: request for member 'size' in 'a.std::pair<const char*, int>::first', 
which is of non-class type 'const char*'


Пара-то на самом деле не из строки и числа, а из указателя и числа. Но это и правильно. Компилятор не умеет читать мысли, а четко работает с тем, что ему предоставили. В данном случае "Hello there!" действительно преобразуется в тип const char*. Если вы хотите std::string, то нужно явно показать это компилятору:

std::pair pair{std::string("Hello there!"), 1};


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

Поэтому при использовании CTAD нужно тщательно следить за типами аргументов. Классы с не explicit конструкторами могут наделать большую невкусную кучу беспокойства.

Кстати, знаю адептов строгой типизации, которые даже auto не признают. А как вы относитесь к автоматическому выводу типов? Жду ваши мысли в комментах)

Be careful. Stay cool.

#cppcore #cpp17
Материалы для обучения
#новичкам

В этом посте вы очень хорошо постарались и накидали много ресурсов. Сейчас мы их немного систематизируем.

Начнем с самого популярного запроса. Книги. Пдфки будут в комментах.

База:

Бьерн Страуструп. "Программирование: принципы и практика использования C++".

Стивен Прата. «Язык программирования C++»

Стенли Липпман. "Язык программирования C++. Базовый курс"

Эндрю Кениг. "Эффективное программирование на С++"

Брайан Керниган. «Язык программирования С»


Немножко компьютер сайенса:

Бхаргава Адитья. "Грокаем алгоритмы".

Кирилл Бобров. "Грокаем конкурентность".


Книжки по продвинутому С++. Накладываются уже на адекватные знания языка и навыки написания кода.

Скотт Майерс. "Эффективный и современный С++"

Бартоломей Филипек. "С++17 в деталях".

Энтони Уильямс. «С++. Практика многопоточного программирования»

Пикус Ф. «Идиомы и паттерны проектирования С++».

Можно еще вот сюда заглянуть. Там еще больше полезных книжек.



Курсы:

Пояса от Яндекса. Платный.

"Добрый, добрый ОПП С++" на Stepik. Совсем недорогой.

1 и 2 части курса программирования на C++ от Computer Science Center на платформе Stepik. Из всех курсов, которые я изучал, это лучший в рунете имхо.

Программирование на языке C++ на Stepik. Бесплатный.

Программирование на языке C++ (продолжение) на Stepik. Бесплатный.


Введение в программирование (C++)курс Яндекса на Stepik. Бесплатный

Базовый курс С++ от Хэкслет. Бесплатный

Бесплатный курс от Яндекса

C++ Tutorial . Бесплатно

Яндекс Практикум «Разработчик С++». Платно.


Ютуб:

Константин Владимиров обо всем

Илья Мещерин С++

Роман Липовский. Конкурентность. Лекции и семинары

TheCherno. Нужен английский.

Simple Code


Интернет ресурсы:

https://ravesli.com/uroki-cpp. Нужен впн

https://www.learncpp.com

https://metanit.com/cpp/tutorial

https://leetcode.com - решение алгоритмических задачек

Теперь отсебятина

У всех разная подходящая модель обучения. Не концентрируйтесь только на книгах или курсах. У всего есть свои плюсы. Надо попробовать все и найти подходящий ВАМ формат обучения. Но нужны какие-то начальные рекомендации. Я бы начал с одной из базовых книг и обязательно после каждой главы решал бы задачки(это самое главное, иначе не запомнится). "Чтобы научиться программированию, необходимо писать программы" - Брайан Керниган. Поэтому чуть освоившись с языком я бы пошел на какие-нибудь курсы из списка и просто начал бы писать код. Пройдите 3-4 из них и вы уже будете довольно хороши.

Дальше уже можете на эту базу наваливать и лекции, и специфику, и прочее.

Не обязательно делать именно так. Обучайтесь так, чтобы вам было интересно. Читайте те книги, которые вам по кайфу читать. Не любите читать книги? Пользуйтесь интернет ресурсами и ютуб уроками.

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

Заслуженно помещаем этот пост в закреп. Теперь можно отправлять всех на него.

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

Upgrade yourself. Stay cool.

#digest
Квиз
#новичкам

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

У меня к вам всего один вопрос. Что будет в результате попытки компиляции и запуска этого кода?

#include <iostream>

int id;

int main()
{
std::cout << id;
}