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

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

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

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

Вернемся к примерам из вот этого поста(продублирую его в прикрепленной картинке к этому посту), но только уберем constexpr, чтобы компилятор не просто вставлял значение переменной в место ее использования, а прям создал эту переменную в секции .data, чтобы ее можно было видеть. Ну и пометим их static, чтобы на нас линкер не ругался. Да, это глобальная переменная, так нельзя делать, и ля ля ля. Но пример учебный, просто для понимания.

Как будет выглядеть переменная light_speed в единице трансляции, соответствующей файлу first.cpp?

.data
.align 4
.type _ZN9constantsL11light_speedE, @object
.size _ZN9constantsL11light_speedE, 4
_ZN9constantsL11light_speedE:
.long 299792458


Рассмотрим по порядку, что здесь происходит. Начинается сегмент данных, которые выровнены на 4 байта. Говорим, что наш символ _ZN9constantsL11light_speedE - это объект с размером 4 байта. И определяем этом символ, говорим, что он типа long со значением 299792458.

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

0000000000004010 d _ZN9constantsL11light_speedE
0000000000004020 d _ZN9constantsL11light_speedE

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

Теперь, что будет, если мы static заменим на inline.
.weak    _ZN9constants11light_speedE
.section .data._ZN9constants11light_speedE,"awG",@progbits,_ZN9constants11light_speedE,comdat
.align 4
.type _ZN9constants11light_speedE, @gnu_unique_object
.size _ZN9constants11light_speedE, 4
_ZN9constants11light_speedE:
.long 299792458


Здесь определяется слабый символ _ZN9constants11light_speedE. Слабый символ может быть переписан другим определением. Дальше идет секция данных и очень много страшных букв, но нам важно только последнее слово "comdat". Оно значит: "Здарова братишка, компоновщик! Не в службу, а в дружбу, не конкатенируй определения для символа _ZN9constants11light_speedE, а просто выбери из них всех одно и вставь в финальный бинарь. Мое увожение!". Это и есть тот маркер, по которому линкер определяет inline сущности. Ну и это все идет с компании с типом @gnu_unique_object, который должен быть уникальным во всех программе и это предотвращает дупликацию кода.

Тогда в бинарнике будет только одна запись на эту переменную.

0000000000004018 u _ZN9constants11light_speedE

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

Кстати, можно заметить пару деталей. В первом случае символ имел имя _ZN9constantsL11light_speedE, а во втором случае _ZN9constants11light_speedE.

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

Но можно заметить и разницу. После имени неймспейса в случае статических переменных мы имеем заглавную L. Так компилятор помечает символ с внутренним связыванием.

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

Stay hardcore. Stay cool.

#cppcore #hardcore #cpp17 #compiler
anonymous namespace vs static

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

1️⃣ static имеет очень много применений. Я бы сказал слишком много. Он и к функциям применим, и к переменным, и к методам, и к полям, и к локальным переменным. А еще он может бабушку через дорогу перевести и принять роды в ванной. Многофункциональный персонаж. Довольно сложно по-началу разобраться во всех тонкостях каждой стороны этой многогранной медали.

А вот unnamed namespace имеют одно применение - скрывают все свое содержимое от лишних глаз соседних юнитов трансляции. И все. Очень просто и понятно. И запомнить можно сразу.

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

2️⃣ В анонимных пространствах можно хранить все, что угодно! static хоть и применяется в куче ситуаций, он также не может быть применен в другой куче ситуаций. Например, к классам, енамам или даже другим неймспейсам. Если вы хотите полностью скрыть класс от внешних глаз, то его можно конечно определить внутри другого класса, но это не всегда подходящий вариант. А вот unnamed namespace с этим справляется очень хорошо. Это просто еще один дополнительный механизм, который позволит вам усилить безопасность кода и защитить от коллизий имен классов(нарушения ODR).

