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

По всем вопросам (+ реклама) @ninjatelegramm

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

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

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

Оставляйте в комментах свои замечания и возможные варианты исправления ситуации.

Комментарии наиболее продуктивного критика выложу на канал вместе с ответом.

Хватит отдыхать, пора ошибки в коде искать!

Analyse your life. Stay cool.
🔥18👍64🤯3😭2🤨1
​​Результаты ревью

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

Fix your flaws. Stay cool.

#template #STL #cppcore #cpp17 #cpp20
🔥23👏113👍3
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
33👍15🔥5🤔2
Всем привет)

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

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

Темы:

👉🏿 Вывод типов
👉🏿 Template type deduction
👉🏿 Небольшой пролог для вывода типов
👉🏿 ParamType - не cv-квалифицированная ссылка
👉🏿 ParamType - не cv-квалифицированный указатель
👉🏿 ParamType - cv-квалифицированный параметр
👉🏿 ParamType - универсальная ссылка
👉🏿 ParamType - не ссылка и не указатель
👉🏿 Аргумент шаблонной функции - массив
👉🏿 Аргумент шаблонной функции - функция

Возможно где-то вы уже это видели(привет, Скотт!). Однако оттуда мы взяли структуру, наполнение будет переформатировно и расширено.
20👍8🔥5😢2
Вывод типов
#новичкам

С++ - статически типизированный язык, что значит, что типы всех объектов должны быть известны на этапе компиляции. Это хорошо для безопасности программы и предсказуемости поведения, но не очень хорошо с точки зрения удобства написания программы. Не всегда мне хочется писать что-то типа "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
🔥31👍1951
​​Опасности std::unordered_map
#опытным

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

В копилку полезности auto.

Вдруг вы решили не пользоваться этой фичей и пишите вот так:

struct Customer{
Customer(int num) : data{num} {}
Customer(const Customer& other) {
data = other.data;
std::cout << "Copy ctor" << std::endl;
}
private:
int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const std::pair<std::string, std::vector<Customer>>& item : data) {
std::cout << "Idle print" << std::endl;
}


Вроде бы все хорошо и выглядит, как надо. И ожидать мы в консоли будем такой вывод:

Copy ctor
Copy ctor
Idle print


При заполнении вектора кастомеры копируются из временных объектов, вызывается копирующий конструктор с принтом, и далее вывод цикла.

Однако на самом деле вывод будет такой:

Copy ctor
Copy ctor
Copy ctor
Copy ctor
Idle print


Мы этого совсем не ожидали. Откуда еще 2 копии?!!

Дело в том, что в нашей неупорядоченной мапе хранятся не std::pair<std::string, std::vector<Customer>>, а std::pair<const std::string, std::vector<Customer>>. Это в принципе особенность std::unordered_map: ключ мапы - неизменяемый объект, поэтому обобщенно мапа хранит std::pair<const Key, Value>.

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

Ну и естественно, эта проблема просто решается использованием ключевого слова auto.

struct Customer{
Customer(int num) : data{num} {}
Customer(const Customer& other) {
data = other.data;
std::cout << "Copy ctor" << std::endl;
}
private:
int data;
};
std::unordered_map<std::string, std::vector<Customer>> data;
data["qwe"] = {Customer{1}, Customer{2}};
for (const auto& item : data) {
std::cout << "Idle print" << std::endl;
}


Теперь у нас есть ожидаемый вывод.

Make your life easier. Stay cool.

#cpp11 #STL
🔥29👍165
​​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
👍21🔥75
​​Небольшой пролог для вывода типов
#новичкам

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

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

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

Коротко напомню контекст. 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
👍4326🔥8😁2
​​Как посмотреть шаблонный тип
#новичкам

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

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

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

Для шланга и гцц этот макрос называется __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
19👍11🔥3
Квиз

Сегодня будет довольно противоречивый #quiz. Ничего не буду говорить. Просто задам вопрос. А подробный ответ будет вечером.

Какой результат попытки компиляции и выполнения этого кода:

#include <algorithm>
#include <iostream>

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

int main() {
std::cout << std::max(0, foo::qwerty) << std::endl;
return 0;
}
🔥123👍3👎2
Ответ

Несмотря на всю простоту и краткость кода, он не запустится! Будет ошибка линковки undefined reference to foo::qwerty.

Как такое возможно, если мы четко определили статическое поле qwerty?

Обратимся к стандарту:

If a non-volatile non-inline const static data member is of integral 
or enumeration type, its declaration in the class definition can specify
a brace-or-equal-initializer in which every initializer-clause that is an 
assignment-expression is a constant expression.

The member shall still be defined in a namespace scope if it is odr-used
in the program and the namespace scope definition shall not contain an initializer


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

И этот маркет не является определением! Оно все равно нужно, так как foo::qwerty odr-использована. Один из признаков того, что переменная odr-используется - на нее ссылается ссылка.

Так вот функция std::max принимает константные ссылки на объекты. А передаем мы в нее lvalue. Значит у этого lvalue должен быть существующий адрес, чтобы нормально забиндится на ссылку.

Вот и получается порочная цепочка: чтобы забиндится на ссылку нам нужен адрес -> адрес глобальная переменная получает только если у нее есть определение -> так как мы используем ссылку на статическое поле класса нам нужно его определение -> мы его не предоставили -> получили по рукам от линкера.

Чтобы избежать ошибки можно использовать 3 стратегии:

👉🏿 Честно предоставить определение, согласно стандарту, без инициализатора.

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


👉🏿 Использовать ключевое слово inline, которое, начиная с С++17, позволяет определять поля внутри описания класса.

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


👉🏿 Вместо const использовать ключевое слово constexpr. Это фактически сразу делает qwerty компайл тайм константой, которая должна быть инициализирована при объявлении и к которой спокойно можно обращаться в том числе и по ссылке.

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


Вот такое тонкое место есть в С++)

Surprise everyone. Stay cool.
👍32❤‍🔥116🔥61
Один плюс решает все

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

#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
🔥33👍86
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
👍19🔥62🤯2
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
👍13🔥73❤‍🔥2
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 будет неконстантным указателем. Здесь как бы создается новый указатель, являющийся копией старого. Он не обязан сам быть константным, так как оригинальный поинтер остается неизменным.

Теперь рассмотрим настоящие константные указатели.
👍11🔥43👎1
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
👍95❤‍🔥4🔥4👎1🤯1
​​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
👍10🔥82
Квизы

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

Не будет драконьих конструкций, только все то, что мы уже знаем и разбирали на канале.

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

У меня к вам всего один вопрос.

Во что выведется тип Т?

#quiz
8👍2🔥2