this->notes.
4.54K subscribers
35 photos
1 file
372 links
О разработке, архитектуре и C++.

Tags: #common, #cpp, #highload и другие можно найти поиском.
Задачки: #poll.
Мои публикации: #pub.
Автор и предложка: @vanyakhodor.
GitHub: dasfex.
Download Telegram
1 июня начнём марафон.
Все посты будут объединены одной темой (которую вы быстро поймёте, я думаю). Появляться будут должны в районе 9 утра по Лондону (11 по Минску/Москве).

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

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

Как долго марафон продлится, выясним в процессе. Я таким раньше не занимался. Будем открывать новое коллективно.
30🔥8👍5❤‍🔥4🫡3💩2👏1
#cpp

Day 1.

#include

Думаю, все вы знаете, что #include просто вставляет весь код в файл, в котором он расположен. Из-за этого даже простой Hello world с использованием std::cout разрастается до десятков-сотен тысяч строк (хотя сам хедер может быть маленький, он транзитивно тянет много других зависимостей). Собсна поэтому инклуды не очень любят: легко засрать ваш проект и получить большое время компиляции. Отсюда и появляются штуки вроде forward declaration, pimpl, precompiled headers, include-what-you-use и модули.

Инклудить вы можете что угодно. Хоть txt, хоть бинарный файл. Препроцессору на это всё равно. Главное, чтобы содержимое после препроцессинга было валидно.

Можно хоть так:

#include "/dev/stdin"

и потом

echo 'int x = 42;' | g++ main.cpp

Порядок инклудов может значимо менять поведение в программе. Вот вам пример от Паши: https://xn--r1a.website/cpp_durka/49

Инклуды могут быть с <> и с "". Вариант подключения влияет на то, где хедеры ищутся.

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

Ну и всегда можно воспользоваться инструментом неправильно. Случайно или специально. Вспомним The Grand C++ Error Explosion Competition.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🔥33👍86🍌3💩2
#cpp

Day 2.

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

Защищаться от подобной ситуации нам помогают include guards (и pragma, но про неё не сегодня). Пишем в начале файла


#ifndef GUARD_H
#define GUARD_H

#endif // GUARD_H


Какие могут быть проблемы?

Если один и тот же GUARD используется в нескольких файлах, то один из них просто молча не подключится. Удачи дебагать!
Сейчас IDE (по крайней мере те, с которыми я работал) успешно генерируют длинное название, связанное с именем файла. Но не всегда успешно меняют имена guards при переименовании файла.

Есть ещё мнение, что include guards медленные. Хотя обычно, если у вас вид канонический, как в примере выше, то компиляторы способны запомнить, что файл уже был прочитан, и не тратить время на повторные операции.
Но если у вас какой-то специфический случай (начинаете вставлять код до include guards или после), то уже уверенными быть не стоит.

Исходя из того, как работают макросы и на что опираются guards, получается, что легко можно сломать инклуд хедера, если где-то определить #define:


#define GUARD_H
#include "some.h"


Вот ещё один пример, когда порядок хедеров может на что-то влиять.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍18💩3🔥211🗿1
#cpp

Day 3.

Другой способ быть уверенным, что хедер включится только единожды: #pragma once.

Важно помнить, что это не стандартное решение, а универсальное расширение, которое реализуется де-факто всеми компиляторами.

#pragma once хорошо оптимизируется (фактически). С ней даже чуть проще, так как она просто влияет на файл, в котором находится, когда include guards только на то, что внутри своего скоупа.

#pragma once обычно работает через какой-то признак, помогающий понять, что файл — один и тот же. Из вариантов могут быть:
• inode
• канонический path
• какой-то hash
• и другие варианты.

Из-за чего в сложных специфических системах может вполне себе сломаться. И вы получите один и тот же файл дважды. Удачи дебагать!

Аналогично могут быть проблемы в распределённых build-системах.

Иногда вы хотите, чтобы один файл инклудился дважды. Тогда #pragma once вам не подходит.

В некоторых сферах #include guards выбирают просто за надёжность и стабильность, так как они зависят исключительно от кода, а не ещё каких-то посторонних вещей.

Можно вообще в один файл совать и #pragma once, и include guards. Чтобы спокойнее было.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍194💩2
#cpp

Day 4.

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

Паша @cppdurka Сухов говорил, что когда-то видел какой-то гайд на 18 страниц, как правильно писать макросы. Видел и потерял. А теперь жалеет об этом.

Я бы тоже хотел почитать. Скиньте, если знаете про такой!

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍15💩6😁3🫡1
#cpp

Day 5.

#ifndef в include guards на самом деле #if !defined(...). Есть ещё #ifdef (#if defined(...)). Сам #if тоже есть. Можете намутить себе условной компиляции по самые колени. Например, может у вас есть какая-то дебажная сборка с большим кол-вом логов:


#ifdef DEBUG
printf("debug log");
#endif

И код можно скомпилировать с -DDEBUG.

Или вы хотите написать разную логику для 32-битной и 64-битных систем:


#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64
// code for a 32-bit system
#else
// code for a 64-bit system
#endif


Или вы хотите использовать новый стандарт, если он доступен, и не использовать, если есть своя поделка:


#if __cplusplus >= 202002L
#include <span>
#else
#include "my_span.hpp"
#endif