3️⃣ Не очень удобно каждую функцию, переменную или класс оборачивать в anonymous namespace, поэтому хотелось бы вынести все такие сущности в общий безымянный скоуп. Но тогда возникает проблема. При больших объемах кода внутри пространства начинаешь уже забывать, что смотришь на сущности с внутренним связыванием. Это заставляет больше информации держать в голове, что программисты делать очень не любят. Оперативка и так переполнена.

4️⃣ Вы не можете снаружи специализировать шаблон, объявленный внутри анонимного неймсейса. Об этом мы говорили в посте про inline namespace. Здесь такая же логика работает. И ADL тоже будет сложно.

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

Например

template <int const& Size>
class test {};

static int Size1 = 10;

namespace {
int Size2 = 10;
}

test<Size1> t1; // ERROR!!!
test<Size2> t2;


В этом примере не получится создать t1 по причинам описанным выше. А вот с t2 все хорошо, потому что Size2 имеет внешнее связывание(изначально внешнее, но из-за того, что никто не знает скрытого названия этого namespace'а, получается эффект внутреннего связывания). В прошлом посте мы об этом говорили. Почему я сказал, что был такой прикол? Начиная с С++17 мой gcc компилит этот пример полностью, поэтому проблему с невозможностью инстанцирования шаблонов с локально связными объектами пофиксили.

Я точно за использование anonymous namespace'ов при определении каких-то глобальных переменных. Они обычно компактные, их немного и все вместятся на экран внутри скоупа неймспейса. Это удобно читать и не надо везде приписывать static.

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

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

В целом, это все, что смог выдумать. Будут еще примеры или мысли - пишите, обсудим в комментах)

Use proper tools. Stay cool.

#cppcore #cpp17 #design
Anonymous namespace в хэдэрах

Безымянные пространства имен звучат, как хорошая альтернатива static. Была даже тема, что в С++11 задеприкейтили static для свободных функций и переменных в угоду замены на anonymous namespace. Однако от этой идеи отказались, чтобы сильно с сишечкой не расходиться.

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

Открываем С++ Core Guidelines и видим, что там написано "не используйте анонимные пространства имен в заголовочных файлах". Эх, а так хотелось...

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

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

2️⃣ Вы рискуете нарваться на неопределенное поведение. Рассмотрим такой пример:

namespace {
double const pi = 3.14159;
}

inline double twoPiR( double r ) { return 2.0 * pi * r; }


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

Конечно, даже без анонимного пространства имен это было бы неопределенное поведение здесь (поскольку const означает внутреннюю линковку по умолчанию), но основной принцип сохраняется. Любое использование в заголовке чего-либо в неназванном пространстве имен (или любого объекта const, определенного в заголовке), скорее всего, вызовет неопределенное поведение. Является ли это реальной проблемой или нет, зависит, от того, как будет обработана переменная pi. Скорее всего в этом конкретном случае компилятор просто вставит значение 3.14159 в место использования. И тогда никакой зависимости от других единиц трансляции не будет. Но для более сложных объектов не стоит надеятся на такие оптимизации.

3️⃣ Внутри одной единицы трансляции все анонимные неймспейсы на самом деле являются одним пространством с уникальным для юнита именем. Это значит, если в двух заголовочниках у вас будут такие строчки:

namespace {
int x;
}


и вы подключите их в один исходник, то произойдет нарушение ODR. У вас будут два определения одной и той же сущности внутри одного пространства имен.

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

До С++17 у нас особо не было универсального и удобного способа распространять код только через хэдэра. Теперь же есть inline переменные. И мы можем определять внешнесвязные сущности в хэдэрах. Это полностью решило все перечисленные здесь проблемы.

Поэтому используйте inline функции и переменные в хэдэрах и радуйтесь жизни!

Enjoy life. Stay cool.

#cpp11 #cpp17 #compiler #cppcore
В чем проблема auto_ptr

