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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Что выведется в консоль в этом случае?
Anonymous Poll
39%
1
9%
0
21%
-1
10%
2^32 - 1
15%
Мне-то откуда знать???
5%
Hello, World!!!
Ответ на вопрос выше не так уж и прост на самом деле. Начнем с того, что когда вы запустите этот код на своей машине, то получите ответ: -1.

Эм. Неожиданно! "Как так получается?" - спросите вы меня. "Ведь я же знаю, как работает бинарный сдвиг: берем да и сдвигаем биты вправо и позади оставляем нули. В итоге получатся все нолики и самый младший бит 1. А это 1, а не -1!".

Логика железная и с ней не поспоришь. Однако в этой цепочке есть одно неверное утверждение. На самом деле вы не знаете, как работает правый бинарный сдвиг!(ну те, кто так рассуждают)

Заинтригованы?

Тогда мы идем к вам!

Что было до С++20.

Рассуждения верные только для беззнаковых чисел. Для знаковых все определяется конкретной реализацией. А у нас как раз такой вариант. Так еще все от стандарта зависит. Так что правильный ответ: "Мне-то откуда знать???". В общем случае вы вряд ли знаете, как во всех компиляторах это реализовано, а спрашивал я без привязки к конкретной реализации и стандарту. Да, вот так завуалировал ответ. Имею право.

Почему я тогда утверждаю, что вы на своих машинах получите -1?

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

Что это за акула такая.

Когда вы делаете правый сдвиг для беззнаковых чисел, то просто старшие разряды заполняете нулями. Арифметический же сдвиг заполняет старшие разряды не нулями, а знаковым битом. Таким образом, правый сдвиг любого 4-х байтного знакового числа оставит после себя либо 32 бита нулей (в случае положительного числа), либо 32 бита единичек(в случае отрицательного числа). А все единички в битах - это -1 для знаковых чисел.

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

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

Арифметический сдвиг - хоть теперь и стандартное поведение, но

Во-первых, не все разрабы имеют достаточного опыта на 20-х плюсах, чтобы понять, что это стандартное поведение

Во-вторых, в каких-то проектах(которых на самом деле огромное количество) до сих пор не используется этот стандарт

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

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

Stay surprised. Stay cool.

#cpp20 #compiler
У нас большой праздник!!!

Вчера на канале случился юбилей - к нам подписался наш тысячный подписчик и соратник!!! Ура!!! Ура!!! Ураааааа!!!

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

Поэтому хочу сказать Вам всем: Спасибо огромное!!!!

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

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

Мысли по созданию канала были уже давненько, но решение о его создании было принято нами в знаменитом самом маленьком стейкхаусе в мире «Steak Me Truck» в Нижнем Новгороде. Это был конец сентября того года.

Сегодня, чтобы отметить наш юбилей, мы пришли в то же самое место и съели по крутому мягкому стейку! Теперь будем считать это тотемным местом нашего канала. Так что тем, кто живет в Нижнем и туристам из других городов, рекомендуем посетить это, не побоюсь этой фразы, на весь мир известное место. А какой там томатный сок с халапеньо…. Ммммм…. Только ради него и стоит идти, не говоря уже про сами стейки.

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

Enjoy your achievements. Stay cool.
Способы узнать знак целого числа

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

И вот мне стало интересно. Сколько способов есть, чтобы узнать является ли число отрицательным? Давайте же узнаем! Я вот что надумал:

💥 Как в прошлом посте сдвинуть число вправо на 31/63 бита и привести все к инту. Если получился 0 - число положительное. Если 1 - отрицательное.

💥 bool is_signed = number < 0. Один из самых очевидных и прямолинейных подходов. Просто проверяем число оператором меньше и все на этом. Скучно, попсово, но зато понятно и эффективно.

💥 Использовать битовую маску. bool is_signed = number & 0x80000000. Здесь мы оставляем только знаковый бит на его месте. Затем приводим число к булевому значению. Положительное число превратится в нолик, а значит в true, а отрицательное - в false. Размер маски естественно меняется в зависимости от типа знакового числа.

💥 std::signbit(number). Эта шаблонная функция вернет вам true, если number - отрицательное, и false в обратном случае. На мой взгляд, это больше по плюсовому и функция имеет человеческое название, поэтому читаться такой код будет намного проще, чем в предыдущих случаях.

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

Generate a dozens of different solutions. Stay cool.

#fun #cppcore
std::signbit

В прошлом посте мы уже упоминали std::signbit. Сегодня мы посмотрим на эту сущность по-подробнее.

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

