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

Tags: #common, #cpp, #highload и другие можно найти поиском.
Задачки: #poll.
Мои публикации: #pub.
Автор и предложка: @vanyakhodor.
GitHub: dasfex.
Download Telegram
#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.
14👍11🔥2
#cpp

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.
👍326❤‍🔥3🔥3
#cpp

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.
👍172
#cpp

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.
👍123🌚3
#cpp

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.
🔥84👍3🤯3🤬3👎1😁1🤔1
#cpp

Day 14.

Бывает, что вы наусловнокомпилировали чего-то. Ифдефами обмазались. И не можете покрыть все-все случаи, которые у вас возникают.

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

Или может хотите юзеру сообщить, что он делает что-то странное (например, пытается использовать стандарт X на платформе Y).

Что делать?

Выдать понятное сообщение!

В препроцессорном мире вам поможет #error:

#error "You did something wrong. Drink beer."


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

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🗿21
#cpp

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.

Есть несколько «служебных» макросов, которые могут помочь сделать что-то полезное.

Сегодня __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.
👍104🗿2
#cpp

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.
👍1342🤪2
#cpp

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.
👍147🔥1
#cpp

Day 20.

В C и C++ есть альтернативные варианты записи некоторых операторов. Например, && можно записать как and. Или != можно заменить на not_eq. Такие альтернативы появились в силу ограниченности некоторых клавиатур, в которых иметь символ & (и другие) было не всегда возможно.

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

if (a and b)
// over
if (a && b)

Это всё субъективно конечно.

Есть ещё альтернативные токены для записи скобок [] {} и # ##.

Есть ещё триграфы: ещё одна альтернатива с чуть более широким набором символов (deprecated в C23).

Ваша программа могла выглядеть так:

%:include <stdio.h>
%:include <stdlib.h>
??=include <iso646.h>