Хотя правильнее было бы проверять не на стандарт, а на доступность фичи/инклуда/атрибута, так как стандарт может быть новым, а STL старой. А некоторые фичи могут бекпортить.


#if __cpp_lib_span

#if __has_include(<format>)

#if __has_cpp_attribute(likely)
#define LIKELY [[likely]]
#else
#define LIKELY
#endif


Значения __cpp_* макросов это кстати дата принятия фичи WG21. Например, для __cpp_constexpr это 202211L (ноябрь 2022). Для того же constexpr можно узнавать его версию и соответственно набор возможностей, которые вы можете использовать, так как от стандарта к стандарту он сильно умощнялся.

Кто-то мне рассказывал байку, что MSVC много лет врал про значение __cplusplus (всегда возвращал 199711L, даже для C++17), потому писали вот так:


#if defined(_MSVC_LANG)
#define CPP_VER _MSVC_LANG
#else
#define CPP_VER __cplusplus
#endif


@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍26🔥2💩21
#cpp

Day 6.

Предположим, мы хотим написать макрос для возведения значения или переменной в квадрат:

#define SQR(x) x * x

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

int a = SQR(1 + 2);

мы на самом деле получим

int a = 1 + 2 * 1 + 2;

Что не совсем то, что вы ожидали.

Для надёжности лучше завернуть аргументы в скобки:

#define SQR(x) (x) * (x)


Но этого тоже может иногда не хватать:

#define INC(x) (x) + 1

int a = 10 / INC(1 + 1);

Получим:

int a = 10 / (1 + 1) + 1;

Что тоже не то, что мы ожидали. Так что адекватный макрос должен как минимум завернуть в скобки каждый отдельный аргумент + завернуть всё выражение:

#define SQR(x) ((x) * (x))


Как максимум, ваш x может быть вообще-то функцией с сайд-эффектом:

int x = SQR(GetValueFromDbAndPostToKafka());

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

Мб пора начать сворачивать с макродорожки на что-то более современное......

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🔥226
#cpp

Day 7.

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

Обопрусь на пример от Паши (https://xn--r1a.website/cpp_durka/23): напишем макрос для инкремента двух переменных.

#define INCREMENT_BOTH(x, y) (x)++; (y)++

Тут мы умные. Сразу взяли выражения в скобки. Не поставили в конце ; , чтобы обязать пользователя её поставить самому (для консистентности кода).

Но если мы чуть-чуть отступим от глупого использования:

if (condition)
INCREMENT_BOTH(a, b);

мы получим

if (condition)
(a)++; (b)++;

b инкрементится вне зависимости от условия.

Или ещё пример:

#define MACRO(condition, x) if (condition) std::cout << (x)

if (flag)
MACRO(flag2, 5);
else
std::cout << 10;

Тут else вдруг начинает относиться к if из макроса, а не изначальному, что очевидно баг.

Канонический способ такое исправить:

#define INCREMENT_BOTH(x, y) \
do { \
(x)++; \
(y)++; \
} while (0)

#define MACRO(condition, x) \
do { \
if (condition) { \
std::cout << (x);\
} \
} while (0)


@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍321
#cpp

Day 8.

В C тоже есть массивы. И работягам тоже хочется знать, сколько в этих массивах элементов. Стандартного решения у ребят там нет, но есть общий подход, перетекающий из кодовой базы в кодовую базу. Зовётся ARRAY_LENGTH/ARRAY_LEN/ARRAY_SIZE/COUNTOF/...
Выглядит так:

#define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0]))


Тут мы полагаемся на sizeof, который, согласно стандарту C99 (моя вольная интерпретация):

sizeof возвращает размер операнда (в байтах). Размер зависит от типа операнда. Результат — int. Обычно результат не evaluated и является integer constant.


Another use of the sizeof operator is to compute the number of elements in an array:

sizeof array / sizeof array[0]


Так что sizeof(x) вернёт кол-во байт типа массива (если x — массив int[3], то можем получить (в зависимости от системы) 12). sizeof((x)[0]) вернёт размер типа одного элемента (в нашем случае 4). Вот и получаем 3.

На собесах могут спрашивать вопросы с подвохом вида:

int x = 10;
sizeof(x++);
std::cout << x; // result?

Конечно, вы на такое не попадётесь и скажете 10, ведь sizeof интересует тип. Он не evaluatит свой аргумент. Но всегда ли это так?

Конечно нет!
Если ваш аргумент — Variable Length Array, то sizeof придётся вычислить аргумент. Мы можем запруфать это через наличие сайдэффекта:


int f() {
printf("called\n");
return 10;
}

int main() {
sizeof(int[f()]);
}



Увидим called в output.
https://godbolt.org/z/8G1soa8T1

Теперь срочно требуйте оффер х3 от вашего текущего дохода, ведь собеседующий почти наверняка этого не знает.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍20🔥96🤯3😭2
#cpp

Day 9.

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

Подстановка макросов (expansion) не являются рекурсивной.


#define A(x) A(x x)
A(x) // A(x x)

#define B(x) C(x x)
#define C(x) B(x x)
B(x) // B(x x x x)


Выстрелить себе в ногу становиться чуть сложнее. Или проще. Это опять как посмотреть.

@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
11👍7🔥1