В стародавние времена, когда еще мамонты ходили по земле, был выпущен стандарт С++98, в стандартной библиотеке которого был один интересный умный указатель std::auto_ptr. Этот шаблон был порождением страхов и мучений. Ибо еще со времен, когда даже динозавры существовали, этим динозаврам было сложно управлять обычными сырыми указателями. И всегда было стремление обезопасить работу с ними в С++. И вот настал тот момент, когда появилось первое стандартное средство безопасного управления памятью. Однако первый блин, как это часто бывает, получился комом...

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

Что мы имеет в С++98/03. Мы имеем 2 специальных метода, которые помогают перенимать характеристики другого объекта. Это копирующий конструктор и копирующий оператор присваивания. На этом все. Как можно на этих двух сущностях имплементировать передачу владения?

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

То есть. Копирующий конструктор и оператор присваивания std::auto_ptr изменяют объект, который в них передается. При чем ладно изменяет. Они делают его пустышкой. Таким образом им больше нельзя пользоваться, так как ресурса там больше нет. Такое контринтуитивное поведение и является основной проблемой auto_ptr.

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

Конечно, это все было из-за отсутствия move-семантики. Язык просто был недостаточно мощным, чтобы реализовать семантику владения.

Однако в С++11 проблема была решена. Введена мув-семантика и std::unique_ptr, в котором реализовали идею авто указателя. При небольшом рефакторинге и замене std::auto_ptr на unique_ptr проблем больше не было. И потребность в использовании бустовых аналогов тоже отпала.

И комитет решил на радостях задеприкейтить auto_ptr, а затем и вовсе удалил его в С++17 за ненабностью.

Fix your mistakes. Stay cool.

#inteview #cpp11 #cpp17
Исправляем косяк std::shared_ptr с массивами

Ну не мы сами, конечно. Стандарт С++17 исправляет этот момент.

Что мы теперь имеем.

Для создания объекта таким конструктором:

template< class T >   
explicit shared_ptr( T* ptr );


используется делитер delete ptr, если T - не массив, и delete[] ptr если Т -массив.

Также теперь изменился тип хранимого объекта element_type. Раньше был просто шаблонный тип Т, теперь же это

using element_type = remove_extent_t<T>;


std::remove_extent - это такой type_trait. Все, что нужно о нем знать - если Т - массив, то тип element_type будет совпадать с типом элементов массива.

Теперь мы даже можем использовать operator[] для доступа к элементам массива. Делается это так:

std::shared_ptr<int[]> num(new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
for (std::size_t i = 0; i < 10; ++i)
std::cout << num[i] << ' ';


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

Но вот вопрос: а нафига это вообще надо? Когда кто-то вообще в последний раз использовал динамический массив?

Мы же вроде на плюсах пишем. Есть плюсовые решения - std::vector, если размер не известен на момент компиляции, и std::array, если известен. У них и интерфейс удобный и унифицированный и все-таки это объектно-ориентированный подход. И сердцу тепло, и глаз радуется. Динамические массивы выглядят, как окаменелые какашки динозавров.

C std::array соглашусь. Думаю, что нет адекватных оправданий использования динамических и статических массивов, длина которых известна в compile-time. std::array - очень простая и тонкая обертка над статическим массивом и ее использование вырождается компилятором до использования массива.

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

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

Fix your flaws. Stay cool.

#cpp17 #memory
Объединения условий в enable_if

Иногда мы хотим сильно ограничить свойства типов, с которыми мы хотим инстанцировать шаблон. Например, тип должен быть default-constructed и иметь оператор сравнения. У нас есть для этого метафункция std::enable_if, которая позволяет нам проверять наличие свойств у типов. Но вот незадача, как проверить два условия одновременно? Я хочу и то, и то.

Ранее для этого использовались обычные операторы && и || между тайптрейтами.

Однако в С++17 появились специальные метаклассы, которые позволяют комбинировать условия. Это

• template<class... B> struct conjunction; - логическое И
• template<class... B> struct disjunction; - логическое ИЛИ
• template<class B> struct negation; - логичесткое НЕ

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

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

template<typename... Ts>
std::enable_if_t<std::conjunction_v<std::is_same<int, Ts>...>> PrintIntegers(Ts ... args)
{
(std::cout << ... << args) << 'n';
}


В функцию PrintIntegers мы можем передать сколько угодно(почти) аргументов и все они будут проверяться на соответствие целочисленному типу

Или например вы хотите принтовать только числа? Тогда мы идем к вам:

template<typename T, typename = std::enable_if_t<std::disjunction_v<std::is_integral<T>, std::is_floating_point<T>>>>
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}


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

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