bool signbit( float num );  
bool signbit( double num );
bool signbit( long double num );


вот такие перегрузки мы имеем для floating-point чисел. А вот такую:

template< class Integer >  
bool signbit( Integer num );


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

В чем особенность целочисленной перегрузки. В том, что число, которое туда попадает трактуется, как double. Поэтому выражение std::signbit(num) эквивалентно std::signbit(static_cast<double>(num)).

Также эта функция детектирует наличие знакового бита у нулей, бесконечностей и NaN'ов. Да, да. У нуля есть знак. Так что 0.0 и -0.0 - не одно и то же. И если вы внимательные, то заметили даже у NaN есть знак. И std::signbit - один из двух возможных кроссфплатформенных способов узнать знак NaN. Этот факт еще больше мотивирует использовать эту функцию(в ситуациях, где это свойство решает).

Начиная с 23 стандарта функция становится constexpr, что не может не радовать любителей compile-time вычислений.

Для языка С тоже кстати есть похожая сущность. Только там это макрос

#define signbit( arg ) /* implementation defined */


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

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

Look for signs in life. Stay cool.

#cpp23 #cpp11 #goodoldc
Бинарные логические операторы. Short circuit.

Хотел прояснить один момент. В этом посте я сказал, что при инстанциации шаблонов операторы && и || не могут похвастаться наличием короткой схемы, в том числе в виде fold expression. Я имел ввиду, что вам придется конкретизировать все метаклассы(ну не прям вам своими ручками, но тем не менее) для того, чтобы начать вычислять выражение. Это правда, без сомнений.

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

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

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>;


вот этот пример крашнется на компиляции, потому что компилятору нужно инстанцировать type_without_value<T2> и достать из него value, но он не сможет этого сделать, потому что type_without_value не содержит члена value. Но в случае удачной инстанциации этот пример:

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

constexpr auto result = numbers<float, int>;


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

Это можно продемонстрировать на следующем примере. Вот такой код успешно соберется и выполнится:

constexpr bool is_even(int value) noexcept {
return (value % 2 == 0) ? true : throw std::logic_error("Don't throw me around, you bastard!");
}

template <class T>
constexpr auto numbers = (std::is_integral_v<T> && is_even(1));

constexpr auto result = numbers<float>;
// constexpr bool fail = is_even(1);


Как видите, при выполнении функции is_even, если в нее попадет нечетное число, то бросится исключение. Исключение, брошенное в compile-time, прерывает компиляцию.

Но в крайнем примере все пройдет хорошо, потому что is_even ни разу не выполнится, как раз из-за короткосхемности оператора &&!

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

Вот такие дела. Надеюсь, я не зря волновался и для кого-то получше прояснил ситуацию.

Explain things clearly. Stay cool.

#template #compiler
Задачка

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

Дан код на языке С++. Нужно проверить, что все скобки расположены правильно. Это значит, что каждая открытая скобка должна быть закрыта соответствующей закрывающей скобкой в правильном порядке. И каждая закрывающая скобка должна иметь соответствующую открывающую скобку.

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

Cпециально не описываю условие подробно, потому что мы все здесь плюсовики и понимаем, что в себя включает С++ код. Хотя на самом деле, это чтобы вы немножко помучались и на камни все-таки наступили😈.

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

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

Вечером выйдет пост с объяснениями и решением.

Погнали решать!

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

Challenge your life. Stay cool.

#задачки
Тред для ревью решений
Решение задачи через пару секунд появится в комментах
Почему не работает?

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

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

#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
Вычисления по короткой схеме vs ленивые вычисления Ч2

Когда мы поняли, что такое ленивые вычисления, давайте определим вычисления по короткой схеме и посмотрим, чем отличаются эти термины.

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

То есть это про конкретные операторы с конкретными свойствами в конкретных языках программирования.

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

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

Однако это мышление в рамках strict languages. Посмотрим на такой псевдокод:

variable = bigAndSlowFunc() or evenSlowerFnc()
if (need_heavy_computations)
print "Here it is: ", variable
else
print "I don't need you heavy computations!"


Если вычисления - ленивые, то переменная variable вычислится только при попадании в позитивную ветку условия. В другом случае ни bigAndSlowFunc, ни evenSlowerFnc никогда не будут вычислены.

И вот когда мы уже попали в позитивную ветку, то там идет в ход короткая схема.

В С/С++ если мы вставим в рандомном месте кода

