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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
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