template<typename T, typename = std::enable_if_t<std::negation_v<std::is_pointer<T>>>> 
void PrintValue(T value)
{
std::cout << "Value: " << value << std::endl;
}


Такие вот удобные метафункции.

Create complex conditions. Stay cool.

#cpp17 #template
Подробности про std::conjunction vs &&

В этом посте я рассказывал про замечательные тайптрейты std::conjunction, std::disjunction. Они позволяют компоновать несколько трейтов в одну логическую последовательность. Там же я рассказывал про то, что до них для этих целей использовались операторы &&, ||. Безусловно, человеческим языком обозванные сущности проще воспринимаются, чем какие-то символы. Но неужели это все различия? Какая-то вялая причина, чтобы вводить в стандарт эти трейты.

И правда, различия есть. Еще какие!

Прежде, чем начать разбирать их, нужно поподробнее рассмотреть эти метаклассы, потому что оттуда все различия. Рассматривать будем на примере std::conjunction, ибо у них все очень похоже.

Примерно так этот класс может быть реализован

template<class...> struct conjunction : std::true_type
template<class B1> struct conjunction<B1> : B1 {};
template<class B1, class... Bn>
struct conjunction<B1, Bn...>
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};


Специализация std::conjunction<B1, ..., Bn> имеет публичную базу, которая варьируется в зависимости от аргументов

Если их нет, то базовым классом для std::conjunction будет std::true_type.

Если они есть, то базой будет первый тип Bi из B1, ..., Bn, для которого bool(Bi::value) == false.

Если для всех Bi bool(Bi::value) == true, тогда базой будет Bn.

Кстати std::conjunction не обязательно по итогу наследуется либо от std::true_type, либо от std::false_type: она просто наследует от первого B, для которого его ::value, явно преобразованное в bool, является ложным, или от самого последнего B, когда все они преобразуются в true. То есть это самое value может быть даже не булевым значением, а например числом. Вот так:

std::conjunction<std::integral_constant<int, 2>,std::integral_constant<int, 4>>::value == 4 - верно!


И вот в этом весь прикол. std::conjunction - это вычисление по короткой схеме! То есть как только мы нашли такой Bi, что для него bool(Bi::value) == false, компилятор прекращает дальше инстанцировать вглубь рекурсии и однозначно определяет тип базового класса, а значит и поля value.

И как раз таки в этом аспекте метаклассы conjunction и disjunction отличаются от обычных && и ||. Но об этом мы поговорим завтра.

Understand true essence of things. Stay cool.

#cpp17 #template #hardcore
std::conjunction vs &&

Вчера мы поговорили о том, что инстанцирование в std::conjunction и std::disjunction происходит по короткой схеме. Как только мы нашли шаблонный аргумент, для которого будет валидно выражение bool(Bi::value) == false, инстанциация остальных аргументов прекращается и выводится тип std::conjunction::value.

Однако при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы. Что в форме fold expression, что в явной последовательной форме. То есть

template <class T>
struct type_without_value
{
};

template <class T1, class T2>
constexpr auto numbers = (std::is_integral_v<T1> && type_without_value<T2>::value);

constexpr auto result = numbers<float, int>;


компиляция вот этого кода закончится с ошибкой error: value is not a member of type_without_value<int>. Хотя float - совсем не интергральный тип и при работающей короткой схеме вычислений, ошибки компиляции не было бы. Потому что второй тип даже не инстанцировался бы. Как в случае с std::conjunction.

template <typename T>
struct valid_except_void : std::false_type
{
};

