1 июня начнём марафон.
Все посты будут объединены одной темой (которую вы быстро поймёте, я думаю). Появляться будут должны в районе 9 утра по Лондону (11 по Минску/Москве).
Постараюсь делать микропост каждый день. Хочется, чтобы он охватывался за минуту-две и давал вам маленький кусочек знания. А может и ничего не давал. Кто знает!
Возможно будем прерываться на другие посты. Но это только если важно будет запостить в конкретное время, чтобы актуальность не терялась.
Как долго марафон продлится, выясним в процессе. Я таким раньше не занимался. Будем открывать новое коллективно.
Все посты будут объединены одной темой (которую вы быстро поймёте, я думаю). Появляться будут должны в районе 9 утра по Лондону (11 по Минску/Москве).
Постараюсь делать микропост каждый день. Хочется, чтобы он охватывался за минуту-две и давал вам маленький кусочек знания. А может и ничего не давал. Кто знает!
Возможно будем прерываться на другие посты. Но это только если важно будет запостить в конкретное время, чтобы актуальность не терялась.
Как долго марафон продлится, выясним в процессе. Я таким раньше не занимался. Будем открывать новое коллективно.
❤35🔥9❤🔥5👍5🫡3💩2👏1
#cpp
Day 1.
Думаю, все вы знаете, что
Инклудить вы можете что угодно. Хоть txt, хоть бинарный файл. Препроцессору на это всё равно. Главное, чтобы содержимое после препроцессинга было валидно.
Можно хоть так:
и потом
Порядок инклудов может значимо менять поведение в программе. Вот вам пример от Паши: https://t.me/cpp_durka/49
Инклуды могут быть с
Когда-то препроцессор не умел добавлять пустую строку после инклудов, потому обязательно было иметь пустую строку в конце вашего файла. Сейчас можно и без этого, но душе уже не прикажешь...
Ну и всегда можно воспользоваться инструментом неправильно. Случайно или специально. Вспомним The Grand C++ Error Explosion Competition.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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://t.me/cpp_durka/49
Инклуды могут быть с
<> и с "". Вариант подключения влияет на то, где хедеры ищутся. Когда-то препроцессор не умел добавлять пустую строку после инклудов, потому обязательно было иметь пустую строку в конце вашего файла. Сейчас можно и без этого, но душе уже не прикажешь...
Ну и всегда можно воспользоваться инструментом неправильно. Случайно или специально. Вспомним The Grand C++ Error Explosion Competition.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🔥36👍8❤6🍌3💩2
#cpp
Day 2.
Так как
Защищаться от подобной ситуации нам помогают include guards (и pragma, но про неё не сегодня). Пишем в начале файла
Какие могут быть проблемы?
Если один и тот же GUARD используется в нескольких файлах, то один из них просто молча не подключится. Удачи дебагать!
Сейчас IDE (по крайней мере те, с которыми я работал) успешно генерируют длинное название, связанное с именем файла. Но не всегда успешно меняют имена guards при переименовании файла.
Есть ещё мнение, что include guards медленные. Хотя обычно, если у вас вид канонический, как в примере выше, то компиляторы способны запомнить, что файл уже был прочитан, и не тратить время на повторные операции.
Но если у вас какой-то специфический случай (начинаете вставлять код до include guards или после), то уже уверенными быть не стоит.
Исходя из того, как работают макросы и на что опираются guards, получается, что легко можно сломать инклуд хедера, если где-то определить
Вот ещё один пример, когда порядок хедеров может на что-то влиять.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
👍20💩3🔥2❤1⚡1🗿1
#cpp
Day 3.
Другой способ быть уверенным, что хедер включится только единожды:
Важно помнить, что это не стандартное решение, а универсальное расширение, которое реализуется де-факто всеми компиляторами.
• inode
• канонический path
• какой-то hash
• и другие варианты.
Из-за чего в сложных специфических системах может вполне себе сломаться. И вы получите один и тот же файл дважды. Удачи дебагать!
Аналогично могут быть проблемы в распределённых build-системах.
Иногда вы хотите, чтобы один файл инклудился дважды. Тогда
В некоторых сферах
Можно вообще в один файл совать и
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
👍20❤5💩2
#cpp
Day 4.
Макросы просто подменяют текст. Вот прям втупую. Но мы часто про это забываем, потому часто пишем их неправильно.
Паша @cpp_durka Сухов говорил, что когда-то видел какой-то гайд на 18 страниц, как правильно писать макросы. Видел и потерял. А теперь жалеет об этом.
Я бы тоже хотел почитать. Скиньте, если знаете про такой!
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 4.
Макросы просто подменяют текст. Вот прям втупую. Но мы часто про это забываем, потому часто пишем их неправильно.
Паша @cpp_durka Сухов говорил, что когда-то видел какой-то гайд на 18 страниц, как правильно писать макросы. Видел и потерял. А теперь жалеет об этом.
Я бы тоже хотел почитать. Скиньте, если знаете про такой!
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍15💩6😁4🫡1
#cpp
Day 5.
И код можно скомпилировать с
Или вы хотите написать разную логику для 32-битной и 64-битных систем:
Или вы хотите использовать новый стандарт, если он доступен, и не использовать, если есть своя поделка:
Хотя правильнее было бы проверять не на стандарт, а на доступность фичи/инклуда/атрибута, так как стандарт может быть новым, а STL старой. А некоторые фичи могут бекпортить.
Значения
Кто-то мне рассказывал байку, что MSVC много лет врал про значение
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
👍28🔥2💩2❤1
#cpp
Day 6.
Предположим, мы хотим написать макрос для возведения значения или переменной в квадрат:
В зависимости от способа использования, вы можете не получить или получить проблемы. В таком коде:
мы на самом деле получим
Что не совсем то, что вы ожидали.
Для надёжности лучше завернуть аргументы в скобки:
Но этого тоже может иногда не хватать:
Получим:
Что тоже не то, что мы ожидали. Так что адекватный макрос должен как минимум завернуть в скобки каждый отдельный аргумент + завернуть всё выражение:
Как максимум, ваш
Так что прям совсем идеально было бы сохранить результат внутри макроса и переиспользовать его. Но я не знаю, как это написать, чтобы было полностью эквивалентно функции.
Мб пора начать сворачивать с макродорожки на что-то более современное......
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
🔥23❤6👍1
#cpp
Day 7.
Вчера мы писали макросы, которые заменяли собой один statement. А что, если я хочу что-то более сложное?
Обопрусь на пример от Паши (https://t.me/cpp_durka/23): напишем макрос для инкремента двух переменных.
Тут мы умные. Сразу взяли выражения в скобки. Не поставили в конце
Но если мы чуть-чуть отступим от глупого использования:
мы получим
Или ещё пример:
Тут
Канонический способ такое исправить:
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 7.
Вчера мы писали макросы, которые заменяли собой один statement. А что, если я хочу что-то более сложное?
Обопрусь на пример от Паши (https://t.me/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.
👍33❤1
#cpp
Day 8.
В C тоже есть массивы. И работягам тоже хочется знать, сколько в этих массивах элементов. Стандартного решения у ребят там нет, но есть общий подход, перетекающий из кодовой базы в кодовую базу. Зовётся
Выглядит так:
Тут мы полагаемся на
Так что
На собесах могут спрашивать вопросы с подвохом вида:
Конечно, вы на такое не попадётесь и скажете 10, ведь
Конечно нет!
Если ваш аргумент — Variable Length Array, то sizeof придётся вычислить аргумент. Мы можем запруфать это через наличие сайдэффекта:
Увидим called в output.https://godbolt.org/z/8G1soa8T1
Теперь срочно требуйте оффер х3 от вашего текущего дохода, ведь собеседующий почти наверняка этого не знает.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
Теперь срочно требуйте оффер х3 от вашего текущего дохода, ведь собеседующий почти наверняка этого не знает.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍26🔥9❤7🤯3😭1
#cpp
Day 9.
До сегодняшнего дня мы были где-то на уровне 1. Сегодня делаем шаг на следующую ступеньку (вниз или вверх, это как посмотреть).
Подстановка макросов (expansion) не являются рекурсивной.
Выстрелить себе в ногу становиться чуть сложнее. Или проще. Это опять как посмотреть.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
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.
❤14👍11🔥2
#cpp
Day 10.
Когда вы работаете с макросами, вам может пригодиться stringification operator.
Он позволяет превратить аргумент макроса в строку:
Причём аргумент в таком случае не expandится:
Если хотите раскрыть, нужен ещё один уровень:
Так что некоторые разные входные данные могут давать одинаковый результат. Надо быть осторожными.
Вы можете использовать
Эта же фича используется в
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 10.
Когда вы работаете с макросами, вам может пригодиться stringification operator.
#define STR(x) #x
Он позволяет превратить аргумент макроса в строку:
STR(123 foo real) // "123 foo real"
Причём аргумент в таком случае не expandится:
#define FOO 123
#define STR(x) #x
STR(FOO) // "FOO"
Если хотите раскрыть, нужен ещё один уровень:
#define STR2(x) #x
#define STR(x) STR2(x)
STR(FOO) // "123"
# почти сохраняет исходный текст (может поудалять лишние пробелы):
STR( a + b ) // "a + b"
Так что некоторые разные входные данные могут давать одинаковый результат. Надо быть осторожными.
Вы можете использовать
# для дебажных утилиток:
#define PRINT(expr) \
std::cout << #expr << " = " << (expr) << '\n';
PRINT(x + y) // x + y = 12
Эта же фича используется в
assert (вы видите упавшее условие в ошибке).@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍32❤6❤🔥3🔥3
#cpp
Day 11.
Используется в макросах для склеивания двух токенов (на этапе препроцессинга конечно же).
Склеиваются именно токены. После препроцессинга должен получиться валидный токен:
Используется для генерации чего угодно. Часто думают только про имена функций/переменных/классов, но можно и для операторов, ключевых слов, литералы.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 11.
## — token pasting operator или token concatenation operator.Используется в макросах для склеивания двух токенов (на этапе препроцессинга конечно же).
#define MAKE_VAR(x) var_##x
int MAKE_VAR(test) = 42; // int var_test = 42;
Склеиваются именно токены. После препроцессинга должен получиться валидный токен:
#define BAD(a, b) a ## b
BAD(+, +) // ++ ✅
BAD(x, *) // x* ⛔️
Используется для генерации чего угодно. Часто думают только про имена функций/переменных/классов, но можно и для операторов, ключевых слов, литералы.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍17❤2
#cpp
Day 12.
Иногда вы хотите написать какой-то общий макрос, который будет работать для произвольного числа аргументов:
Частая проблема с
Мы получаем пустой пак аргументов, из-за чего получаем лишнюю запятую, после которой ничего нет. Это большая боль.
Как жить с этим, расскажу попозже.
У макросов часто возникают проблемы с шаблонами, так как параметры макроса сплитятся по запятой:
И вы получите ошибку компиляции, т.к. в примере выше
•
•
•
•
А может принять только 2 (и никак не
Я постоянно стукаюсь об это, когда пишу тесты с gtest.
С
Раскрывается примерно так:
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 12.
Иногда вы хотите написать какой-то общий макрос, который будет работать для произвольного числа аргументов:
#define MACRO(...) __VA_ARGS__
__VA_ARGS__ — способ сослаться на все аргументы, обозначенные как ... в макросе. Компилятор просто перечислит их через запятую:
#define DEBUG(...) printString(__VA_ARGS__)
DEBUG("1", "2", "3"); // printString("1", "2", "3")
Частая проблема с
__VA_ARGS__ — пустой пак аргументов.
#define LOG(fmt, ...) print(fmt, __VA_ARGS__)
LOG("str"); // print("fmt", )
Мы получаем пустой пак аргументов, из-за чего получаем лишнюю запятую, после которой ничего нет. Это большая боль.
Как жить с этим, расскажу попозже.
У макросов часто возникают проблемы с шаблонами, так как параметры макроса сплитятся по запятой:
#define A(a, b) f(a, b)
A(std::pair<int, int>{1, 1}, 1);
И вы получите ошибку компиляции, т.к. в примере выше
A получил 4 аргумента:•
std::pair<int•
int>{1, •
1}•
1А может принять только 2 (и никак не
f(std::pair<int, int>{1) ).Я постоянно стукаюсь об это, когда пишу тесты с gtest.
С
__VA_ARGS__ это иногда может заработать, т.к. вы теперь все аргументы всегда передаёте пачкой:
#define A(...) f(__VA_ARGS__)
A(std::pair<int, int>{1, 1}, 1); // f(std::pair<int, int>{1, 1}, 1)
__VA_ARGS__ можно использовать для подсчёта кол-ва аргументов в макросе (пример для до 5 аргументов):
#define NARGS_IMPL(_1,_2,_3,_4,_5,N,...) N
#define NARGS(...) NARGS_IMPL(__VA_ARGS__, 5,4,3,2,1)
NARGS(a, b, c) // 3
Раскрывается примерно так:
NARGS(a, b, c)
NARGS_IMPL(
_1 = a
_2 = b
_3 = c
_4 = 5
_5 = 4
N = 3
)
N = 3
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍12❤3🌚3
#cpp
Day 13.
Если вам
Фактически это замена области видимости для макросов.
Один из примеров (я не говорю, что хороших, мы тут вообще про макросы говорим, что с ними хорошего?) — получить доступ к членам объектов для тестирования:
После
Аналогично вы можете подменить все вызовы функции в подключённом хедере на вашу функцию.
Конечно, можно сломать что-нибудь:
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 13.
Если вам
#define нужен «на время», вы можете потом раздефайнить:
#define X
X
X
...
X
#undef X
X // ⛔️CE
Фактически это замена области видимости для макросов.
Один из примеров (я не говорю, что хороших, мы тут вообще про макросы говорим, что с ними хорошего?) — получить доступ к членам объектов для тестирования:
// in test.c
#define private public
#include "logic_with_A_class.h"
// in some test
A a = getA();
assert(a.x == 1);
#undef private
После
#define в вашем инклуде все приватные поля в A станут публичными, что даёт нам возможность к ним обратиться (к полю x в примере). Аналогично вы можете подменить все вызовы функции в подключённом хедере на вашу функцию.
#define malloc my_malloc
#include "code.h"
#undef malloc
Конечно, можно сломать что-нибудь:
#undef assert
#undef errno
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
🔥8❤4👍3🤯3🤬3👎1😁1🤔1
#cpp
Day 14.
Бывает, что вы наусловнокомпилировали чего-то. Ифдефами обмазались. И не можете покрыть все-все случаи, которые у вас возникают.
Или хотите быть прозрачными с пользователем, что в его ситуации нет пока готового решения.
Или может хотите юзеру сообщить, что он делает что-то странное (например, пытается использовать стандарт X на платформе Y).
Что делать?
Выдать понятное сообщение!
В препроцессорном мире вам поможет
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 14.
Бывает, что вы наусловнокомпилировали чего-то. Ифдефами обмазались. И не можете покрыть все-все случаи, которые у вас возникают.
Или хотите быть прозрачными с пользователем, что в его ситуации нет пока готового решения.
Или может хотите юзеру сообщить, что он делает что-то странное (например, пытается использовать стандарт X на платформе Y).
Что делать?
Выдать понятное сообщение!
В препроцессорном мире вам поможет
#error:
#error "You did something wrong. Drink beer."
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍16😁9❤5
#cpp
Day 15.
Как вы могли заметить в макросах из предыдущих дней, иногда мы для удобства разделяем их на строки с помощью
Когда компилятор видит
Что важно, этап склеивания строк идёт до этапа удаления комментариев, так что можно легко напороться на проблему в таком случае:
Вы получите
Хорошая IDE подсветит, но если у вас есть друг, который типа крутой кодит в блокноте, пранканите его как-нибудь. Воткните в конце комментария с отступом в 200 пробелов вправо
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 15.
Как вы могли заметить в макросах из предыдущих дней, иногда мы для удобства разделяем их на строки с помощью
\ (backslash). Когда компилятор видит
\ + \n (new line character), он их просто удаляет. Так вы превращаете несколько physical source lines в одну logical source line.Что важно, этап склеивания строк идёт до этапа удаления комментариев, так что можно легко напороться на проблему в таком случае:
int x = 1;
// some logic here \
++x;
std::cout << x;
Вы получите
int x = 1;
// some logic here ++x;
std::cout << x; // 1
Хорошая IDE подсветит, но если у вас есть друг, который типа крутой кодит в блокноте, пранканите его как-нибудь. Воткните в конце комментария с отступом в 200 пробелов вправо
\. Пусть дебагает.@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
😁44😢3🗿2❤1
#cpp
Day 16.
Пусть вы хотите определить какой-то
В таком случае нам поможет идиома X macro!
Для начала определим базовый макрос, куда мы будем добавлять все значения:
Теперь давайте объявим enum:
Получим:
А теперь можем использовать:
Где
При необходимости добавить новое значение нужно будет только в самый первый макрос! Не будет такого, что имя значения и его строкового представления не будут одинаковы.
Иногда X macro могут вынести в отдельный
Вот кстати ещё один пример, когда мы можем хотеть инклудить один файл в другой несколько раз.
Делать так можно для много чего. Можно описать коды ответов:
Или инструкции какие-нибудь:
Что вам там надо.
Дебагать конечно это тяжко. Как и любую макрокучу.
Почему
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 16.
Пусть вы хотите определить какой-то
enum один разок и потом не упускать все места, где используются его значения (причём используются по-разному). В таком случае нам поможет идиома X macro!
Для начала определим базовый макрос, куда мы будем добавлять все значения:
#define COLORS \
X(Red) \
X(Green) \
X(Blue)
Теперь давайте объявим enum:
enum Color {
#define X(name) name,
COLORS
#undef X
};
Получим:
enum Color {
Red,
Green,
Blue,
};
А теперь можем использовать:
const char* ToString(Color c) {
switch (c) {
#define X(name) case name: return #name;
COLORS
#undef X
}
return "Unknown";
}
Где
switch станет:
switch (c) {
case Red: return "Red";
case Green: return "Green";
case Blue: return "Blue";
}
При необходимости добавить новое значение нужно будет только в самый первый макрос! Не будет такого, что имя значения и его строкового представления не будут одинаковы.
Иногда X macro могут вынести в отдельный
.def файл:
// colors.def
X(Red)
X(Green)
X(Blue)
// usage somewhere
enum Color {
#define X(name) name,
#include "colors.def"
#undef X
};
Вот кстати ещё один пример, когда мы можем хотеть инклудить один файл в другой несколько раз.
Делать так можно для много чего. Можно описать коды ответов:
#define ERRORS \
X(404, NotFound) \
X(500, Internal) \
X(403, Forbidden)
Или инструкции какие-нибудь:
#define INSTRUCTIONS \
X(Add, 0x01) \
X(Sub, 0x02)
Что вам там надо.
Дебагать конечно это тяжко. Как и любую макрокучу.
Почему
X? Видимо исторически. @thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
❤20👍9👎2😱1🤬1
#cpp
Day 17.
С уровнем 2 мы закончили. Делаем шаг в уровень 3.
Есть несколько «служебных» макросов, которые могут помочь сделать что-то полезное.
Сегодня
Первый раскрывается в строковый литерал, содержащий имя текущего файла (будет это просто имя или полный путь depends). Второй в целочисленный литерал, обозначающий номер строки, в которой макрос был expanded.
Подстановка значений происходит в месте использования, а не определения использующего макроса.
Например, можно сделать ассерт:
Сегодня для этих целей можно использовать
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 17.
С уровнем 2 мы закончили. Делаем шаг в уровень 3.
Есть несколько «служебных» макросов, которые могут помочь сделать что-то полезное.
Сегодня
__FILE__ и __LINE__.Первый раскрывается в строковый литерал, содержащий имя текущего файла (будет это просто имя или полный путь depends). Второй в целочисленный литерал, обозначающий номер строки, в которой макрос был expanded.
Подстановка значений происходит в месте использования, а не определения использующего макроса.
Например, можно сделать ассерт:
#define MY_ASSERT(cond) \
do { \
if (!(cond)) { \
std::cerr << "Assertion failed: " \
<< #cond \
<< " in " << __FILE__ \
<< ":" << __LINE__ << '\n'; \
std::abort(); \
} \
} while (0)
Сегодня для этих целей можно использовать
std::source_location. @thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍10❤4🗿2
#cpp
Day 18.
Аналогично есть
Первый раскрывается в строковый литерал, содержащий дату компиляции. Второй содержит время компиляции.
Можно использовать для версионирования ваших бинарников. Чтобы удобнее понимать, что сейчас запущено (та ли версия собралась, что работает у юзера, обновился ли бинарник).
Можно использовать как источник какой-то энтропии (да, есть более подходящие инструменты, но мы пытаемся оправдать инструменты препроцессора):
Проблемы тут понятны.
У вас ломаются reproducible builds.
При неаккуратном использовании заодно и инкрементальные билды (если у вас один из этих макросов где-то в корневом хедере, который пролезает транзитивно в большую часть проекта).
У
Иногда компиляторы ещё дают
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 18.
Аналогично есть
__DATE__ и __TIME__.Первый раскрывается в строковый литерал, содержащий дату компиляции. Второй содержит время компиляции.
Можно использовать для версионирования ваших бинарников. Чтобы удобнее понимать, что сейчас запущено (та ли версия собралась, что работает у юзера, обновился ли бинарник).
Можно использовать как источник какой-то энтропии (да, есть более подходящие инструменты, но мы пытаемся оправдать инструменты препроцессора):
static const char* kId = __TIME__;
Проблемы тут понятны.
У вас ломаются reproducible builds.
При неаккуратном использовании заодно и инкрементальные билды (если у вас один из этих макросов где-то в корневом хедере, который пролезает транзитивно в большую часть проекта).
__TIME__ говорит время компиляции конкретного translation unit, так что в большом проекте вы можете получить разные значения в разных TU.У
__DATE__ (согласно стандарту C99) всегда фиксированный формат: "Mmm dd yyyy". Причём, если день <10, то между месяцем и днём не 1 пробел, а 2. Так длина константы всегда одинаковая. Но это вполне легко может сломать парсинг. Иногда компиляторы ещё дают
__TIMESTAMP__ — дата модификации файла. Там прям и дата, и время может быть: "Sat May 23 11:30:00 2026".@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍13❤4⚡2🤪2
#cpp
Day 19.
Возвращаясь к
Вы можете изменить их значения с помощью
Результат будет:
Или можно только строку переопределить:
Круто. Но зачем?
Представьте, что вы генерируете код. И ваш сгенерированный файл имеет название generated_228228.cpp. Если вы начнёте выдавать юзеру ошибки, основанный на
Потому в начале файла вы можете воткнуть:
Что возволяет вам ссылаться на оригинальный источник.
#line кстати влияет не только на макросы, но и на
Насколько я понимаю (что может быть неправдой), компилятор сам активно пользуется подобным приёмом. Вы же когда подключаете инсклуд, вы фактически получаете один огромный cpp файл. Но в нём при этом все вызове
и скомпилируем
Вот это
Скажу ли я что-то про
Нет. Ведь это не часть препроцессора, а скорее «implicit» переменные. Так что на самостоятельное изучение.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
Day 19.
Возвращаясь к
__FILE__ и __LINE__. Вы можете изменить их значения с помощью
#line:
#line 123 "name.cpp"
int main()
{
std::cout << __FILE__ << ' ' << __LINE__ << std::endl;
std::cout << __FILE__ << ' ' << __LINE__ << std::endl;
std::cout << __FILE__ << ' ' << __LINE__ << std::endl;
}
Результат будет:
name.cpp 126
name.cpp 127
name.cpp 129
Или можно только строку переопределить:
#line 123
Круто. Но зачем?
Представьте, что вы генерируете код. И ваш сгенерированный файл имеет название generated_228228.cpp. Если вы начнёте выдавать юзеру ошибки, основанный на
__FILE__/__LINE__ в этом файле, юзер будет в замешательстве. Он-то ни про какой generated_228228.cpp не в курсе. Потому в начале файла вы можете воткнуть:
#line 1 "query.sql"
Что возволяет вам ссылаться на оригинальный источник.
#line кстати влияет не только на макросы, но и на
std::source_location. Насколько я понимаю (что может быть неправдой), компилятор сам активно пользуется подобным приёмом. Вы же когда подключаете инсклуд, вы фактически получаете один огромный cpp файл. Но в нём при этом все вызове
__FILE__, __LINE__ и других связанных штук работают как будто находятся в разных файлах. Давайте возьмём Hello world:
#include <iostream>
int main() {
std::cout << "Hello, thisnotes!";
}
и скомпилируем
clang -E main.cpp. Мы получим какое-то полотно (от iostream), а в конце будет:
// полотно
# 2 "main.cpp" 2
int main() {
std::cout << "Hello, thisnotes!";
}
Вот это
# 2 "main.cpp" 2 — расширенная версия #line у компилятора. То есть он кроме вставки инклудом файла ещё и добавляет в нужное место #line-like команду, подправляющую текущие значения строк и имён файлов. Хотя файл у вас в итоге всего один. Скажу ли я что-то про
__FUNCTION__, __PRETTY_FUNCTION__ и __func__? Нет. Ведь это не часть препроцессора, а скорее «implicit» переменные. Так что на самостоятельное изучение.
@thisnotes. Patreon, Boosty.
Спасибо Artyom Garkavy и niki4smirn.
👍14❤7🔥1