int main(int argc, char** argv)
??<
if (argc > 1 and argv<:1:> not_eq NULL)
<%
printf("Hello %s??/n", argv<:1:>);
%>
else
<%
printf("Hello %s??/n", argc? argv??(42??'42??) : __FILE__);
%>

return EXIT_SUCCESS;
??>

Мы так лабы сдавали в универе.

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

"What's going on??!"

вы можете получить

"What's going on|"


Проблемес.

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

Day 21.

Вернёмся к __VA_ARGS__. Мы смотрели на такой макрос:

#define LOG(fmt, ...) print(fmt, __VA_ARGS__)

И когда мы используем его с одним аргументом:

LOG("str");

мы получим print("fmt", ), что вообще-то невалидно, т.к. одна запятая без аргумента сзади это CE.

Это решали расширением компиляторов:

#define LOG(fmt, ...) print(fmt, ##__VA_ARGS__)

То есть оно как бы работает, но не везде одинаково, есть проблемы с разными опциями компиляции.

Стандартное решение (с C++20) — использование __VA_OPT__:

#define LOG(fmt, ...) print(fmt __VA_OPT__(,) __VA_ARGS__)


__VA_OPT__ — это такая приколюха, которая оставляет аргумент, если __VA_ARGS__ в том же макросе не пустые. Если пустые (что? да), не оставляет.

Совать в аргумент можно не только запятую:

#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })

SDEF(foo); // replaced by S foo;
SDEF(bar, 1, 2); // replaced by S bar = { 1, 2 };

Хотя с __VA_ARGS__ внутри __VA_OPT__ надо быть аккуратными.

Есть классическая статья David Mazières из Stanford про то, как можно абюзить __VA_OPT__ для создания рекурсивных макросов: https://www.scs.stanford.edu/~dm/blog/va-opt.html

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

Day 22.

Мы с вами уже говорили про #pragma once. Но прагмы бывают разные. Это фактически implementation defined способ сообщить компилятору, что надо сделать.

Но коммуницировать бывает сложно.

Особенно если я пытаюсь генерировать прагму макросами:

#define DISABLE_WARNINGS #pragma GCC diagnostic ignored "-Wall"

Такой синтаксис ломается из-за второго диеза (#).

Поэтому есть альтернатива: _Pragma():

#define DISABLE_WARNINGs _Pragma("GCC diagnostic ignored \"-Wall\"")

#define DO_PRAGMA(x) _Pragma(#x)

DO_PRAGMA(GCC diagnostic ignored "-Wunused-variable")

Последнее как раз эквивалентно:

#pragma GCC diagnostic ignored "-Wunused-variable"


Аргумент — обязательно строковый литерал.

Прагмамируйте на здоровье (смешно?).

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

Day 23.

Мы уже пользовались, но не оговаривали.

У многих компиляторов есть нестандартные флаги компиляции.

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

Если помните, когда мы смотрели на #line, в результате компиляции файла с -E мы видели маркеры строк. Это которые меняли значения __LINE__ и __FILE__. Вот их можно убрать, если к -E добавить -P.

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

Day 24.

Делам шаг на уровень 4.

Большинство компиляторов дают возможность использовать __COUNTER__ (расширение компиляторов, не стандарт).

В первый вызов он отдаёт 0, а далее значение на 1 больше.

Счётчик один внутри translation unit.

Если помните, мы генерировали уникальные имена для переменных с помощью LINE, но с ним проблема в том, что вы не можете объявить две переменные в одной строке, так как у обеих будет одно и то же имя. COUNTER решает эту проблему. Как его ни вызывай, он всё равно вверх растёт:


#define CONCAT_IMPL(a, b) a##b
#define CONCAT(a, b) CONCAT_IMPL(a, b)

#define UNIQUE_NAME(prefix) CONCAT(prefix, __COUNTER__)

int UNIQUE_NAME(x), UNIQUE_NAME(x);

Получим что-то вроде

int x0, x1;

Зачем нужны вообще эти уникальные имена? Например, для defer/finally/scope_exit:

#define SCOPE_EXIT(code) \
auto UNIQUE_NAME(_scope_) = make_scope_exit([&] { code; });


Вроде очевидно, но можно и такое сделать:

#define FOO(x) x + x

FOO(__COUNTER__); // 0 + 1

Всякими хитрыми трюками можно его «сбрасывать», но про это почекайте сами.

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

Day 25.

Мы это краем уха задевали, но давайте ещё раз проговорим.

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

Базовая рекомендация: приписывать в начало макроса имя библиотеки: THISNOTESLIB_.

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

Day 26.

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

#define CONCAT_IMPL(a, b) a##b
#define CONCAT(a, b) CONCAT_IMPL(a, b)


Причём важно иметь прокси реализацию, так как тогда ваш макрос:

#define CONCAT_IMPL(a, b) a##b

будет работать не так, как ожидается:

#define foo FOO
#define bar BAR
#define FOOBAR ~

CONCAT_IMPL(foo,bar) // foobar
CONCAT(foo,bar) // ~


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

Day 27.

В C23 подъехали новые директивы #elifdef #elifndef.

Зачем они нужны?

Чтобы вот такой код:

#ifdef __clang__
#define COMPILER "clang"
#elif defined(__GNUC__)
#define COMPILER "gcc"
#elif defined(_MSC_VER)
#define COMPILER "msvc"
#else
#define COMPILER "unknown"
#endif

выглядел вот так:

#ifdef __clang__
#define COMPILER "clang"
#elifdef __GNUC__
#define COMPILER "gcc"
#elifdef _MSC_VER
#define COMPILER "msvc"
#else
#define COMPILER "unknown"
#endif

Фактически пытаемся снизить шум от defined(...).

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

Day 28.

Давайте представим, что вы разрабатываете какое-нибудь десктопное приложение. У вас есть примерно такой набор файлов:

app.exe
logo.png
config.json
shader.spv

В коде вы работаете с файлами примерно так:

std::ifstream f("config.json");

Какие проблемы? Я вижу такие:
• файл могут забыть положить рядом
• файл могут удалить
• файл могут подменить
• распространять программу неудобно.

В идеале получить просто app.exe.

Достичь этого мы можем с помощью встраивания байтов прямо в исполняемый файл.

Есть несколько вариантов, как это сделать.
Например

xxd -i bytes.bin > bytes.h

С помощью утилиты xxd и флага -i (указывающего сгенерировать C-код). И далее простой #include "bytes.h".

Другие способы:
• objcopy
• inline asm с .incbin
• генерация кода из бинарника любыми другими способами.

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

Все от этих костылей устали и придумали #embed (C23):

constexpr unsigned char data[] = {
#embed "file.bin"
};

Всё. Одна директива. Изменения не теряем.

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