template <>
struct valid_except_void<void>
{
};

template <class T1, class T2>
constexpr auto test = std::conjunction_v<valid_except_void<T1>, valid_except_void<T2>>;

constexpr auto result = test<float, void>;


Такой код успешно скомпилируется и result будет равен false. Потому что инстанциация первого шаблона valid_except_void<T1> завершится успешно и для него bool(value) == false. Поэтому validexceptvoid с войдом не испортит нам игру.

То есть преимущество std::conjunction в том, что как только мы нашли его аргумент, для которого bool(value) == false, то все последующие аргументы не инстанцируются. Это может быть полезно, когда последующие типы очень затратные при инстанцировании или могут вызвать фатальные ошибки, если их инстанцировать с неправильным типом.

Прошу прощение за частое употребление слова "инстанцировать". Я просто не знаю нормального аналога/синонима. Кто знает, поделитесь в комментах.

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

Always compare your tools. Stay cool.

#template #cpp17
Fold expressions. Мотивация.

Стандарт C++11 привнес в нашу жизнь замечательную фичу - variadic templates, которая является очень мощным инструментом в метапрограммировании. Она используется, когда нам необходимо написать функцию, которая принимает неопределенное количество аргументов. Ранее такой возможности в С++ не было (имею ввиду типобезопасные шаблонные функции) и приходилось отдельно специфицировать функцию в начале с одним аргументом, потом с двумя, потом с тремя и так далее, пока не надоест, не настанет обед или больше не нужно будет. Не очень удобненько.

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

auto SumCpp11() {
return 0;
}

template<typename T1, typename... T>
auto SumCpp11(T1 s, T... ts) {
return s + SumCpp11(ts...);
}

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

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

Но решение этих проблем есть!

Называется fold expression. Появилось это спасение в С++17 и позволяет писать намного более простой код. Посмотрим, как будет выглядеть прошлый пример при его использовании.

template<typename ...Args> 
auto SumCpp17(Args ...args) {
return (args + ...);
}

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

Однако есть все-таки одно ограничение. Функцию Sum не получится инстанцировать без аргументов. Это свойство оператора сложения. И об этом в том числе мы поговорим завтра, когда будем подробнее разбирать внутрянку fold expression.

Make things simplier. Stay cool.

#cpp11 #cpp17 #template
Fold expression. Подробности.

В сущности, fold expression - сворачивание всего пака шаблонных параметров с помощью комбинации синтаксиса variadic templates и бинарных операторов. Есть всего 4 формата, в которых можно использовать эту фичу.

1️⃣ ( pack op ...) - унарный правый фолд
2️⃣ ( ...  op pack) - унарный левый фолд
3️⃣ (pack op ... op init ) - бинарный правый фолд
4️⃣ (init op ... op pack) - бинарный левый фолд

где pack - выражение, содержащее нераспакованный набор шаблонных параметров. op - бинарный оператор. В последних двух случаях он должен быть одинаковым справа и слева от точек. В число бинарных операторов входит почти все, что вы могли бы себе представить: +  -    /   %  ^   &   |   =   <   >   <<   >>   +=  -=   =   /= %=   ^=   &=   |=   <<=   >>=   ==   !=   <=   >=   &&   ||   ,   .   ->. init - выражение, которое никак не относится к шаблонным параметрам и является базой вычислений. Это как в std::accumulate вы можете выставить начальное значение для аггрегации. Вот это тоже самое.

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

Очень важное уточнение по поводу левых и правых фолдов.

👉🏿 Унарный  правый фолд (E op ...) раскрывается в  (E1 op (... op (En-1 op En)))
👉🏿 Унарный левый фолд (... op E) раскрывается в  (((E1 op E2) op ...) op En)
👉🏿 Бинарный правый фолд (E op ... op init) раскрывается в  (E1 op (... op (En−1 op (EN op init))))
👉🏿 Бинарный левый фолд(init op ... op E) раскрывается в  ((((init op E1) op E2) op ...) op En)

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