auto variable = bigAndSlowFunc() || evenSlowerFnc();

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

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

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

Differentiate the meanings of things. Stay cool.

#cppcore
if constexpr. Мотивация.

Представим, что мы хотим определить, равны ли два числа, потенциально разных типов. Казалось бы, что задачка очень простая. Но это только на первый взгляд.

На самом деле это тривиально только для интегральных типов. a == b на этом все. Но вот для чисел с плавающей точкой все не так тривиально. Чисто исходя из того, что из себя представляют такие числа и как с ними оперировать, нельзя их сравнивать оператором ==. Корректное сравнение представляет собой сравнение модуля разности двух чисел с некоторой очень маленькой величиной epsilon. Если расстояние между двумя числами находится в пределах допускаемой нами погрешности, тогда эти числа равны.

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

Только вот это не очень эффективно с точки зрения производительности. Для целых чисел мы могли бы использовать один оператор, а тут будем использовать 3 действия - вычитание, взятие модуля и сравнение. Поэтому хотелось бы эти ветки разделить, чтобы они не пересекались. А сделать это можно с помощью шаблонов и sfinae. Для С++14 код будет выглядеть примерно так:

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

template <class T>
constexpr std::enable_if_t<std::is_floating_point<T>::value, bool>
is_equal(T a, T b) {
return absolute(a - b) < static_cast<T>(0.000001);
}

template <class T>
constexpr std::enable_if_t<!std::is_floating_point<T>::value, bool>
is_equal(T a, T b) {
return a == b;
}


Как видите, здесь используется std::enable_if. Код проверяет тип аргументов функции и направляет нужные типы в нужную перегрузку. Но как будто бы это очень сложно. И много кода просто повторяется. Хочется две эти перегрузки как-то объединить. Тем более тут вообще явно проглядываются две ветки условия, при ложном и правдивом исходе. Разве просто if тут не подойдет? Тут может и подойдет. Но не всегда обычный if в принципе может являться опцией. Давайте рассмотрим такой пример.

Мы хотим универсальную функцию to_str, которая возвращает строку, сделанную из переданного параметра. Используя стандартный if мы пишем:

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

auto str = to_str("10"s);


И при попытке это дело скомпилировать мы получим неожиданную ошибку: компилятор отказывается находить перегрузку функции std::to_string для std::basic_string. И правильно делает, ведь такой перегрузки нет. Но как же так? is_same дает true и мы можем просто вернуть строку без преобразований. Но не так все просто.

Компилятору нужно проверить весь код функции на корректность. Чтобы все символы резолвились, синтаксис соблюдался и вызывались корректные перегрузки. Во всех ветках кода. И в этом примере при попадании строки в функцию для нее не найдется перегрузка для std::to_string,

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

И с этим очень хорошо справляется if constexpr. То есть статический if. Условие времени компиляции. Как хотите. В следующем посте подробнее разберем эту конструкцию и с чем ее едят.

Choose the right path and don't overanalyze others. Stay cool.

#cpp14 #template #compiler
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
static_assert и if constexpr

Вернемся к прошлому примеру с функцией to\_str, но сделаем ее немножко безопасней.

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(false, "cannot convert type to std::string");
}


Все просто. Функция std::to_string имеет перегрузки только для тривиальных арифметических типов, поэтому чтобы ее использовать и получить ошибку, нужно провести проверку на эту арифметичность. Если переданный в функцию to\_str тип не конструируется в строку или не является арифметическим типом, то не совсем понятно, как такой тип переводить в строку. Точнее так: в каждом конкретном случае мы можем сказать как, но общего поведения нет. Поэтому, чтобы детектировать такие нештатные ситуации, я поставлю ассерт времени компиляции.

И вроде всё хорошо, но да не всё...

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

Тут в чем прикол. Согласно стандарту, если хотя бы в одной из веток if constexpr не может быть сгенерировано ни одной валидной специализации, то программа - ill-formed и это ошибка. И вправду, false всегда будет false, как бы мы там не тужились родить что-то валидное.

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

template<typename>
inline constexpr bool dependent_false_v = false;

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(dependent_false_v<T>, "cannot convert type to std::string");
}


Видимо из-за того, что шаблон можно вручную специализировать, то теоритически возможна специализация, которая будет возвращать true. И даже если она еще не инстанцирована, то возможность-то остается. Поэтому компилятор пропускает такую конструкцию.

Однако ситуация изменилась с приходом С++23, но об этом мы поговорим завтра.

Find loopholes in the system. Stay cool.

