Я уже писал (qoto.org/@noxdev/1106438943324) про то, что хорошей практикой при разборе какой-то проблемы одним из удачных, на мой взгляд, подходов, является написание сэмпла, который локализованного воспроизводит проблему.
Хочется еще добавить про необходимость подготовки вспомогательных утилит и/или инструментов. Например, нечто, что автоматизирует и ускорит анализ воспроизведений проблемы: скрипт с запросами, парсинг логов, сложная команда в терминале и т.п. Довольно часто такие вспомогательные инструменты являются одноразовыми, для конкретного кейса, и у разработчика отсутствует желание вкладывать время и усилия в их подготовку - хочется поскорее закончить с проблемой и забыть её.
У меня например есть большая подборка "заготовок" различных команд для терминала (поиск файлов по фильтрам, анализ логов по регуляркам, анализ бинарных файлов, отправка запросов), из которых можно быстро "слепить" некоторую автоматизацию для уже конкретного кейса. Выглядит как очень такое быстрое "программирование" одноразовой автоматизации.
Главное что в сложных и нетривиальных кейсах такой подход с предварительной подготовкой инструментария экономит кучу времени и позволяет эффективно работать и концентрироваться на проблеме.

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

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

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

Один из недооцененных знаний, необходимых разработчику - это знание английского алфавита!

В vim обычно я использую сочетания клавиш Ctrl + d и Ctrl + u, в NORMAL режиме, чтобы скроллить текст, перемещая курсор на половину экрана вниз и вверх соответственно. В целом это удобно при изучении содержимого файла, т.е. когда нет точного понимания в какую строку пеместиться или какое сочетание символов интересно.

Недавно узнал про сочетания клавиш Ctrl + e и Ctrl - y, в NORMAL режиме, которые перемещают экран на одну вниз и вверх, соответственно, при этом позиция курсора остаётся неизменной. Есть ощущение, что эти сочетания клавиш очень "прижились" у меня и крайне удобны.
Довольно часто курсор отказывается внизу экрана и я выполняю либо Ctrl + e, чтобы строку с ним переместить ближе к центру экрана, либо же вообще выполняю команду zz и тогда строка с курсором будет в центре экрана.

Уже не в первый раз вижу ситуацию, когда разработчик из-за слабых soft skills вынужденно перерабатывает и, как результат, страдает из-за этого.

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

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

Исследуем внутреннюю структуру std::vector с помощью отладчика GDB.

Возьмём следующий тривиальный код:
1 <vector>
2
3 int main()
4 {
5 std::vector<int> vec = {10, 20, 30, 40, 50};
6 int size = vec.size();
7 int capacity = vec.capacity();
8 vec.push_back(60);
9 size = vec.size();
10 capacity = vec.capacity();
11 return 0;
12 }
13

Соберём его с помощью gcc под Ubuntu 20.04:
g++ -std=c++17 -g -O0 example.cpp -o example

Далее запускаем получившийся исполняемый файл под отладчиком GDB:
$ gdb ./example

Устанавливаем breakpoint'ы на 8-ой и 11-ой строках кода:
>>> break 8
Breakpoint 1 at 0x12b4: file example.cpp, line 8.
>>> break 11
Breakpoint 2 at 0x12ec: file example.cpp, line 11.

Запускаем выполнение программы:
>>> run

Отладчик остановится на 8-ой строке vec.push_back(60), как и планировалось.
Выведем значения переменных size и capacity, с помощью команды print в GDB:
>>> print size
$1 = 5
>>> print capacity
$2 = 5

Количество элементов в векторе и ёмкость вектора (размер внутреннего буфера равны) 5. Соответственно, добавление еще одного элемента приведёт к тому, что нужно будет увеличить ёмкость вектора, выделив новый внутренний буфер бОльшего размера, переместить туда уже добавленные элементы и добавить новый элемент.

Получим значение размера переменной vec и адрес на стеке, в котором она хранится.
>>> print sizeof(vec)
$3 = 24
>>> print &vec
$4 = (std::vector<int, std::allocator<int> > *) 0x7fffffffddd0