А помните, я вчера упоминал функцию Sum, которую нельзя расшаблонивать с нулевым количеством аргументов(там в комментах @PyXiion придумал как, но я сейчас имею ввиду нативный формат без оберток)? Вот сейчас и коснемся этого вопроса.

Функция без аргументов - всегда был особым случаем при использовании вариадик шаблонов. И для fold expression она также является таковым. Тут следующие правила:

💥 У оператора Логическое И (&&) значение для пустого набора параметров - true.
💥 У оператора Логическое ИЛИ (||) значение для пустого набора параметров - false.
💥 У оператора "запятая" (,) значение для пустого набора параметров - void().
☠️ Для всех остальных операторов конкретизация шаблона с пустым набором параметров запрещена.

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

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

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

Explore internals of things. Stay cool.

#cpp17 #template
Принтуем с fold expression

Для начала разберем, как бы все выглядело до С++17.

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

void print() {
}

template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print(ts...);
}

print(1.7, -2, "qwe");

// Output: 1.7-2qwe


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

void print_impl() {
}

template<typename T1, typename... T>
void print_impl(T1 s, T... ts) {
std::cout << ' ' << s;
print_impl(ts...);
}

template<typename T1, typename... T>
void print(T1 s, T... ts) {
std::cout << s;
print_impl(ts...);
std::cout << std::endl;
}

print(1.7, -2, "qwe");
print("You", ", our subscribers,", "are", "the", "best!!");

// Output:
// 1.7 -2 qwe
// You , our subscribers, are the best!!


Нам мало того, что пришлось использовать базу рекурсии, так еще и прокси-функцию, которая допиливает форматирование. Слишко МНОГА БУКАВ. Ща исправим.

Вот так будет выглядеть базовый принт без форматирования на fold expression:

template<typename... Args>
void print(Args&&... args) {
(std::cout << ... << args);
}

print(1.7, -2, "qwe");

// Output: 1.7-2qwe


Уже лучше. Точнее не так. Проще не бывает уже)
Как видите, здесь я использую бинарный левый фолд. В качестве инициализатора выступает стандартный поток вывода и он слева не только потому, что так обычно принято, а потому что оператор << также применяется например для бинарного сдвига. И чтобы мы всегда именно в поток писали, нужно, чтобы слева всегда был нужный поток. Тогда будет вызываться соответствующая перегрузка для ostream'ов и каждый раз будет возвращаться ссылка на этот поток. Таким образом мы и будем продолжать писать именно в него.

Но как тут быть с форматингом? args тут просто раскроются в последовательность "arg1 << arg2 << arg3" и тд
И непонятно, как в таких условиях добавить вывод пробела, не придумывая нагромождения в виде проксей и прочего. Для решения этой проблемы надо воспользоваться двумя хаками:

1️⃣ Не обязательно использовать сырой пакет параметров. Можно использовать функцию, принимающую этот пак.
2️⃣ Применяя оператор запятую, мы можем в операндах выполнять любое выражение, даже возвращающее void.

Получается такая штука:

template<typename ...Args>
void print(Args&&... args) {
auto print_with_space = [](const auto& v) { std::cout << v << ' '; };
(print_with_space(args), ... , (std::cout << std::endl));
}