#cpp17 #template
Обновления в отношениях static_assert и if constexpr

Наш подписчик Вадим упомянул о важном изменении в отношениях static_assert и if constexpr. Эти изменения вступили в силу с приходом С++23 и говорят о том, что теперь программа не считается ill-formed, даже если static_assert фейлится для всех специализаций.

Посмотрим на уже заезженном примере:

template <typename T>
std::string to_str(T t) {
if constexpr (std::is_constructible_v<std::string, T>)
return t;
else if constexpr (std::is_arithmetic_v<T>)
return std::to_string(t);
else
static_assert(false, "cannot convert type to std::string");
}

class A{};

int main()
{
std::cout << to_str("qwe") << std::endl; // OK
std::cout << to_str(5.0) << std::endl; // OK
std::cout << to_str(A{}) << std::endl; // static_assert failed
}


Создали структуру пустышку и использовали функцию to_str по всем трем веткам условия. И если закомментировать последний вывод в консоль - все корректно компилируется и выполняется. Но как только мы инстанцируем специализацию с пессимистичной веткой условия, компиляция падает на ассерте.

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

До прихода 23-х плюсов была очевидная проблема с этим static_assert'ом. Вроде бы это очень логично писать условия как в примере выше и ожидать, что это сработает. Да и наличие смешного донельзя воркэраунда с шаблонным типозависимым выражением, которое все равно возвращает false, как бы намекает, что что-то не так. Радостно видеть развитие языка и закрытие таких болючих пробелов.

Fix your flaws. Stay cool.

#cpp23 #template
static_assert

Раз уж так много его обсуждаем, почему бы не рассказать о том, что это вообще такое.

Это фича С++11, которая позволяет прерывать компиляцию программы, если определенное условие не выполнено. Естественно, условие должно быть bool-constexpr выражением, иначе ничего не выйдет. static_assert на то и static, потому что работает во время компиляции. Рантайм выражения в это время не могут быть вычислены.

На данный момент доступны 2 вида этого ассерта:

static_assert(bool-constexprunevaluated-string)

static_assert(bool-constexpr)

Первая версия доступна с 11-х плюсов, вторая с 17-х. Первая добавляет к сообщению об ошибке вашу кастомную, но захардкоженую, строку для более полного и понятного контекста. Вторая такого поведения не предполагает, но оно и не всегда нужно.

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

Мне кажется, что не стоит ставить ассерты на определенный тип шаблонных параметров шаблонных сущностей. Как верно указал Михаил в [этом комментарии](https://t.me/c/2009887601/2547), так будет сложно проверить на корректность перегрузку. Для такого рода ограничений лучше использовать концепты или sfinae. Единственное, что вы тогда потеряете вменяемое сообщение об ошибке и возможность его дополнить своим сообщением.

Статический ассерт может быть удобен для проверки размера структуры, которая взаимодействует с внешним миром. Например, низкоуровневый сетевой код может полагаться на один размер структуры, и какой-то нерадивый человек внес изменения в эту структуру и изменил ее размер без изменения всего связанного кода. Тогда возможны очень трудноотловимые ошибки. Этого можно избежать, поставив простой ассерт на размер этой структуры. И если размер вдруг изменится, то компиляция просто прервется. Тоже самое можно проводить и с размерами базовых типов. Правда тут возможно придется со всякими паддингами повозиться(эффективный рэндж беззнакового инта может быть [0, 65тыщ_что-то_там], а в памяти он будет занимать будет 4 байта).

Можно также проверять версию подключаемых библиотек, если они предоставляют свою версию в виде constexpr выражения, а не макроса(хотя это довольно редкое явление). Типа такого:


#include "SomeVeryC++ishLibrary.hpp"

static_assert(SomeVeryC++ishLibrary.::Version > 2,
"Old versions of SomeVeryC++ishLibrary. are missing functionality to destroy the world.
It is very desired functionality so cannot process!");

class DestroyTheWorld {
// BOOM!!
};


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

Check your assumptions in advance. Stay cool.

#cpp11 #cpp17 #compiler
Почему нельзя помещать определение шаблона в cpp файл

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

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

Так вот, чтобы получить этот полный подробный чертеж(класс), нужно добавить эти подробные схемы отдельных частей. То есть инстанцировать шаблон.

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

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

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

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

Возьмем стандартную схему объявления шаблонного класса в хэдэре и ее реализацию в файле исходников.


// ship.hpp
template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command);
};

// ship.cpp
#include "ship.hpp"