Видно, что размер вектора 24 байта и располагается он по адресу 0x7fffffffddd0. Исследуем эти 24 байта с помощью команды eXamine в GDB, выведя содержимое 3-х блоков памяти по 8 байт (24 == 8 * 3), начиная от адреса вектора:
>>> x/3xg &vec
0x7fffffffddd0: 0x000055555556ceb0 0x000055555556cec4
0x7fffffffdde0: 0x000055555556cec4

В команде x/3xg число 3 означает интересующее количество блоков (unit'ов) в памяти, x - выводить содержимое памяти в 16-ичной системе счисления, а g - размер блока (в данном случае 8 байт). Если мы хотим вывести содержимое памяти по 4 байта, то вместо g используем w для определения размера блока, но при этом увеличиваем в 2 раза количество выводимых блоков до 6:

>>> x/6xw &vec
0x7fffffffddd0: 0x5556ceb0 0x00005555 0x5556cec4 0x00005555
0x7fffffffdde0: 0x5556cec4 0x00005555

В данном выводе стоит отметить обратный порядок хранения байт little-endian (от младшего к старшему), в данном случае на Intel'овской машине.

Вернёмся к выводу команды x/3xg &vec:
0x7fffffffddd0: 0x000055555556ceb0 0x000055555556cec4
0x7fffffffdde0: 0x000055555556cec4

Видно что в блока памяти хранится 3 адреса: 0x000055555556ceb0, 0x000055555556cec4 и 0x000055555556cec4. Адрес 0x000055555556cec4 повторяется дважды.

Одна из возможных реализаций std::vector'а основана на хранении 3-х указателей, содержащих:
- адрес на начало внутреннего буфера;
- адрес во внутреннем буфере куда будет добавлен следующий элемент;
- адрес конца внутреннего буфера.

Если посчитать разницу между адресами 0x000055555556cec4 и 0x000055555556ceb0:
>>> print 0x000055555556cec4 - 0x000055555556ceb0
$5 = 20

будет получено значение 20 байт. 20 / 4 = 5 элементов типа int хранится в векторе.

В первом блоке памяти хранится адрес на начало внутреннего буфера.
По значениям size и capacity, выведенных ранее, видно что количество элементов в векторе (size) и размер внутреннего буфера (capacity) одинаковы и равны 5. Это и обуславливает тот факт, что во 2-ом и 3-ем блоке памяти одинаковые указатели.

Выведем 5 блоков памяти, начиная с адреса 0x000055555556ceb0, по 4 байта (т.к. в векторе хранятся int'ы):
>>> x/5xw 0x000055555556ceb0
0x55555556ceb0: 0x0000000a 0x00000014 0x0000001e 0x00000028
0x55555556cec0: 0x00000032

Как результат получили содержимое внутреннего буфера вектора. Выведем его значения в десятичной системе счисления, заменив x на d в команде x:
>>> x/5dw 0x000055555556ceb0
0x55555556ceb0: 10 20 30 40
0x55555556cec0: 50

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

Выполним в GDB команду continue, которая исполнит код до следующего breakpoint'а. Будет выполнено добавления еще одного элемента в вектор (vec.push_back(60)).

Выведем значения переменных size и capacity.

>>> print size
$6 = 6
>>> print capacity
$7 = 10

В этот раз они содержат разные значения. Количество элементов в векторе стало 6, а вот размер внутреннего буфера стал равен 10.
Снова выведем содержимое блоков памяти вектора:

>>> x/3xg &vec
0x7fffffffddd0: 0x000055555556ced0 0x000055555556cee8
0x7fffffffdde0: 0x000055555556cef8

Видно, что значения адресов изменились.

Выведем 6 блоков памяти, начиная с адреса 0x000055555556ced0

>>> x/6dw 0x000055555556bed0
0x55555556bed0: 10 20 30 40
0x55555556bee0: 50 60

Добавленный элемент равен 60.

Посчитаем разницу между 1-ым и 2-ым блоком памяти:

>>> print 0x000055555556cee8 - 0x000055555556ced0
$8 = 24

Значение в 24 байт соответствует 6 элементам типа int.

Посчитаем разницу между 1-ым и 3-им блоком памяти:

>>> print 0x000055555556cef8 - 0x000055555556ced0
$9 = 40

Значение в 40 байт соответствует 10 элементам типа int.

Соответственно эти данные доказывают следующее:
- во 2-ом указателе вектора хранится адрес во внутреннем буфере куда будет добавлен следующий элемент и этот адрес используется для расчёт количества элементов в вектора (size);
- в 3-ем указателе вектора хранится адрес конца внутреннего буфера и этот адрес используется для расчёта ёмкости вектора (capacity).

Как итог, посмотрели с помощью отладчика GDB внутреннее устройство std::vector и как оно меняется при изменении данного контейнера.


Получить последнее слово из выполненной команды в bash можно с помощью !$.

Стал довольно часто эту эту возможность использовать в комбинации mkdir и cd (создать директории и перейти в неё:

$ mkdir -pv path/to/dir
$ cd !$
cd path/to/dir
$

Команда cd будет выполнена с опцией path/to/dir.

Раньше я в сдвигал блок текста в основном двумя способами:
1) В режиме VISUAL выделял строки (V и несколько раз j) и нажимал >
2) В режиме NORMAL командой [n]>>, где [n] - количество строк в блоке текста для сдвига.

Недавно узнал про еще одну команду в NORMAL режиме:
>[n]G
где [n] - номер строки, до которой (включительно) произвести сдвиг блока текста.

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

Давно хотел в плюсах посмотреть на проблему при возвращении const'ной ссылки из метода/функции в C++. Тема старая, но всё никак руки не доходили.
Есть следующий код (очередной искусственный пример для демонстрационных целей):

<iostream>
<string>

<cstdlib>

const std::string& takeString(int value, const std::string& str1, const std::string& str2, const std::string& str3)
{
if (value < 0)
{
return str1;
}
if (value > 0)
{
return str2;
}
return str3;
}

int main(int argc, char* argv[])
{
if (argc != 2)
{
std::exit(1);
}

int value = std::atoi(argv[1]);
std::string s1 = "one";
std::string s2 = "two";
const std::string& s = takeString(value, s1, s2, "three");
std::cout << "s: " << s << "\n";

return 0;
}

Компилируем и выполняем с разными опциями:
$ ./example -10
s: one

$ ./example 10
s: two

Если же вызвать с опцией 0:
$ ./example 0

результат будет неопределён (конкретно у меня был выведен "мусор" на экран). Если скомпилировать программу с адрес санитайзером (-fsanitize=address), то при вызове example с опцией 0 получим следующую ошибку:
ERROR: AddressSanitizer: stack-use-after-scope

В чём суть проблемы? При внимательном рассмотрении кода видно, что вызов функции takeString на самом деле будет следующим:

const std::string& s = takeString(value, s1, s2, std::string("three"));

т.е. при передаче третьим аргуметом литерала "three" будет сконструирован временный объект типа std::string, который разрушится перед выходом из функции takeString, что логично. Продление времени жизни объекта за счет const std::string& s здесь не будет выполняться.
В результате, когда функция takeString возвращает str3, будет получена висячая ссылка. В случае, когда вызов takeString возвращает s1 или s2 никаких проблем нет, т.к. они разрушатся при выходе из main, их можно спокойно использовать после вызова takeString (в std::cout).

Какой вывод нужно сделать? Не нужно возвращать из функции/метода по константной ссылке параметр типа константная ссылка, да и любой другой временный объект, например:

const int& badFunction()
{
int value = 42;
return value;
}

Правда тут мне компилятор выдал предупреждение:
warning: reference to stack memory associated with local variable 'value' returned [-Wreturn-stack-address]

Хочу поделиться опытом изучения макросов в Vim. Попробовал использовать их в реальной работе и на определённых задачах вижу как они здорово ускоряют работу.

Покажу на упрощенном примере. Есть следующий код (очень синтетический, чисто для примера):
1 struct Record
2 {
3 int position{0};
4 int count{0};
5 };
6
7 struct AdditionalInfo
8 {
9 int weight{0};
10 bool visible{false};
11 };
12
13 struct RecordEx
14 {
15 Record record;
16 AdditionalInfo additionalInfo;
17 };
18
19 void ProcessRecord(const Record& record, const AdditionalInfo& info)
20 {
21 // Processing record
22 }
23
24 void ProcessRecordEx(const RecordEx& record)
25 {
26 // Processing record with additional info
27 }
28 RecordEx CreateRecordWithAdditionalInfo(int position, int count, int weight, bool visible)
29 {
30 return RecordEx{position, count, weight, visible};
31 }
32
33
34 int main()
35 {
36 // records from 1st source
37 ProcessRecord(Record{20, 30}, AdditionalInfo{900, false});
38 ProcessRecord(Record{100, 50}, AdditionalInfo{100, true});
39 ProcessRecord(Record{200, 21}, AdditionalInfo{800, false});
40
41 // records from 2nd source
42 ProcessRecord(Record{901, 2}, AdditionalInfo{750, false});
43 ProcessRecord(Record{905, 5}, AdditionalInfo{25, true});
44 ProcessRecord(Record{991, 12}, AdditionalInfo{15, true});
45
46 return 0;
47 }

Задача: заменить вызов ProcessRecord(Record{...}, AdditionalInfo{...}) на ProcessRecordEx(CreateRecordWithAdditionalInfo(...)).

Общая идея: в макрос происходит запись редактирования одной строки, а далее в VISUAL режиме для каждой строки выделенного текста применяются действия, записанные в данном макросе. При записи макроса значения аргументов position, count, weight и visible будут записаны в регистры a, b, c, d соответственно, а далее они будут использованы в новым вызове.

Редактирование начинается с вызова команды qr, осуществляющая запись макроса в регистр r.
Внизу появится вот такое сообщение:
recording @r
Далее происходит вызов следующих команд:
0 - переход в начало строки
/ProcessRecord<cr> - происк текста ProcessRecord и перемещение к нему
<C-a> - перемещение курсора к ближайшему числу справа и увеличение его на 1, далее см. команду ниже
<C-x> - уменьшение числа на 1, выполняется чтобы число осталось тем же самым, какие было до вызова <C-a> (т.е. вызов двух команд <C-a><C-x> нужен чтобы переместиться к числу в строке)
wb - перемещение в начало числа, т.к. команды выше установят курсор на конец числа
"ayw - запись значения слова под курсором в регистр a
W - перемещение к следующему слову (это будет второе число в строке)
"byw - запись значения слова под курсором в регистр b
f, - перемещение к ближайшей запятой справа
<C-a><C-x> - уже известная пара команд, которая осуществит переход к третьему числу
wb - перемещение в начало числа
"cyw - запись значения слова под курсором в регистр c
W - перемещение к следующему слову (четвёртный аругмент типа bool)
"dyw - запись значения слова под курсором в регистр d
S - удаление все строки и переход в режим INSERT, внизу появится следующая -- INSERT --recording @r
Вводится текст:
ProcessRecordEx(CreateRecordWithAdditionalInfo(
новый вызов функции (можно использовать комплишн <C-n>)
<C-r>a - вставка значения регистра a в режиме INSERT
, - вставка запятой и пробела
<C-r>b - вставка значения регистра b в режиме INSERT
, - вставка запятой и пробела
<C-r>c - вставка значения регистра c в режиме INSERT
, - вставка запятой и пробела
<C-r>d - вставка значения регистра d в режиме INSERT
)); - вставка двух закрывающих скобок и ;
q - завершить запись макроса

Далее выполняется выделение блока текст, с помощью вызова команда V и перемещения курсора вниз, используя j
и происходит выполенеие команды
:'<,'>normal @r
которая для выделенных строк применит действия хранящиеся в макросе r.
Итоговый результат выглядит следующим образом:

...
// records from 1st source
ProcessRecordEx(CreateRecordWithAdditionalInfo(20, 30, 900, false));
ProcessRecordEx(CreateRecordWithAdditionalInfo(100, 50, 100, true));
ProcessRecordEx(CreateRecordWithAdditionalInfo(200, 21, 800, false));

// records from 2nd source
ProcessRecordEx(CreateRecordWithAdditionalInfo(901, 2, 750, false));
ProcessRecordEx(CreateRecordWithAdditionalInfo(905, 5, 25, true));
ProcessRecordEx(CreateRecordWithAdditionalInfo(991, 12, 15, true));
...

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

tips

Как переместиться в начало слова в режиме Normal, если курсор находится на одном из символов слова?

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

Можно воспользоваться командой wb, которая независимо от количества символов переместит в начало слова ("вперёд на одно слово", "назад на одно слово"). Если курсор находится на первом символе слова он остается на той же позиции.

Есть у меня такая хорошая привычка. Если к вечеру готов pull request по задаче, то я его не публикую до утра. Утром на свежую голову "пробегаюсь" по сделанным изменения и, как правило, 1-2 исправления выполняю.

Постепенно добираюсь до C++20. Понравилась возможность использования template parameters в лямбдах, которая явно улучшает читаемость кода.

Пример:
<iostream>
<vector>

int main()
{
std::vector<int> intItems = { 9, 10, -7, 11 };
std::vector<float> floatItems = { 1.0f, 9.0f, 5.01f, 99.12f };
std::vector<double> doubleItems;
int counter{};

auto processItem = [&counter]<typename T>(const std::vector<T>& items) {
std::cout << "================\n";
auto it = std::max_element(items.begin(), items.end());
if (it != items.end())
{
std::cout << "Max value: " << *it << "\n";
}
else
{
std::cout << "No items\n";
}
std::cout << "================\n";
++counter;
};

processItem(intItems);
processItem(floatItems);
processItem(doubleItems);
std::cout << "Processed " << counter << " items\n";
return 0;
}

Результат исполнения:
================
Max value: 11
================
================
Max value: 99.12
================
================
No items
================
Processed 3 items

До C++20 вынужден использовать generic lambda следующим образом:
...
auto processItem = [&counter](const auto& items) {
...
}
...

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

Данные команды могу вставить текст из регистра:
- ниже (команда p) или выше (команда P) текущей строки, если в регистр, из которого производится вставка, была скопирована строка целиком, например, с помощью команды yy;
- слева (команда P) или справа (команда p) от символа под курсором, если в регистр, из которого производится вставка, был скопирован набор символов, например, с помощью команды yw (копирование слова);

Как-то всё обходил меня стороной возможный рекурсивный вызов лямбды в C++, благодаря generic lambda expression (который давно уже появился, начиная с C++14):

<iostream>
<string>

int main()
{
auto print_star = [](int n, const auto& f) -> void {
std::cout << std::string(n, '*') << std::endl;
if (n-- > 1)
{
f(n, f);
}
};
print_star(5, print_star);
return 0;
}

Вывод:
*****
****
***
**
*

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

Расширил набор команд, которыми я удаляю текст в

Раньше в основном использовал две команды:
dd - удаление строчки, на которой находится курсор
x - удаление символа, на котором находится курсор

Сейчас набор используемых команд расширился следующими:
1) daw - удалить слово вместе с пробелами.
Строка:
One two three four
Курсор находится на одном из символов слова two или на одном из пробелов между словом One и two. Команда daw отредактирует строку следующим образом:
One three four

2) dt<символ> - удалить текст в строке, начиная от символа под курсором до первого найденного <символ> справа.
Строка:
a = b * c * (d + e)
Курсор находится на символе b. Команда dt( отредактирует строку следующим образом:
a = (d + e)

3) d<номер строки>G - удалить строки, начиная от той на которой находится курсор до <номер строки>, включая ее.
Текст:
1 0000000
2 1111111
3 2222222
4 3333333
5 4444444
6 5555555
7 6666666
8 7777777
9 8888888
10 9999999
Курсор находится на строке номер 5 с содержимым 4444444. Команда d9G отредактирует текст следующим образом:
1 0000000
2 1111111
3 2222222
4 3333333
5 9999999

По ощущениям почувствовал небольшой прирост эффективности при работе в

tips:
Довольно часто из терминала вызываю git diff с фильтром по файлам. Например, мне нужно показать только CMakeLists.txt's, которые модифицированы:

$ git diff -- "*CMakeLists.txt"

Show more
Qoto Mastodon

QOTO: Question Others to Teach Ourselves
An inclusive, Academic Freedom, instance
All cultures welcome.
Hate speech and harassment strictly forbidden.