print(1.7, -2, "qwe");
print("You, "are", "the", "best!!");

// Output:
// 1.7 -2 qwe
// You are the best!!


Здесь мы за счет лямбды и запятой выполняем каждый раз отдельную операцию вывода в поток с пробелом. А затем вместо init выражения подставляем вывод конца строки.

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

Hack this life. Stay cool.

#cpp17 #template
Почему не работает?

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

Что произойдет при попытке компиляции и запуска следующего кода?

#include <type_traits>

template<typename... Args>
constexpr bool AndL(Args&&... args)
{
return (... && std::forward<decltype(args)>(args));
}

template<typename... Args>
constexpr bool AndR(Args&&... args)
{
return (std::forward<decltype(args)>(args) && ...);
}

int main()
{
bool* pb = nullptr;
false && (*pb = true);
AndR(false, (*pb = true));
AndL(false, (*pb = true));
}


Ответ не то, чтобы сильно интригующий, но все же оставлю его под спойлерами, чтобы была возможно подумать, а не сразу ответ читать.

Компиляция завершится успешно, но при выполнении будет сегфолт. На вот этой строчке "AndR(false, (*pb = true));".

Коротко разберем происходящее. Допустим я хочу проверить короткосхемность логического И во всех ипостасях. То есть в обычном виде и в виде fold expression. Для fold выражения нужны функции шаблонные. Вот я и определяю 2 штуки для правой и левой свертки. Ну и есть использование оператора в обычном виде.

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

Так вот. Свойство short circuit дает операции право не вычислять следующие аргументы после того, как ответ уже будет известен. В нашем случае, первый аргумент - false и по идее дальше вычислений быть не должно.

Так и делается во второй строчке функции main. Но вот все падает на 3-й.

Хотя казалось бы должно упасть на 4-й, потому что там так фолд раскрывается, что первым будет учтен последний аргумент. Поэтому первый false нас не защитит от разыменования нулевого указателя.


На 4-й бы тоже упало, если бы не было третьей строчки. Но не по той причине, которую я указал.

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

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


Train your brain. Stay cool.

#template #cpp17
Вычисления по короткой схеме vs ленивые вычисления Ч1

Вчера Евгений поднял тему различий short circuited evaluation и lazy evaluation. Действительно важная тема для обсуждений. Потому что многие путают. Это конечно почти ни на что не влияет, но прояснить все же нужно.

Разберемся с ленивыми вычислениями. Если описать этот термин лозунгом, то это будет что-то типа "вычисли меня, когда я буду нужен". Для плюсов это ненативный термин. Потому что это strict language. В таких языках, например, все параметры функции должны быть вычислены до входа в функцию. Однако, есть языки(Scala, Haskell), где такого условия нет и аргумент вычисляется только тогда, когда он нужен в теле функции. Это значит, что функция может выдать результат, даже не вычислив всех аргументов! То есть вычисление аргумента откладывается до того момента, когда его значение будет нужно.

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

Логгер-синглтон. Не хотим мы запариваться над сложным и безопасным логгером, поэтому напишем его очень просто и безобразно:

class Logger{
Logger() {}
public:
static Logger * instance() {
static Logger myinstance;
return &myinstance;
}
void Log(const std::string str) {
std::cout << str << std::endl;
}
};


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

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

template<typename O, typename T1, typename T2>
struct Lazy
{
Lazy(T1 const& l,T2 const& r)
:lhs(l),rhs(r) {}

typedef typename O::Result Result;
operator Result() const
{
O op;
return op(lhs,rhs);
}
private:
T1 const& lhs;
T2 const& rhs;
};

struct ZeroDimentionalTensor
{
ZeroDimentionalTensor(int n) : a{n} {}
int a;
};

using ZDTensor = ZeroDimentionalTensor;

struct ZDTensorAdd
{
using Result = ZDTensor;
Result operator()(ZDTensor const& lhs,ZDTensor const& rhs) const
{
Result r = lhs.a + rhs.a;
std::cout << "Actual calculation" << std::endl;
return r;
}
};

Lazy<ZDTensorAdd,ZDTensor,ZDTensor> operator+(ZDTensor const& lhs,ZDTensor const& rhs)
{
return Lazy<ZDTensorAdd,ZDTensor,ZDTensor>(lhs,rhs);
}

int main() {
ZDTensor a{1};
ZDTensor b{2};
std::cout << "Check" << std::endl;
auto sum = a + b;
std::cout << "Check" << std::endl;
bool external_parameter = true;
ZDTensor c{0};
if (std::cout << "Condition" << std::endl; external_parameter) {
c = sum;
} else {
c = 5;
}
std::cout << c.a << std::endl;
}


Здесь мы определяем ленивую структурку, которая может выполнять какую-то бинарную операцию над какими-то объектами. Причем делает она это в операторе преобразования к типу результата бинарной операции. Это довольно важный момент. Пример и так довольно сложный, поэтому не буду мудрить с compile-time проверками правильности типов.

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

Создаю совсем простенький класс этого тензора. А затем класс операции сложения тензоров. Это по факту функтор, в котором дополнительно хранится тип результата, и который выполняет реальное сложение тензоров при вызове оператора() с двумя тензорами. Для проверки момента реальных вычислений я вставил консольный вывод

И вишенка на торте - перегружаю оператор+ для того, чтобы он на сложение двух тензоров возвращал не тензор, а нашу ленивую структурку.

Ну и теперь магия в действии. ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ.

Differentiate the meanings of things. Stay cool.

#template #cppcore #cpp17
if constexpr

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

if constexpr - расширение обычной условной конструкции с двумя дополнениями/ограничениями.

1️⃣ Условие проверяется в compile-time. И нужная ветка выбирается тоже на этом этапе. Это значит, что выражение в условии должно быть тоже вычислимым на этапе компиляции. То есть должно быть constexpr. Это несколько ограничивает его функционал(например constexpr функции могут вычисляться и во время компиляции, и во время выполнения). Хотя учитывая, что конструкция используется в шаблонном коде, для разделения поведения в зависимости от шаблонного параметра, и других кейсов я особо не вижу, то это не совсем и ограничения.

2️⃣Если одна из веток условий выбрана, то другая ветка будет отброшена(discarded) и она не будет компилироваться!

Благодаря этим особенностям, наш пример с функцией to_str будет выглядеть вот так:

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_same_v<T, std::string>) // строка или преобразуемый в строку
return t;
else
return std::to_string(t);
}