template <class T>
void Ship<T>::TurnShip(T command) {/* do stuff using command */}

// main.cpp
#include "ship.hpp"

int main() {
Ship<int> ship;
ship.TurnShip(5);
}


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

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

А вы в main.cpp пытаетесь использовать уже сам объект. То есть полностью готовый корабль из полноценного чертежа. В файле main.cpp нет никакого чертежа. Ну хорошо, возможно там что-то в ship.hpp есть подобное. А там только объявление шаблона. Ну хорошо, компилятор на данном этапе может работать только с объявлением, как и с объявлениями обычных классов, ничего странного. Сейчас он поставит заглушки на моменты создания и использования объекта. Но он попытается на этапе линковки разрезолвить все символы. Но вот незадача - на этапе линковки компилятор не увидит сгенерированного кода специализации Ship<int>. Потому что в единице трансляции, соответствующей файлу ship.cpp, никакого полезного сгенерированного кода нет!

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

Однако есть и другое решение!

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

Don't do extra work in vain. Stay cool.

#cppcore #template
Явная и неявная инстанциация шаблона

Как только компилятор видит полное определение шаблона, он может инстанцировать его с каким-то конкретным аргументом. И тут есть 2 варианта.

Вернемся к вчерашнему примеру с шаблоном корабля.


// ship.hpp

template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command) {// do some stuff}
};

// main.cpp

#include "ship.hpp"

int main() {
Ship<int> ship;
ship.TurnShip(5);
}


Согласно заветам предыдущего поста, мы перенесли все определение шаблона в хэдэр, подключили этот хэдэр в мэйн и использовали объект. И раз в мэйне мы используем объект, то в этом конкретном случае произошло неявное инстанцирование шаблона - компилятор все сделал за нас. Мы дали ему определение шаблона, укропу, кошачью жопу, ... и охапку дров, а он нам выдал плов. Точнее конкретный код, соответствующий конкретной специализации Ship<int>.

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

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


// ship.hpp
template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command);
};

// ship.cpp
#include "ship.hpp"
#include <string>

template <class T>
void Ship<T>::TurnShip(T command) {/* do stuff using command */}

template struct Ship<std::string>; // HERE IT IS!!

// main.cpp
#include "ship.hpp"
#include <string>

int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
}


template class Ship<std::string> - яное инстанцирование шаблона. Синтаксис следующий:

template class-key template-name <argument-list>;

class-key - любое из struct/class/union, должно соответствовать оному в самом шаблоне.

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

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

Но в целом, это хорошая практика. Поэтому используйте на здоровье.

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

Find a way out of your problems. Stay cool.

#cppcore #template
Внутрянка инстанциаций шаблонов

Хочу в этом небольшом посте в явном виде продемонстрировать вам, что значит определение шаблона и его явная/неявная инстанциация. Сразу говорю, что буду использовать gcc в качестве компилятора.

Возьмем тот же пример с кораблем и оставим от него только хэдэр и сорец:

// ship.hpp
template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command);
};

// ship.cpp
#include "ship.hpp"
#include <string>

template <class T>
void Ship<T>::TurnShip(T command) {/* do stuff using command */}



Сейчас в единице трансляции, соответствующей ship.cpp, есть только объявление и определение шаблона, больше ничего. Это значит, что никакого кода для этого юнита генерироваться не будет.

Проверим это с помощью утилитки nm, которая показывает символы бинарника. Скомпилируем ship.cpp в объектный файл и посмотрим, какие там символы есть внутри:

nm ship.o

//Output
ship.o: no symbols



Как говорят математики: ЧТД и точка!

Теперь проверим неявную инстанциацию. Добавим в ship.cpp функцию:

void foo() {
Ship<std::string> ship{};
ship.TurnShip(std::string{"Turn upside down"});
}



Посмотрим теперь на символы этого юнита. Помимо всего прочего побочного непотребства, получим следующее:

nm ship.o

//Output
0000000000000000 T __Z3foov
0000000000000060 T __ZN4ShipINSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEEE8TurnShipES6_


Теперь мы имеем скомпилированную функцию foo и метод TurnShip класса Ship параметризованного строкой.

Для явной инстанциации уберем из ship.cpp функцию foo и добавим строчку:

template struct Ship<std::string>;


и посмотрим на символы:

nm ship.o

//Output
0000000000000000 T __ZN4ShipINSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEEE8TurnShipES6_



Как и ожидалось в принципе.

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

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

Don't rely on words, check them. Stay cool.

#cppcore #template #compiler