А пример со сравнением чисел так:

template <class T>
constexpr T absolute(T arg) {
return arg < 0 ? -arg : arg;
}

template <class T>
constexpr auto precision_threshold = T(0.000001);

template <class T>
constexpr bool is_equal(T a, T b) {
if constexpr (is_floating_point_v<T>)
return absolute(a - b) < precision_threshold<T>;
else
return a == b;}


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

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_same_v<T, std::string>)
Life_without_coding();
else
return std::to_string(t);
}

auto str = to_str(5);


Получим ошибку: error: use of undeclared identifier 'Life_without_coding'. Эх. Не прожить нам без кода...

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

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

Choose the right path. Stay cool.

#cpp17 #template #compiler
Возвращаемый тип при if constexpr

Ещё одним интересным местом функционала if constexpr, является возможность возвращения совершенно разных типов из одной функции (естественно, не одновременно). То есть с использованием if constexpr мы можем иметь несколько return выражений, каждое из которых возвращает объект типа, который не конвертируется в другой. Разумеется, все такие return должны быть спрятаны в блоки кода, которые будут выбрасываться на этапе компиляции.

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

template <typename T>
auto GetStringNumber(T val)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(val);
else if constexpr(std::is_same_v<T, std::string>)
return std::stoi(val);
}

Здесь мы используем фичу С++14, благодаря которой компилятор сам выводит тип возвращаемого значения.

Очевидно, что для различных веток, функция GetStringNumber будет иметь разный тип возвращаемого значения: в первом случае std::string, а во втором int. И это работает! Правда, мы легко можем всё сломать, для этого достаточно добавить ещё один return, находящийся вне условий, который будет несовместим с двумя прочими. Если мы перестараемся со своими шаловливыми мыслями, то можем написать и что-то такое:

template <typename T>
auto GetStringNumber(T val)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(val);
else if constexpr(std::is_same_v<T, std::string>)
return std::stoi(val);
return std::vector<std::string>{"Joke has gone too far"};
}


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

Такая вот интересная особенность нашлась. Поделитесь им в комментариях практическими примерами использования if constexpr. Думаю, многим пригодится опыт наших замечательных комментаторов)

Apply things in an unusual ways. Stay cool.

#cpp17 #cpp14 #template