Русский
Русский
English
Статистика
Реклама

С++

С23 международный стандарт на удалёнке

08.12.2020 10:15:01 | Автор: admin


C++20 прошёл все бюрократические инстанции и теперь официально готов! Международный комитет переехал в онлайн, и теперь мы вовсю работаем над C++23. Под катом вас ждут:

  • std::stacktrace,
  • z и uz,
  • 61 с половиной багфикс в ядре языка,
  • string::contains,
  • Executors & Networking,
  • и прочие новости.

std::stacktrace


Ура! РГ21 вместе с Fails протащили в стандарт std::stacktrace. Скоро можно будет делать программы с хорошей диагностикой:

#include <stacktrace>// ...std::cout << std::stacktrace::current();

И получать читаемый трейс:

0# bar(int) at /path/to/source/file.cpp:701# bar(int) at /path/to/source/file.cpp:702# bar(int) at /path/to/source/file.cpp:703# bar(int) at /path/to/source/file.cpp:704# main at /path/to/main.cpp:935# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.66# _start

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

Библиотека приехала в стандарт C++ из Boost, но есть отличие в поведении: конструктор по умолчанию для std::stacktrace создаёт пустой объект, а не захватывает trace текущего потока, как это делает boost::stacktrace. Описание интерфейса и особенности дизайна класса доступны в P0881R7.

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

Z и UZ


Если вы не любите печатать std::size_t или std::ssize_t, то вам приглянутся суффиксы C++2b, uz и z соответственно:

for (auto i = 0uz; i < vec.size(); ++i) {      assert(i == vec[i].size());}

Должен признать, что это нововведение из P0330R8 меня не особо будоражит Так что перейдём к следующей новинке.

Большой рефакторинг стандарта


К C++20 успели отрефакторить описание стандартной библиотеки, явно разделив static_assert, SFINAE и рантайм условия для классов и функций. К C++23 подгруппа ядра языка решилась переработать часть стандарта, отвечающую за декларации и их поиск.

В итоге закрыли 61 ошибку в ядре языка, частично решили ещё четыре проблемы. Любители артхаусной литературы могут оценить новый слог в P1787R6.

string::contains


Долой str.find(str2) != std::string_view::npos! Теперь, благодаря P1679R3, можно писать более вменяемый код str.contains(str2). Лично меня весьма радует, что с C++20 люди в комитете активно работают над красотой и лаконичностью языка, отодвинув концепцию итераторов и позиций на второй план:
Было Стало
str.substr(0, str2.size()) == str2
str.starts_with(str2);
str.size() >= str2.size() && !str.compare(    str.size() - str2.size(),    std::string_view::npos,    str2)
str.ends_with(str2);
std::sort(std::begin(d), std::end(d));
std::ranges::sort(d);

Executors & Networking


Увы, но Executors всё ещё не приняли в стандарт.

Однако комитет сдвинулся с мёртвой точки! Интерфейс практически устаканился, P0443 почти готов к принятию. Появилось множество реализаций предложенного интерфейса:


Работа над Networking идёт одновременно с Executors, постоянно вносятся небольшие правки. Networking продолжает зависеть от Executors, есть шанс, что успеют сделать обе вещи к C++23.

Прочие новости


В ближайшие полтора года международный комитет планирует работать удалённо. Планы на C++2b остаются в силе выпустить новый стандарт в 2023 году.

Конференции по C++ тоже продолжаются в онлайн-формате, ближайшая С++ meetup Moscow #11 в Технопарке Сколково. Следите за новостями и анонсами в канале t.me/ProCxx.

Напоследок ещё немного приятных новостей: в рамках подготовки к выпуску userver мы начали актвнее апстримить наши таксишные наработки в смежные проекты. segoon уже успел заапстримить в clang-tidy-проверку на безопасность функции для многопоточных приложений. Надеюсь, вам пригодится!
Подробнее..

Чему равно выражение -33u3 на С? Не угадаете. Ответ -4. Приглашаю на небольшое расследование

04.05.2021 18:23:50 | Автор: admin

Вот пример для проверки:

#include <iostream>int main(){    std::cout << "-3/3u*3 = " << int(-3/3u*3) << "\n";}

Посмотреть результат можно тут.

Или попробуйте поиграться с этим примером здесь или здесь.

Вообще-то мне не удалось найти хоть какой-то компилятор С++, который бы выдавал результат отличный от -4. Даже старый GCC-4.1.2, clang-3.0.0 или Borland C 1992 года. Так же заметил, что результат одинаковый и для константы, вычисляемой в момент компиляции и для времени выполнения.

Предлагаю внимательно рассмотреть результат выражения -3/3u*3.

Если убрать приведение к типу intв примере выше, то получим 4294967292 или 0xFFFFFFFС(-4). Получается, что компилятор на самом деле считает результат беззнаковым и равным 4294967292. До этого момента я был свято уверен, что если в выражении используется знаковый тип, то и результат будет знаковым. Логично же это.

Если посмотреть откуда берется -4 вместо -3, посмотрим внимательней на ассемблерный код примера, например здесь.

Пример изменю, чтобы результат вычислялся не в момент компиляции, а в момент выполнения:

int main(){    volatile unsigned B = 3;    int A = -3/B*3;}

Для x86-64 clang 12.0.0 видим, что используется беззнаковое деление, хотя числитель откровенно отрицательное -3:

        mov     dword ptr [rbp - 4], 3    // B = 3        mov     ecx, dword ptr [rbp - 4]        mov     eax, 4294967293        xor     edx, edx        div     ecx                       // беззнаковое деление !!        imul    eax, eax, 3               // знаковое умножение        mov     dword ptr [rbp - 8], eax

Для x64 msvc v19.28 тот же подход к делению:

        mov     DWORD PTR B$[rsp], 3      // B = 3        mov     eax, DWORD PTR B$[rsp]        mov     DWORD PTR tv64[rsp], eax        xor     edx, edx        mov     eax, -3                             ; fffffffdH        mov     ecx, DWORD PTR tv64[rsp]        div     ecx        imul    eax, eax, 3        mov     DWORD PTR A$[rsp], eax

Получается, что для деления беззнакового числа на знаковое используется БЕЗЗНАКОВАЯ операция деления процессора div. Кстати, следующая команда процессора, это правильное знаковое умножение imul. Ну явный баг компилятора. Банальная логика же подсказывает, что знаковый тип выиграет в приведении типа результата выражения если оба знаковый и беззнаковый типы используются в выражении. И для знакового деления требуется знаковая команда деления процессора idiv, чтоб получить правильный результат со знаком.

Проблема еще и в том, что число 4294967293 не делится на 3 без остатка:4294967293 = 1431655764 * 3 + 1и при умножении 1431655764 обратно на 3, получаем 4294967292 или -4. Так что прикинуться веником и считать, что 4294967293 это то же -3, только вид сбоку, для операции деления не прокатит.

Двоично-дополнительное придставление отрицательных чисел.

Благодаря представлению чисел в двоично-дополнительном виде, операции сложения или вычитания над знаковыми и без-знаковыми числами выполняются одной и той же командой процессора (add для сложения и sub для вычитания). Процессор складывает (или вычитает) только знаковое со знаковым или только без-знаковое с без-знаковым. И для обоих этих операций используется одна команда add (или sub) и побитово результат будет одинаковый (если бы кто-то решил сделать раздельные операции сложения для знаковых и без-знаковых типов). Различие только во флагах процессора. Так что считать знаковое без-знаковым и складывать их оба как без-знаковых корректно и результат будет побитово правильным в обоих случаях. Но для деления и умножения этот подход в корне неправильный. Процессор внутри использует только без-знаковые числа для деления и умножения и результат приводит обратно в знаковое с правильным признаком знака. И для этого процессор использует разные команды для знакового (idiv) и без-знакового деления (div) и так же и для умножения (imul и соответственно mul).

Я когда обнаружил, что используется без-знаковое деление, решил, что это бага компилятора. Протестировал много компиляторов: msvc, gcc, clang. Все показали такой же результат, даже древние трудяги. Но мне довольно быстро подсказали, что это поведение описано и закреплено в самом стандарте.

Действительно, стандарт говорит об этом прямо:

Otherwise, if the unsigned operand's conversion rank is greater or equal to the conversion rank of" "the signed operand, the signed operand is converted to the unsigned operand's type.

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

Вот где оказывается собака зарыта: "the signed operand is converted to the unsigned operand's type"!! Ну почему, почему, Карл!! Логичнее наоборот: "the unsigned operand is converted to the signed operand's type", разумеется при соблюдении ранга преобразования. Ну вот как -3 представить без-знаковым числом?? Наоборот кстати можно.

Интересная получается сегрегация по знаковому признаку!

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

Проверим на ассемблере здесь этот пример:

int main(){    volatile unsigned B = 3;    int C = -3*B;}
Вот ассемблерный код:

mov dword ptr [rbp - 4], 3 mov eax, dword ptr [rbp - 4] imul eax, eax, 4294967293 mov dword ptr [rbp - 8], eax

Стандарт ничего не говорит о неприменимости этого правила для операции умножения. И деление и умножение должны быть БЕЗЗНАКОВМИ.

Я подумал, что что-то в стандарте сломалось и надо срочно это исправить. Написал письмо напрямую в поддержку Стандарта со всеми моими выкладками и предложением поправить это странное правило стандарта пока еще не поздно.

Ага! Наивный!

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

Хоть это исследование и было больше года назад, я до сих пор под впечатлением от многих вещей в этой истории:

  • Как я не натыкался на это раньше? Не один десяток лет интенсивно кодирую на С и С++ с погружением в ассемблер, но только сейчас споткнулся на неё. Хотя может и натыкался ранее, но не мог поверить что причина именно в этом.

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

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

int main(){    const unsigned a[] = {3,4,5,6,7};    unsigned p = (&a[0] - &a[3])/3u*3;    // -3    unsigned b = -3/3u*3;   // -4}

Хоть я и понимаю, что могу ошибаться в логике работы этого мира, но задумайтесь, в следующий раз садясь в современный, нашпигованный вычислительной логикой самолёт (или автомобиль), а не сработает ли вдруг не оттестированный кусок кода в какой-то редкой нештатной ситуации, и не выдаст ли он -4 вместо элементарных -3, и не постигнет ли его участь подобная Boeing 737 MAX?

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

Ошибка в команде просессора FDIV у Интела

Помните, в начале 2000-х была выявлена ошибка с вычислением в команде FDIV у Интела. Там было различие в 5 знаке после запятой в какой-то операции деления. Какой был шум тогда!!
Но исправили оперативно и быстро. В компиляторы добавили условный флаг для обхода этой команды. Интел срочно исправил логику в кристалле и выпустил новые чипы.

И это всего лишь 5-й знак после запятой! Многие его даже и не заметили, подумаешь, мелочь какая! А тут -4 вместо -3 и считаем знаковое без-знаковым и вместо -3 имеем еще и 4294967292! И тишина в ответ! И в этой тишине тихо падают Боинги.

Предвижу возражение, что есть рекомендация использовать преимущественно без-знаковый тип. Понимаю, с таким правилом Стандарта по другому никак. Но как -4 в беззнаковое перевести, и чтоб всё остальное работало? И почему на других языках и всех процессорах не надо этими танцами с бубном заниматься, а в С++ надо? И что делать с константными выражениями, вычисляемыми компилятором? Вместо элегантных выражений, в которых мы уверены, надо записывать их с оглядкой, чтоб у компилятора небыло изжоги!

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

Как хорошую подсказку познавательно добавить предупреждение в компилятор когда он применяет это правило из Стандарта:"Signed value is intentionally converted to unsigned value. Sorry for crashing one more airplane. Have a nice flight!" Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.

Можно еще исправить Стандарт, ограничив правило только операциями сложения и вычитания. Как компромис. Но это крайне маловероятно в этой Вселенной. Да и Боингов еще много летает.

Представьте студента (С) на экзамене у преподавателя по информатике (П) в ВУЗе.

- П: Хорошо, последний вопрос на 5. Можно ли привести знаковое число к беззнаковому типу?
- С: Хе, можно. НУЖНО! Обязательно НУЖНО! Ставте 5, я пойду.
- П: Как НУЖНО?? О_О. Подумайте. Как можно представить, например, -4 беззнаковым числом? - С: Чего тут думать! Стандарт С++ сказал, что НУЖНО, значит НУЖНО и точка. А то что -4 станет беззнаковым очень большим числом - это уже ни печалька Стандарта, ни моя. - П: Подумайте еще раз. Вы на экзамене по информатике и вас спрашивают о базовых вещах, которые общие для всех языков программирования и процессоров, а не только для С++. - С: Чего вы пристали к мне со своими языками и процессорами. У меня в билете вопрос про С++, вот я про С++ и отвечаю. А вы про какой то там Ассемблер, базовые вещи, языки программирования! Стандарт С++ сказал, компилятор сделал, я ответил! У вас есть вопросы к Стандарту про базовые вещи, вот ему их и задавайте, а я ответил правильно! - П: Да уж. Подстава конкретная.

Подробнее..

Прочти меня код, который не выбесит соседа

16.03.2021 12:09:05 | Автор: admin


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

Я расскажу о подходах, которые мы используем в Яндекс.Такси для написания читаемого кода на C++, Python, JavaScript и других языках.

Обычный рабочий процесс


Допустим, вы работаете в компании, пишете код. Написали функцию, начинаете обкладывать её тестами и понимаете, что что-то глючит. Ну что ж, отлаживаем Оказывается, что плохо работает не ваш код, а функция sample от другого разработчика, которую вы используете.

Выглядит функция sample как-то так:

std::string sample(int d, std::string (*cb)(int, std::string)) {  // Getting new descriptors from d with a timeout  auto result = get(d, 1000);  if (result != -13) {    // Descriptor is fine, processing with non bulk options    auto data = process(result, true, false, true);    // Passing processing result to the callback    return cb(data.second.first, data.second.second);  }  // Notifying callback on error  return cb(result, {});}

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

Нам повезёт, мы сможем исправить функцию, не разобравшись до конца в её работе.
Начинаем читать первый комментарий, где написано: получаем какие-то новые дескрипторы из d с заданным timeout, d дескриптор. То есть часть разбросанных по коду int на самом деле не int они относятся к отдельному классу данных.

Решаем заменить часть int на отдельный тип данных Descriptor в надежде, что компилятор сам найдёт ошибки и нам не придётся дальше отлаживать код. Зовём автора кода, просим его подсказать, где дескрипторы, а где числа. Он сейчас работает над другим проектом, но после долгих уговоров неохотно помогает и быстро ретируется:

enum class Descriptor : int {};std::string sample(Descriptor d, std::string (*cb)(Descriptor, std::string)) {  // Getting new descriptors from d with a timeout  auto result = get(d, 1000);  if (result != Descriptor(-13)) {    // Descriptor is fine, processing with non bulk options    auto data = process(result, true, false, true);  // <== ERROR    // Passing processing result to the callback    return cb(data.second.first, data.second.second);  // <== ERROR  }  // Notifying callback on error  return cb(result, {});}

И тут понеслось:

  • Компилятор нашёл сразу две ошибки. Это очень подозрительно, может, мы не так типы расставили?
  • А что вообще такое data.second.first и data.second.second? В комментариях не написано.

Делать нечего, придётся прочитать весь код и комментарии, чтобы понять, как исправить ошибки.

Боль


Поначалу казалось, что много комментариев это хорошо. Однако при отладке всё выглядит иначе. Код написан на двух языках: на английском и C++. Когда мы пытаемся понять, что происходит в коде, и отладить его, нужно прочитать английский, перевести его в голове на русский. Затем прочитать код на C++, его тоже перевести в голове на русский и сравнить эти два перевода. Убедиться, что смысл комментария совпадает с тем, что написано в коде, а если код делает что-то другое, то, возможно, там и кроется ошибка.

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

Попробуйте угадать, что значат булевые флажки true, false, true в функции process? В комментариях о них ни слова. Чтобы разобраться, нужно пойти в header file, где объявлена функция process:

std::pair<Descriptor, std::pair<int, std::string>> process(bool, Descriptor, bool, bool);

И там мы увидим, что у булевых переменных нет осмысленных имён.

Чтобы понять, что происходит, нужно перейти в соседний файл, прочитать код функции process и разобраться в нём. Возможно, походить по соседним функциям и почитать их. На исследование смежных файлов тратится уйма времени, что мешает осознанию функции sample и портит настроение.

Наконец, data.second.first и data.second.second. Чтобы выяснить их назначение, нужно отмотать назад туда, где мы получаем переменную data. Пойти в место, где объявлена функция process, увидеть, что комментариев нет, а process возвращает пару от пары. Пойти в исходники, узнать, что обозначают переменные int и string, и на всё это снова уходит очень много нашего времени.

Ещё одна маленькая боль код обработки ошибок перемешан с основной логикой. Это мешает ориентироваться. Обработка ошибок функции sample находится внизу, а в середине, внутри цикла if, с большими отступами находится happy path.

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

Выжимка проблем


  • Код написан на двух языках:
    Его в два раза больше.
    При отладке возникают проблемы со сверкой двух языков.

  • Комментариев всё ещё недостаточно:
    Приходится читать код смежных функций.
    Есть магические константы.

  • Код обработки ошибок и основной логики перемешаны:
    Большие блоки кода с большими отступами.

Читаемый код


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

Поехали:

std::string Sample(Descriptor listener,                              std::string (*cb)(Descriptor, std::string)){  UASSERT_MSG(cb, "Callback must be a non-zero function pointer");  const auto new_descriptor = Accept(listener, kAcceptTimeout);  if (new_descriptor == kBadDescriptor) {    return cb(kBadDescriptor, {});  }  auto data = Process(Options::kSync, new_descriptor);  return cb(data.descriptor, data.payload);}

В первой же строчке проверяем входные параметры. Это мини-подсказка/документация по тому, какие данные ожидаются на входе функции.

Следующая правка: вместо функции с непонятным именем Get появляется Accept, широко известная в кругах сетевых программистов. Затем страшную константу 1000 превращаем в именованную константу с осмысленным читаемым именем.

Теперь строка прекрасно читается без дополнительных комментариев: из listener мы принимаем новый дескриптор, на эту операцию даётся kAcceptTimeout.

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

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

Специфика C++
Маленький бонус большинство компиляторов в C++ считают одиночные if без блока else холодным путём.

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

В итоге мы незначительно (а то и вовсе незаметно) ускорили приложение. Пустячок, но приятно.

Дальше. Функция process преобразовалась. Вместо true, false, true теперь есть перечисление возможных опций для process. Код можно прочитать глазами. Сразу видно, что из дескриптора мы процессим какие-то данные в синхронном режиме и получаем их в переменную data.

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

В результате код стал почти в два раза короче. Он стал понятнее. Больше не нужно ходить в соседние функции и файлы. Читать такой код куда приятнее.

Приёмы


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

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

1) Compute(payload, 1023) нечитаемо. Что такое 1023?
Используйте именованные константы.
Compute(payload, kComputeTimeout)

Альтернативным решением может быть явное использование имён параметров. Например, Python позволяет писать:

Compute(payload=payload, timeout=1023);

Ну и C++20 не отстаёт:

Compute({.payload=payload, .timeout=1023});

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

Compute(payload=payload, timeout=MAX_TEST_RUN_TIME);

2) Compute(payload, false) нечитаемо. Что такое false?
Используйте перечисления или именованные константы вместо bool.
У bool не всегда понятна семантика. Введение перечисления даже из двух значений явно описывает смысл конструкции.

Compute(payload, Device::kGPU)

Именованные аргументы в этом месте не всегда спасают:

Compute(payload=payload, is_cpu=False);

Всё ещё непонятно, что False заставляет считать на GPU.

3) Compute(data.second.first, data.second.second) или Compute(data[1][0], data[1][1]) что вообще тут происходит?
Используйте типы с информативными именами полей, избегайте кортежей.
Compute(data.node_id, data.chunk_id)

Совет помогает разобраться не только в том, что происходит в месте вызова, но ещё и полезен для документирования функции.

Попробуйте угадать, какой смысл у возвращаемых int и std::string в коде.

std::tuple<int, std::string> Receive();

int это дескриптор устройства? Код возврата?

А вот так всё становится кристально ясно:

struct Response {    int pending_bytes;    std::string payload;};Response Receive();

4) void Accept(int , int); что это за два числа?
Заводите отдельные типы данных для разных по смыслу вещей.
void Accept(Descriptor, std::chrono::milliseconds)

Или для Python:

def Accept(listener: Descriptor, timeout: datetime.timedelta) -> None:

На самом деле это совет не столько про читаемость кода, сколько про отлов ошибок. Многие (но далеко не все) современные языки программирования позволяют статически проверять типы и узнавать об ошибках ещё до запуска приложения или тестов. В C++ эта функциональность доступна из коробки, в Python нужно пользоваться линтерами и typing.

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

5) void Compute(Data data) функция есть в модуле или заголовке, но должны ли мы ей пользоваться?
Используйте особый namespace или именование для служебных вещей.
namespace detail { void Compute(Data data); }

Или для Python:

def _compute(data: Data) -> None:

С namespace detail/impl или с особым наименованием пользователь поймёт, что функцию использовать не нужно.

6) d, cb, mut, Get что это?
Придумывайте информативные имена переменных, классов и функций.
descriptor, callback, mutator, GetDestination

Самый избитый, но важный совет давайте осмысленные и информативные имена переменным, функциям и классам.

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

Так вот, через неделю или месяц будет сложно вспомнить, что такое d или cd, что делает метод Get (или это вообще класс Get?). Что он возвращает?

Информативные имена вам очень помогут. При чтении будет сразу видно, где descriptor, callback и mutator, а где функция под именем GetDestination() возвращает какой-то Destination.

7) connection = address.Connect(timeout) отладчик завёл нас в эту строчку кода. Что там за переменные, откуда они и куда мы их присваиваем?
Закрепите разные стили за переменными класса, аргументами функций и константами.
Если закрепить отдельные стили за разными типами переменных, код читается лучше. В большинстве распространённых code style именно так и делают:

connection_ = address.Connect(kTimeout);

Мы сразу видим, что переменная address локальная, что мы пытаемся соединиться с kTimeout, который является константой. Результат соединения присваиваем переменной класса connection_. Поменяли буквально пару символов, а код стал понятнее.

Для Python стоит дополнительно придерживаться правила, что приватные поля начинаются с нижнего подчёркивания:

self._connection = address.Connect(TIMEOUT);

8) connection_ = address.Connect(timeout / attempts) есть ли тут ошибка?
Используйте assert, чтобы проверить, правильно ли используют ваш код.
Если количество attempts будет равно нулю, то нативное приложение, скорее всего, рухнет. Где-то возникнет stack trace или корка. С помощью дополнительных телодвижений можно добраться до stack trace и понять, что падение произошло именно в этой строчке.

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

Однако если внутри Connect не будет проверки, всё станет сильно-сильно сложнее. Приложение не упадёт, но будет работать неправильно, не так, как мы ожидаем.

Коду явно не хватает проверок:

ASSERT(attempts > 0);

assert timeout > 0

Теперь ошибка будет сразу обнаружена, и разработчик легко определит неправильное использование.

Assert не только позволяет быстро находить ошибки, но и добавляет читаемости. Выражение assert timeout > 0 прямо говорит, что код ниже будет работать неправильно с отрицательными числами и 0.

8.1) connection_ = address.Connect(timeout / attempts) есть ли тут ошибка?
НЕ используйте assert для проверки пользовательского ввода.
Будет невежливо (и опасно!), если ваша библиотека уронит весь сервер потому, что кто-то на сайте ввёл неправильное число. Здесь стоит использовать другие механизмы для сообщения об ошибках:

if (attempts <= 0) throw NegativeAttempts();

if (attempts <= 0) raise NegativeAttempts();

Как отличить неправильное использование функции программистом от неправильного ввода?

Вот несколько примеров:

  • функция init() должна быть вызвана перед первым использованием функции foo() assert,
  • мьютекс не должен дважды захватываться из одного и того же потока assert,
  • баланс на карте должен быть положительным НЕ assert,
  • стоимость поездки не должна превышать миллиона доллларов НЕ assert.

Если не уверены не используйте assert.

Ещё один хороший приём использовать в отладочных сборках assert, а в релизе другой механизм информирования об ошибках. Пример с attempts идеально ложится на этот случай:

ASSERT(attempts > 0);if (attempts <= 0) throw NegativeAttempts();

9) v = foo(); bar(v); baz(v); assert(v); а функциям bar() и baz() точно хорошо?
Не тяните с обработкой ошибок, не несите их в блок else.
Как только получили значение сразу проверяйте и обрабатывайте ошибки, а дальше работайте как ни в чём не бывало.

Такой подход поможет избежать излишней вложенности конструкций за вашей мыслью будет проще следить.

Пример:

if (value != BAD) {    auto a = foo(value);    auto b = bar(value);    if (a + b < 0) {         return a * b;    } else {         return a / b + baz();    }}return 0;

Сравните:

if (value == BAD) {    return 0;}auto a = foo(value);auto b = bar(value);if (a + b < 0) {     return a * b;}return a / b + baz();

10) [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
Придерживайтесь вменяемой вложенности конструкций.
Если в вашем коде есть if, внутри которого for, внутри которого if, внутри которого while, это сложно читать. Стоит разнести какие-то внутренности на отдельные функции и дать функциям осмысленные имена. Так код станет красивее и приятнее для чтения.

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

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

Вместо заключения


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

Даже в рамках одной компании практики могут расходиться (например, в userver у нас пожёстче с наименованями и bool).

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

Перевод О C и объектно-ориентированном программировании

16.11.2020 10:18:38 | Автор: admin
Привет, Хабр!

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


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

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

Об объектно-ориентированном программировании (ООП) как инструменте



Хотя C++ и описывается как мультипарадигмальный язык программирования, на практике большинство программистов используют C++ сугубо как объектно-ориентированный язык (обобщенное программирование используется для дополнения ООП).

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

Об энтропии как тайной силе, подпитывающей разработку ПО



Мне нравится представлять решение в стиле ООП как созвездие: это группа объектов, между которыми произвольно прочерчены линии. Такое решение вполне можно рассматривать и как граф, в котором объекты являются узлами, а отношения между ними ребрами, но мне ближе феномен группы/кластера, который передается метафорой созвездия (по сравнению с ней граф слишком абстрактен).

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

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

Я видел, как это самым разным образом проявляется в ООП-решениях:
  • В иерархии возникают новые промежуточные уровни, тогда как изначально вводить их не предполагалось.
  • Добавляются новые виртуальные функции с пустыми реализациями в большей части иерархии.
  • Один из объектов в созвездии требует большей обработки, чем планировалось, из-за чего связи между остальными объектами начинают пробуксовывать.
  • В иерархию добавляются обратные вызовы, так, что объекты одного уровня могут обмениваться информацией с объектами другого уровня, при этом не обладая явным знанием друг о друге.
  • Т.д

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

В жизненном цикле любого ООП-проекта рано или поздно наступает такой момент, после которого поддерживать его невозможно. Как правило, в такой момент следует предпринять одно из двух действий:

  • Перейти к черному ящику: скрыть созвездие за каким-нибудь фасадом и медленно вытягивать его из остальной части кода. Система может и далее решать исходную задачу, для которой создавалась, если пока еще работает прилично, но разработка новых фич полностью останавливается, а исправление багов требует очень много времени, если вообще приводит к успеху.
  • Переписать с чистого листа: ООП-дизайн, созданный для решения исходной проблемы, уже так далек от ее текущего состояния, что никаким постепенным рефакторингом его не подстроить под актуальное решение.


Обратите внимание: вариант с черным ящиком все равно потребует переписывания в случае, если разработку новых фич придется продолжить и/или сохранится необходимость в устранении багов.

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

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

О легкости удаления кода как о принципе проектирования



В любой системе, построенной по принципу ООП, именно объектам в составе созвездия уделяется основное внимание. Но я считаю, что взаимосвязи между объектами важны не менее, если не более, чем сами объекты.

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

О производительности по определению



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

Также невозможно не отметить, что ООП-фичи по определению не блещут производительностью. Я реализовал простую ООП-иерархию с интерфейсом и двумя производными классами, которые переопределяют единственный вызов чистой виртуальной функции в Compiler Explorer.

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

В данном случае наиболее бросается в глаза, какую кучу кода генерируют компиляторы, даже после оптимизации. Затем, присмотревшись, можно заметить, как затратно и при этом бесполезно такое сопровождение: когда программе передается ненулевое количество аргументов, код все равно выделяет память (вызов new), загружает адреса vtable обоих объектов, загружает адрес функции Work() для ImplB и перескакивает к ней, чтобы затем сразу же вернуться, так как делать там нечего. Наконец, вызывается delete, чтобы высвободить выделенную память.

Ни одна из этих операций совершенно не была необходимой, но процессор исправно исполнил их все.

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

Возьмем, к примеру, Unity. В рамках принятой у них в последнее время практики производительность это корректность используется C#, объектно-ориентированный язык, поскольку этот язык уже применяется в самом движке. Однако, они остановились на подмножестве C#, причем, на таком, которое жестко не привязано к ООП, и на его основе создают конструкты, заточенные на высокую производительность.

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

О борьбе со стереотипами



В статье Анджело Песке Переусложнение корень всего зла автор попадает в самую точку (см. последний раздел: People) признавая, что большинство софтверных проблем на самом деле обусловлено человеческим фактором.

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

Мне много доводилось беседовать с разными людьми об ООП и присущих ему проблемах и, хотя я считаю, что мне всегда удавалось объяснить, почему я думаю так, а не иначе, не думаю, что мне удалось кого-то отвратить от ООП.

Правда, за годы работы я выделил для себя три основных аргумента, из-за которых люди не готовы дать шанс другой стороне:

  • С хорошим ООП так бы не вышло. Это плохо спроектированное ООП. Этот код не следует принципам ООП и тому подобное. Слышал такие вещи, когда демонстрировал примеры ООП, заведшего во все тяжкие (как я уже говорил выше, с ООП-кодом такое происходит неизбежно). Это типичный пример логического заблуждения Ни один истинный шотландец.
  • Я знаю ООП, и, если начинать с чистого листа, то больше ничем пользоваться не хочу. Это страх потерять свой сеньорский статус после того, как на протяжении всей карьеры пользовался принципами ООП и руководил другими людьми, от которых также требовал использовать эти принципы. Я считаю, что здесь мы имеем дело с примером ошибки невозвратных издержек.
  • Все знают ООП, очень удобно говорить с людьми на общем языке, обладая общими знаниями. Это логическая ошибка, называемая аргумент к народу, то есть, если практически все программисты пользуются принципами ООП, то эта идея не может быть неподходящей.


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

Исследование COVID-19 и неинициализированная переменная

05.02.2021 14:16:53 | Автор: admin

0796_covid_sim_ru/image1.png
Существует открытый проект COVID-19 CovidSim Model, написанный на языке C++. Существует статический анализатор кода PVS-Studio, который умеет хорошо находить ошибки. Однажды они встретились. Познайте хрупкость алгоритмов математического моделирования и почему нужно прикладывать максимум усилий к качеству программного кода.


На днях мне понадобилось кое-что найти на GitHub, что является началом этой маленькой истории. Изучая результаты поиска, я случайно набрёл на проект COVID-19 CovidSim Model. Недолго думая, я решил проверить его с помощью анализатора PVS-Studio.


Проект оказался совсем крошечным. В нём всего 13 000 строк кода, если не считать пустые строки и комментарии. И ошибок там тоже почти нет. Но одна ошибка настолько проста и красива, что я не могу пройти мимо!


void CalcLikelihood(int run, std::string const& DataFile,                    std::string const& OutFileBase){  ....  double m = Data[row][col]; // numerator  double N = Data[row][col + 1]; // denominator  double ModelValue;  // loop over all days of infection up to day of sample  for (int k = offset; k < day; k++)  {    // add P1 to P2 to prevent degeneracy    double prob_seroconvert = P.SeroConvMaxSens *      (1.0 - 0.5 * ((exp(-((double)(_I64(day) - k)) * P.SeroConvP1) + 1.0) *      exp(-((double)(_I64(day) - k)) * P.SeroConvP2)));    ModelValue += c * TimeSeries[k - offset].incI * prob_seroconvert;  }  ModelValue += c * TimeSeries[day - offset].S * (1.0 - P.SeroConvSpec);  ModelValue /= ((double)P.PopSize);  // subtract saturated likelihood  LL += m * log((ModelValue + 1e-20) / (m / N + 1e-20)) +        (N - m) * log((1.0 - ModelValue + 1e-20) / (1.0 - m / N + 1e-20));  ....}

Серьёзный научный код. Что-то считается. Формулы. Выглядит всё умно и обстоятельно.


Вот только все эти вычисления разбиваются о человеческую невнимательность. Хорошо, что на помощь может прийти анализатор кода PVS-Studio и указать на баг: V614 [CWE-457] Uninitialized variable 'ModelValue' used. CovidSim.cpp 5412


И действительно, посмотрим внимательнее на это:


double ModelValue;for (int k = offset; k < day; k++){  double prob_seroconvert = ....;  ModelValue += c * TimeSeries[k - offset].incI * prob_seroconvert;}

Перед нами простая и одновременно страшная ошибка: неинициализированная переменная. Этот алгоритм может насчитать всё что угодно.


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


Это уже не первая наша статья на эту тему:



Используйте статический анализатор кода PVS-Studio! Польза от своевременно найденных ошибок может быть колоссальной. Спасибо за внимание.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. COVID-19 Research and Uninitialized Variable.

Подробнее..

О поиске утечек памяти в СQt приложениях

16.02.2021 14:15:09 | Автор: admin

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

Эта статья посвящена разным инструментам, которые можно с той или иной степенью успешности применять для отлова утечек памяти в С++/Qt приложениях (desktop). Инструменты будут рассмотрены в связке с IDE Visual Studio 2019. В статье будут рассмотрены не все возможные инструменты, а лишь наиболее популярные и эффективные.

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

В чем проблема?

Утечка памяти ситуация, когда память была выделена (например, оператором new) и ошибочно не была удалена соответствующим оператором/функцией удаления (например, delete).

Пример 1.

int* array = nullptr;for (int i = 0; i < 5; i++){array = new int[10];}delete[] array;

Здесь налицо утечка при выделении памяти для первых 4 массивов. Утекает 160 байт. Последний массив удаляется корректно. Итак, утечка строго в одной строке:

array = new int[10];

Пример 2.

class Test{public:Test(){a = new int[100];b = new int[300];}~Test(){delete[] a;delete[] b;}private:int* a;int* b;};int main(){Test* test = new Test;return 0;}

Здесь утечек уже больше: не удаляется память для a (400 байт), для b (1200 байт) и для test (16 байт для x64). Впрочем, удаление a и b в коде предусмотрено, но его не происходит из-за отсутствия вызова деструктора Test. Таким образом, утечек три, но ошибка, приводящая к этим утечкам, всего одна, и она порождается строкой

Test* test = new Test;

При этом в коде класса Test ошибок нет.

Пример 3.

Пусть есть класс Qt, примерно такой:

class InfoRectangle : public QLabel{Q_OBJECTpublic:InfoRectangle(QWidget* parent = nullptr);private slots:void setInfoTextDelayed();private:QTimer* _textSetTimer;};InfoRectangle::InfoRectangle(QWidget* parent): QLabel(parent){_textSetTimer = new QTimer(this);_textSetTimer->setInterval(50);connect(_textSetTimer, &QTimer::timeout, this, &InfoRectangle::setInfoTextDelayed);}void InfoRectangle::setInfoTextDelayed(){// do anythingsetVisible(true);}

Пусть также где-то в коде затесалось выделение памяти:

InfoRectangle* rectangle = new InfoRectangle();

Будет ли являться это утечкой, если явно не вызван delete? Это зависит от того, включен ли объект в иерархию объектов Qt. Если объект включён одним из следующих примерных вызовов, то нет, не утечка:

mnuLayout->addWidget(rectangle);rectangle->setParent(this);

В остальных же случаях утечка. Причем если мы будем считать точное количество утечек в этом примере, то можем наткнуться на неожиданный вывод: утечек больше, чем можно сначала предположить. Очевидная утечка выделение памяти для InfoRectangle. Побочная утечка выделение памяти для QTimer, несмотря на включение объекта _textSetTimer в иерархию объектов Qt. А вот утечка, которая совсем не очевидна вызов функции connect.

Дело в том, что в ее реализации вызовом new всё же создается некий объект:
template <typename Func1, typename Func2>    static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,                const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,                Qt::ConnectionType type = Qt::AutoConnection)    {        typedef QtPrivate::FunctionPointer<Func1> SignalType;        typedef QtPrivate::FunctionPointer<Func2> SlotType;        const int *types = nullptr;        if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)            types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();        return connectImpl(sender, reinterpret_cast<void **>(&signal),                           receiver, reinterpret_cast<void **>(&slot),                           new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,                                          typename SignalType::ReturnType>(slot),                            type, types, &SignalType::Object::staticMetaObject);    } 

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

Зафиксируем еще один важный момент: причина утечки одна, а самих утечек при этом может быть много. Этот нюанс важен при обсуждении инструментариев по отлову утечек. Идеальный инструментарий тот, который, не давая ложных срабатываний и не пропуская утечек, указывает именно на первопричины утечек, то есть на каждую причину выдает ровно одну строку кода, ей соответствующую (а не целую кучу). То есть делает так, как делал бы человек. Забегая вперед, скажем, что таких инструментариев на данный момент не существует.

Раз их не существует, то как сравнивать между собой разные, не всегда коррелирующие результаты поиска утечек, полученные разными реальными инструментами? Мы ведь хотим сравнения

Для этого мы взяли реальный пример конкретную ревизию репозитория, для которой было точно известно, что утечки есть, и начали подробно с ней работать. Мы взяли один типичный сценарий работы пользователя с нашим приложением и начали на нём запускать все возможные динамические инструменты отлова утечек. Запускали многократно с разными настройками, детально анализируя полученные отчеты об ошибках. В итоге сформировали итоговый список ошибок в коде, приведших к утечкам памяти. Этот список назвали эталонным и посчитали, что других ошибок (ненайденных) нет. И в дальнейшем результаты, полученные каждой отдельной утилитой поиска утечек, сравнивали с эталонным списком ошибок.

Проект

Размер кода

Сценарий работы пользователя

Ревизия репозитория

Кол-во ошибок в эталоне

Суммарный объем утекающей памяти

Конкретный проект

1.5 млн строк

Конкретный сценарий: запускаем ПО, жмем на кнопку 1, потом на кнопку 2, ждем завершения вычислений, закрываем ПО

конкретная

7

253 кБ

Таблица 1. Эталон поиска утечек памяти.

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

Intel Inspector

Intel Inspector графическая утилита, удобно встраиваемая в Visual Studio и позволяющая в виде кликабельного списка выдавать места в коде с предполагаемыми утечками оперативной памяти проверяемого приложения и некоторыми другими проблемами памяти. В сценарии отлова утечек памяти Intel Inspector использует динамический анализ, а это значит, что если в процессе работы приложения код с утечками памяти не будет вызван, то и проблем в нем не будет найдено.

Установка

Intel Inspector входит в состав пакета Intel Parallel Studio 2019, при этом есть возможность установить только сам Intel Inspector, убрав галочки с остальных компонентов дистрибутива при установке. Visual Studio 2019 должна быть закрыта в момент установки Intel Parallel Studio. После установки, Intel Inspector будет автоматически встроен в Visual Studio и должен появиться на панели инструментов (рис. 1).

Рис. 1. Начало работы с Intel Inspector`омРис. 1. Начало работы с Intel Inspector`ом

Если значок Intel Inspectorа не виден на панели инструментов, нужно щёлкнуть правой кнопкой мыши где-нибудь на этой панели инструментов и поставить галочку Intel Inspector.

Запуск

При нажатии на кнопку-значок появится вкладка Intel Inspector с выбором глубины анализа. Выбираем первый пункт Detect Leaks и включаем все галочки, соответствующие всем видам анализа (рис. 2). Если какие-то галочки пропустить, то, к сожалению, есть риск, что не все утечки будут найдены.

Рис. 2. Вкладка Intel Inspector`а для его настройки и запускаРис. 2. Вкладка Intel Inspector`а для его настройки и запуска

Далее нажимаем кнопку Start, через некоторое время откроется приложение. В нем нужно запустить тот или иной сценарий работы, а лучше все сразу (то есть, как следует погонять приложение), затем закрыть. Чем больше на разных параметрах, в разных режимах и в разных сценариях проработает приложение, тем больше утечек памяти будет найдено. И это общий принцип для всех механизмов поиска утечек, использующих динамический анализ. Как мы уточнили ранее, в целях сравнения мы запускали только эталонный сценарий тестирования (см. табл. 1). Итак, после закрытия приложения Intel Inspector слегка задумывается и в итоге выдаёт отчёт следующего вида (рис. 3):

Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.

В отчете выдаются кликабельный и сортируемый список утечек, размеры утечек, места в коде с утечками, call-stack и многое другое. Короче, форма выдачи результатов весьма и весьма на уровне. Все очень быстро понимается и усваивается. Все это внутри IDE!

Это будет работать, если есть отладочная информация. То есть debug работать будет, а release нет. В С++-приложениях часто бывает так, что работа в режиме debug намного медленнее, чем в release (мы фиксировали разницу в скорости до 20 раз), и пользоваться debug'ом очень некомфортно. Однако на этот случай есть лайфхак собрать версию release (быструю, со всеми ключами оптимизации), дополнительно включив в нее отладочную информацию. Это позволяет Intel Inspector'у подсветить строки в исходном коде, где он предполагает наличие утечек. О том, как включить в release отладочную информацию, написано здесь.

Результаты

Мы провели сравнение скоростных характеристик работы приложения в разных режимах работы: с Intel Inspector (будем называть его Инспектор) и без него, в debug и release. Тестирование проводилось на эталонном примере (см. табл 1).

Конфигурация

Среднее время теста, с

Замедление
работы, что привносит Инспектор, раз

Без Инспектора

С Инспектором

Release c отладочной информацией

10

70

7

Debug

101

973

9,6

Таблица 2. Время тестирования с учётом работы Intel Inspector`а

По таблице можно видеть, что проверка на утечки памяти требует весьма существенного времени. Особенно этот факт начинает играть существенными красками в свете непрерывной интеграции. То есть, если, к примеру, тот сценарий, на котором мы проверяли, включен в состав тестов регрессионного тестирования, то он в них выполняется за 10 секунд. Если же мы захотим на этом же сценарии прогонять поиск утечек памяти (в debug), то это займет примерно в 100 раз больше времени. Прогонять каждую ночь (а тем более, по каждому коммиту) тесты становится невозможно.

Самое главное что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных Intel Inspector`ом?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Release c отладочной информацией

7

192

168

24

0

1 (100%)

27 раз

Debug

7

129

107

22

0

1 (100%)

18 раз

Таблица 3. Результаты работы Intel Inspector

Да, Intel Inspector действительно способен найти реальные утечки памяти. Это долго и мучительно, но он их находит. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует Intel Inspector, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится куча строчек, которые породились этой ошибкой (как в примерах 2 и 3, см. выше).

Если ликвидировать все такие реальные ошибки, то Intel Inspector все равно будет показывать еще немало утечек, и все они ложные. Более того, по таблице видно, что этих ложных срабатываний в release больше, чем в debug. И в этом случае, судя по всему, сработала оптимизация при компиляции она скрыла от Инспектора некоторые детали, и Инспектор запутался.

Приведем несколько произвольных примеров ложных срабатываний.

Пример 1. Утечки в системных dll.

Intel Inspector может обнаружить вот такие вот странные утечки в подгружаемых системных dll, с таким интересным стеком. К проверяемому нами коду такие утечки вообще отношения не имеют, даже если по факту там утечки и есть.

Рис. 4. Утечки в системных dll.Рис. 4. Утечки в системных dll.

Пример 2. aligned_malloc.

m_pVitData = (VITDEC_DATA*)_aligned_malloc(sizeof(VITDEC_DATA), 16);m_pDcsnBuf = (byte*)_aligned_malloc(64 * (VITM6_BUF_LEN + VITM6_MAX_WND_LEN), 16);..._aligned_free(m_pDcsnBuf);_aligned_free(m_pVitData);

К счастью, подобная "утечка" находится только в release, а в debug нет.

Пример 3. Pragma.

#pragma omp parallel for schedule(dynamic)for (int portion = 0; portion < portionsToProcess; ++portion){}

Утечка показывается именно в строке с директивой #pragma!

Возможно, какими-то настройками (внутри Intel Inspector, внутри VS, переменные окружения и т.д.) можно победить этот вал ложных утечек, но из коробки точно нет. Возможно также, что на маленьких и простых приложениях (<50000 строк кода) таких проблем с Intel Inspector не будет. На серьезных же приложениях точно будут, к гадалке не ходи.

Вывод

Intel Inspector штука довольно удобная и полезная, способная найти все утечки (если прогнать все сценарии), выдающая относительно немного ложных срабатываний. Работа в конфигурации release с отладочной информацией довольно быстра, но выдает больше ложных срабатываний (а значит, больше ручной работы по их проверке), чем debug. При этом работа в конфигурации debug фантастически медленна.

Что касается стабильности работы, то Intel Inspector здесь не может показать выдающиеся результаты. Иногда в процессе тестирования бывают падения, иногда зависания, причем на ровном месте. Иногда нам попадались такие важные для нас проекты и сценарии работы пользователя, когда вообще не получалось дажезавести Intel Inspector, не то, что доехать на нём до получения результатов.

Visual Leak Detector

Visual Leak Detector (далее VLD) маленькая библиотека, включаемая в исходный код каждого проекта и выводящая в окно Output (IDE Visual Studio 2019) отчёт по утечкам памяти.

Установка

  1. Убедиться, что Visual Studio не запущена.

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

  3. Далее установить последний VLD (на момент написания статьи vld-2.5.1-setup.exe) по умолчанию, оставив при установке все галочки включёнными (на добавление в Path и встраивание в Visual Studio). Установщик можно скачать отсюда.

  4. На момент написания статьи в дистрибутиве VLD нет нужных dll-файлов для работы с Visual Studio 2019, потому необходимо скопировать dbghelp.dll из папки C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Cpp\x64 в папку C:\Program Files (x86)\Visual Leak Detector\bin\Win64.

  5. Нужно создать заголовочный файл примерно со следующим содержанием:

    #pragma once//#define LEAKS_DETECTION#ifdef LEAKS_DETECTION#include <vld.h>#endif
    

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

  6. В любой файл реализации (сpp) нужно включить этот новый заголовочник. Это нужно сделать только для одного файла внутри проекта, и для каждого проекта в solution.

Запуск

Достаточно раскомментировать в заголовочном файле строчку

#define LEAKS_DETECTION

и собрать solution. После этого можно запускать (F5) приложение и прогонять разные сценарии, где могут быть утечки памяти. Запускать можно только в конфигурации debug. Release c отладочной информацией работать не будет.

После закрытия проверяемого приложения VLD выведет отчет в окно Output. Отчет содержит список утечек, кликабельный call-stack по каждой утечке, размеры утечек.

Пример того, что выводит VLD
---------- Block 652047 at 0x0000000027760070: 8787200 bytes ----------  Leak Hash: 0x02B5C300, Count: 1, Total 8787200 bytes  Call Stack (TID 30996):    ucrtbased.dll!malloc()    d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\heap\new_array.cpp (29): SniperCore.dll!operator new[]()    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (445): SniperCore.dll!CS2Ldfg::CreateLLRTbls() + 0xD bytes    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (217): SniperCore.dll!CS2Ldfg::SetModeEB()    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (1447): SniperCore.dll!CS2Ldfg::Set() + 0xA bytes    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (509): SniperCore.dll!DFBase::instanceS2Dec()    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (58): SniperCore.dll!DFBase::DFBase() + 0xF bytes    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (514): SniperCore.dll!DgbS5FecAnlzr::DgbS5FecAnlzr() + 0xA bytes    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\fbganalyser.cpp (45): SniperCore.dll!TechnicalLayer::FBGAnalyser::FBGAnalyser() + 0x21 bytes    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\fbganalysishandler.cpp (218): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::init() + 0x2A bytes    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\fbganalysishandler.cpp (81): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::enqueueRequest()    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (57): SniperCore.dll!TotalCore::ThreadedHandler2::run()    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes    kernel32.dll!BaseThreadInitThunk() + 0xD bytes    ntdll.dll!RtlUserThreadStart() + 0x1D bytes  Data:    00 00 00 00    01 01 01 01    01 01 01 02    02 02 02 02     ........ ........    02 02 03 03    03 03 03 03    03 04 04 04    04 04 04 04     ........ ........    05 05 05 05    05 05 05 05    06 06 06 06    06 06 06 07     ........ ........    07 07 07 07    07 07 08 08    08 08 08 08    08 09 09 09     ........ ........    09 09 09 09    0A 0A 0A 0A    0A 0A 0A 0B    0B 0B 0B 0B     ........ ........    0B 0B 0C 0C    0C 0C 0C 0C    0C 0D 0D 0D    0D 0D 0D 0D     ........ ........    0E 0E 0E 0E    0E 0E 0E 0E    0F 0F 0F 0F    0F 0F 0F 10     ........ ........    10 10 10 10    10 10 11 11    11 11 11 11    11 12 12 12     ........ ........    EE EE EE EE    EF EF EF EF    EF EF EF F0    F0 F0 F0 F0     ........ ........    F0 F0 F1 F1    F1 F1 F1 F1    F1 F2 F2 F2    F2 F2 F2 F2     ........ ........    F3 F3 F3 F3    F3 F3 F3 F3    F4 F4 F4 F4    F4 F4 F4 F5     ........ ........    F5 F5 F5 F5    F5 F5 F6 F6    F6 F6 F6 F6    F6 F7 F7 F7     ........ ........    F7 F7 F7 F7    F8 F8 F8 F8    F8 F8 F8 F9    F9 F9 F9 F9     ........ ........    F9 F9 FA FA    FA FA FA FA    FA FB FB FB    FB FB FB FB     ........ ........    FC FC FC FC    FC FC FC FC    FD FD FD FD    FD FD FD FE     ........ ........    FE FE FE FE    FE FE FF FF    FF FF FF FF    FF 00 00 00     ........ ........---------- Block 2430410 at 0x000000002E535B70: 48 bytes ----------  Leak Hash: 0x7062B343, Count: 1, Total 48 bytes  Call Stack (TID 26748):    ucrtbased.dll!malloc()    d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\heap\new_scalar.cpp (35): SniperCore.dll!operator new() + 0xA bytes    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (78): SniperCore.dll!std::_Default_allocate_traits::_Allocate()    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (206): SniperCore.dll!std::_Allocate<16,std::_Default_allocate_traits,0>() + 0xA bytes    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (815): SniperCore.dll!std::allocator<TotalCore::TaskResult *>::allocate()    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (744): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::_Emplace_reallocate<TotalCore::TaskResult * const &>() + 0xF bytes    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (708): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::emplace_back<TotalCore::TaskResult * const &>() + 0x1F bytes    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (718): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::push_back()    D:\SOURCE\SAP_Git\sap_win64\include\core\engine\task.h (119): SniperCore.dll!TotalCore::LongPeriodTask::setTmpResult()    D:\SOURCE\SAP_Git\sap_win64\include\core\engine\discretestephandler.h (95): SniperCore.dll!TotalCore::DiscreteStepHandler::setResult()    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\prmbdtcthandler.cpp (760): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::setContResult() + 0x1A bytes    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\prmbdtcthandler.cpp (698): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::processPortion()    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (109): SniperCore.dll!TotalCore::ThreadedHandler2::tryProcess()    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (66): SniperCore.dll!TotalCore::ThreadedHandler2::run()    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes    kernel32.dll!BaseThreadInitThunk() + 0xD bytes    ntdll.dll!RtlUserThreadStart() + 0x1D bytes  Data:    10 03 51 05    00 00 00 00    B0 B4 85 09    00 00 00 00     ..Q..... ........    60 9D B9 08    00 00 00 00    D0 1B 24 06    00 00 00 00     `....... ..$.....    30 B5 4F 11    00 00 00 00    CD CD CD CD    CD CD CD CD     0.O..... ........

В конце отчёта присутствует краткий итог в виде:

Visual Leak Detector detected 383 memory leaks (253257876 bytes).Largest number used: 555564062 bytes.Total allocations: 2432386151 bytes.Visual Leak Detector is now exiting.

Или, если утечек нет,

No memory leaks detected.Visual Leak Detector is now exiting.

Результаты

Мы провели сравнение скоростных характеристик работы приложения в конфигурации debug в разных режимах работы: с VLD и без него. Как было сказано, в конфигурации release (пусть даже и с отладочной информацией) vld работать не может. В табл. 4 замеры времени выполнения для release приводятся исключительно для сравнения с debug. Тестирование проводилось на эталонном примере (см. табл. 1).

Конфигурация

Среднее время теста, с

Замедление работы, что привносит VLD, раз

Без VLD

С VLD

Debug

101

172

1,7

Release c отладочной информацией

10

-

-

Таблица 4. Время тестирования с учётом работы VLD

Что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных VLD?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Debug

7

185

185

0

0

1 (100%)

26 раз

Таблица 5. Результаты работы VLD

Да, VLD находит реальные утечки памяти. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует VLD, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится очень большая куча строчек, которые породились этой ошибкой (как в примерах 2 и 3, см. выше). Из-за того, что эти утечки нельзя никак сортировать (или группировать), оказывается не очень приятно работать с такой большой плоской простыней. Да и вообще, как поначалу можно доверять тому списку утечек, где фигурирует такая вот утечка:

connect(arrowKeyHandler, &ArrowKeyHandler::upPressed,[this] { selectNeighbourSignal(TopSide); }); 

Потом, разбираясь, оказывается, что деструктор класса всё же не вызывается, и утечка в функции connect действительно есть (см. пример 3). Но поначалу этому списку сложно поверить из-за большой избыточности.

Если ликвидировать все такие реальные ошибки, то VLD честно скажет, что утечек нет. И этот факт крайне важен для continuous integration.

Вывод

Visual Leak Detector штука очень простая и очень полезная, способная найти все утечки (если прогнать все сценарии) и при этом не выдающая ложных срабатываний. Прогон сценариев в VLD довольно медленный, однако, он всё же быстрее, чем в Intel Inspector в конфигурации debug. Плоский, не очень дружественный и простынообразный вывод результатов способен запутать своей объемностью и дубликатами, однако со временем и к нему можно привыкнуть и даже использовать в continuous integration.

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

Снимки оперативной памяти в VS 2019

В IDE Visual Studio 2019 есть собственный встроенный компонент для диагностики проблем Diagnostic Tools. В его составе есть механизм получения снимков памяти (snapshots). С его помощью можно находить утечки памяти как разницу (дельта) между снимками. Само собой, чтобы дельта показывала именно утечки, надо делать снимки в определенные, далеко не случайные моменты.

Запуск

Запустите приложение в отладчике (в конфигурации debug или release c отладочной информацией). При запуске должна по умолчанию появиться панель Diagnostic Tools. Выберите на этой панели вкладку Memory Usage, нажмите кнопку Heap Profiling и дальше делайте снимки кнопкой Take Snapshot.

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

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

Рис. 5. Работа со снимками памяти.Рис. 5. Работа со снимками памяти.

Для просмотра результатов нажмём на прирост памяти между снимками (см. Рис. 5, где стрелочка). В появившейся вкладке в области редактора кода выберем ViewMode -> Stacks View (вместо Types View), и через некоторое время формирования отчёта увидим интерактивное дерево вызовов:

Рис. 6. Работа со снимками памяти, call-stack.Рис. 6. Работа со снимками памяти, call-stack.

Результаты

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

Вывод

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

PVS-Studio

Последняя из рассматриваемых в данной статье утилита, которая может помочь в нахождении утечек памяти, - PVS-Studio. Она делает статический анализ кода, не требующий запуска приложения. Анализ может запускаться и для одного выделенного проекта, и для всех проектов в solution. Также он может запускаться при каждом новом сохранении редактируемого файла исходного кода, инкрементно, сразу указывая на сомнительные места в новом коде.

Установка

Проблем с установкой нет. В результате установки PVS-Studio оказывается встроенной в Visual Studio 2019, в меню Extensions.

Запуск

Для запуска всего solution`а вызываем команду Extensions->PVS-Studio->Check. Результат проверки выдается во вкладке PVS-Studio и содержит список потенциальных ошибок в коде, распределённых по вкладкам с критичностью High, Medium и Low.

Этот объемный результирующий список будет содержать не только утечки памяти, а и все остальные ошибки в коде, что PVS-Studio посчитает ошибками. Чтобы оставить только то, что касается только утечек памяти, нужно фильтровать список по следующим кодам: V599, V680, V689, V701, V772, V773, V1005, V1023 (более подробно см. здесь).

Для фильтрации нужно зайти в настройки Visual Studio в меню Tools -> Options -> PVS-Studio и на вкладке Detectable Errors (C++) выставить нужные галочки, убрав остальные (при этом удобно сначала использовать команду Hide All, а потом уже ставить галочки) Рис. 8. Также нужно убрать галочки из других групп и вкладки Detectable Errors (C#) (выбирая Hide All или Disabled).

Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.

Чтобы показать все сообщения с выбранными кодами ошибок, нужно убедиться, что в окне PVS-Studio над сообщениями все кнопки High, Medium и Low включены.

Результаты

Итак, для поиска утечек памяти был запущен анализ на проекте, включающем около 1.5 млн строк кода и 2269 файлов кода. Анализ производился на Intel Core i7 4790K. Конфигурация кода (debug или release) значения не имеет, поскольку анализ статический (если более точно, разница есть из-за условной компиляции, но она непринципиальна).

Время анализа

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Всего

Верных

Ложных

30 мин

7

2

0

2

7

0 %

Таблица 6. Поиск утечек памяти утилитой PVS-Studio

Вывод

Для поиска утечек памяти этой утилитой можно пользоваться только, если под рукой нет чего-то более заточенного под утечки памяти (Intel Inspector, VLD). Она не способна находить все утечки, но выдает ложные срабатывания. Это не удивительно, поскольку утилита PVS-Studio никогда и не заявлялась как специализированный инструмент поиска утечек.

Сравнение работы инструментов для поиска утечек памяти

Подводя итоги, можно однозначно выделить в качестве лучших 2 инструмента для поиска утечек Intel Inspector и Visual Leak Detector. На основании проведенного тестирования мы получаем следующую их сравнительную таблицу:

Intel Inspector

VLD

Вид анализа

Динамический

Динамический

Стабильность работы

Средняя

Высокая

На любом ли примере (сценарии) может отработать

Нет

Нет

Удобство использования

Среднее

Низкое

Замедление debug

9.6 раз

1,7 раз

Замедление release с отладочной информацией

7 раз

-

Находит ли реальные утечки в debug

Да, все. Избыточность результатов 18 раз.

Да, все. Избыточность результатов 26 раз.

Находит ли реальные утечки в release с отладочной информацией

Да, все. Избыточность результатов 27 раз.

-

Ложные срабатывания в debug

Да, немного

Нет

Ложные срабатывания в release с отладочной информацией

Да, немного

-

Можно ли использовать в Continuous Integration

Нет

Да

Таблица7. Сравнение Intel Inspector и VLD.

Место 1 в рейтинге целесообразно отдать VLD, поскольку он не выдает ложных срабатываний, более стабилен в работе и более подходит для использования в сценариях непрерывной интеграции.

Подробнее..

Ленивые итераторы и диапазоны в C

15.03.2021 12:08:42 | Автор: admin

Для того, чтобы упростить написание и чтение кода, программисты периодически придумывают всякие техники. Об одной из таких техник я уже писал в публикации Долой циклы, или Неленивая композиция алгоритмов в C++.


Однако есть и классическая, более распространённая техника для борьбы с циклами использование итераторов и диапазонов для ленивых операций над последовательностями. Всё это уже сто лет есть в Бусте и других сторонних библиотеках (к примеру, range-v3) и постепенно просачивается в стандартную библиотеку.


Хотя, в некотором смысле, и в стандартной библиотеке ленивые итераторы уже есть давно (см. std::reverse_iterator).

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



Содержание


  1. Итератор
  2. Ленивость
    1. Transform Iterator
    2. Filter Iterator
  3. Ленивые диапазоны
    1. Transform Range
    2. Stride
  4. Компоновка
  5. Суть итераторов и диапазонов
  6. Ссылки


Итератор


Начнём с простого. Что вообще такое итератор?


Итератор


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


  • Продвижение (например, ++i или i + n);
  • Разыменование (*i).

Операции с итератором


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



Ленивость


Внедрение в операции над диапазонами может быть сколь угодно хитрым и сложным (простые примеры я привёл ниже). Ленивость же состоит в том, что нет никаких промежуточных результатов. Все вычисления происходят только тогда, когда вызываются операции разыменования или продвижения.


Определение 1. Итератор e достижим из итератора b, если существует схема f продвижения итератора b такая, что f(b) = e.


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


Достижимость


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



Transform Iterator


Простой пример внедрения в операцию разыменования это boost::transform_iterator.


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


Преобразующий итератор


Таким образом, каждому итератору i типа I мы поставили в соответствие итератор j типа J такой, что *j = f(*i).


auto v = std::vector{1, 2, 3, 4};//                   2  4  6  8auto i = v.begin();auto t = boost::make_transform_iterator(i, [] (auto x) {return x * 2;});assert(*t == 2);++t;assert(*t == 4);...


Filter Iterator


Пример внедрения в продвижение это boost::filter_iterator.


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


Фильтрующий итератор


Таким образом, мы "выбросили" из исходной последовательности итераторы i такие, что p(*i) == false, и в результирующей последовательности, для каждого итератора j типа J выполняется p(*j) == true.


auto v = std::vector{1, 2, 3, 4};//                      ^     ^auto i = v.begin();auto f = boost::make_filter_iterator(i, [] (auto x) {return x % 2 == 0;});assert(*i == 2);++i;assert(*i == 4);


Ленивые диапазоны


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


При этом диапазон это уже более сложная конструкция, и у него другой интерфейс, похожий на интерфейс контейнеров:


  • Взятие итераторов на начало и конец (r.begin(), r.end());
  • Взятие первого элемента диапазона (r.front());
  • Проверка на пустоту (r.empty()).

Разница только в том, что диапазон не владеет элементами, которые он задаёт. Хотя бы потому что канонический диапазон это просто пара итераторов (к примеру, std::equal_range).


Важно отметить, что диапазон принято задавать полуинтервалом [b, e). Это значит, что итератор-начало b указывает на первый элемент последовательности, а итератор-конец e указывает на элемент после последнего. Таким образом, когда мы приходим в итератор-конец, мы точно знаем, что последовательность закончилась.


Диапазон



Transform Range


На основе преобразующих итераторов можно собрать диапазон (см. boost::iterator_range).


auto v = std::vector{...};auto l = [] (auto x) {return x * x;};auto tb = boost::make_transform_iterator(v.begin(), l);auto te = boost::make_transform_iterator(v.end(), l);auto tr = boost::make_iterator_range(tb, te);for (auto x: tr){    ...}

Или проще (см. boost::transformed):


auto v = std::vector{...};auto tr = boost::adaptors::transform(v, [] (auto x) {return x * x;});for (auto x: tr){    ...}

В C++20 это std::transform_view:


auto v = std::vector{...};auto tr = std::ranges::views::transform(v, [] (auto x) {return x * x;});for (auto x: tr){    ...}


Stride


Другой пример ленивого диапазона это boost::strided.


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


Шагающий диапазон


auto v = std::vector{1, 2, 3, 4};//                   ^     ^auto s = boost::adaptors::strided(v, 2);assert(s.front() == 1);s.advance_begin();assert(s.front() == 3);


Компоновка


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


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


  • возвести их в квадрат,
  • взять только каждый четвёртый элемент,
  • и оставить только чётные числа,

то можно это сделать так:


auto v = std::vector{...};auto r = v | transformed([] (auto x) {return x * x;})           | strided(4)           | filtered([] (auto x) {return x % 2 == 0;});

Или, в C++20:


auto v = std::vector{...};auto r = v | std::views::transformed([] (auto x) {return x * x;})//         | strided(4) // В C++20 такого нет.           | std::views::filtered([] (auto x) {return x % 2 == 0;});

Ещё раз хочу подчеркнуть, что этот код не производит никаких вычислений. Он только сохраняет "схемы" работы с диапазоном, а настоящие вычисления будут происходить только во время продвижения или разыменования обёрнутого итератора.



Суть итераторов и диапазонов


Помимо C++, в некоторых языках программировани также существует концепция под названием "итератор", но эта концепция зачастую имеет какой-то свой, альтернативный смысл.
К примеру, "итераторы" в языках Java и C# знают свой предел. С точки зрения языка C++ это, скорее, диапазоны.


В C++ итератор это именно обобщение указателя. По сути указатель это самый сильный (или наиболее конкретный) итератор, причём иерархия следующая:


  • Однопроходный итератор (input iterator);
  • Однонаправленный итератор (forward iterator);
  • Двунаправленный итератор (bidirectional iterator);
  • Итератор произвольного доступа (random access iterator);
  • Непрерывный итератор (contiguous iterator);
  • Указатель.

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


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


Один из примеров создания сложной операции над диапазонами я привёл в статье Ленивые операции над множествами в C++.



Ссылки


Подробнее..

Самые полезные новинки C 20

21.04.2021 10:22:09 | Автор: admin


В сентябре прошлого года профильный комитет ISO утвердил С++ 20 в качестве текущей версии международного стандарта. Предлагаю ознакомиться с самыми полезными и долгожданными изменениями нового стандарта.

Библиотека концепций C++


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

template <список параметров>concept concept-name = constraint-expression;...<i>// concept</i>template <class T, class U>concept Derived = std::is_base_of<U, T>::value;

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

#include <string>#include <cstddef>#include <concepts>template<typename T>concept Sorter = requires(T a) {{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;};struct asdf {};template<Sorter T>void f(T) {}int main() {using std::operators;f(abcs); <i>// Верно, std::string удовлетворяет условиям Sorter</i><i>//f(asdf{}); // Ошибка: asdf не удовлетворяет условиям Sorter</i>}

Вслед за директивами #include следует объявление концепции Sorter, которой удовлетворяет любой тип T такой, что для значений a типа T компилируется выражение std::hash{}(a), а его результат преобразуется в std::size_t. Если в main вызвать f(asdf), то получим вполне осмысленную ошибку компиляции.

main.cpp: In function 'int main()':main.cpp:18:9: error: use of function 'void f(T) [with T = asdf]' with unsatisfied constraints18 | f(asdf{}); <i>// Ошибка: asdf не удовлетворяет условиям Sorter</i>|     ^main.cpp:13:6: note: declared here13 | void f(T) {}|   ^main.cpp:13:6: note: constraints not satisfiedmain.cpp: In instantiation of 'void f(T) [with T = asdf]':main.cpp:18:9:  required from heremain.cpp:6:9:  required for the satisfaction of 'Sorter<T>' [with T = asdf]main.cpp:6:18:  in requirements with 'T a' [with _Tp = asdf; T = asdf]main.cpp:7:21: note: the required expression 'std::hash<_Tp>{}(a)' is invalid7 |   { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>|    ~~~~~~~~~~~~~~^~~cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail

Еще компилятор преобразует концепцию, как и requires-expression в значение типа bool и затем они могут использоваться как простое значение, например, в if constexpr.

template<typename T>concept Meshable = requires(T a, T b){a + b;};template<typename T>void f(T x){if constexpr(Meshable<T>){ <i>/*...*/</i> }else if constexpr(requires(T a, T b) { a + b; }){ <i>/*...*/</i> }}

Requires-expression


Новое ключевое слово в C++20 существует в двух значениях: requires clause и requires-expression. Несмотря на значительную полезную нагрузку, эта двойственность requires приводит к путанице.

В requires-expression используется тип bool, код в фигурных скобках вычисляется при компиляции. Если выражение корректно requires-expression возвращает true, иначе false. Первая странность заключается в том, что код в фигурных скобках должен быть написан на специально придуманном языке, не на C++.

template<typename T>constexpr bool Movable = requires(T i) { i>>1; };bool b1 = Movable<int>; <i>// true</i>bool b2 = Movable<double>; <i>// false</i>Главный сценарий использования <i>requires-expression</i> состоит в создании концепций, просто проверить наличие нужных полей и методов внутри типа.template <typename T>concept Vehicle =requires(T v) { <i>// любая переменная m из концепции Vehicle</i>v.start();   <i>// обязательно должна обладать `v.start()`</i>v.stop();   <i>// и `v.stop()`</i>};

Однако, у requires-expression есть и другие применения. Часто необходимо проверить, обеспечивает ли данный набор параметров шаблона требуемый интерфейс: свободные функции, функции-члены, связанные типы и т. д.

template <typename T>void smart_swap(T& a, T& b){constexpr bool have_element_swap = requires(T a, T b){a.swap(b);};if constexpr (have_element_swap) {a.swap(b);}else {using std::swap;swap(a, b);}}

Requires clause


Чтобы действительно что-то ограничить, нам нужен requires clause. Его можно применять к любой шаблонной декларации, или не-шаблонной функции, чтобы выявить является ли та видимой в определенном контексте. Основная польза от requires clause в том, его использование позволяет забыть о SFINAE и прочих странных обходных решениях шаблонов C++.

template<typename T>void f(T&&) requires Eq<T>;template<typename T> requires Dividable<T>T divide(T a, T b) { return a/b; }

В декларации requires clause возможно использование нескольких предикатов, объединенных логическими операторами && или ||.

template <typename T>requires is_standard_layout_v<T> && is_trivial_v<T>void fun(T v);int main(){std::string s;fun(1); <i>// верно</i>fun(s); <i>// ошибка компиляции</i>}

Из-за двойственной сути ключевого слова requires могут возникать ситуации с эталонным неудобочитаемым кодом.

template<typename T>requires Sumable<T>auto f1(T a, T b) requires Subtractable<T>; <i>// Sumable<T> && Subtractable<T></i>auto l = []<typename T> requires Sumable<T>(T a, T b) requires Subtractable<T>{};template<typename T>requires Sumable<T>class C;template<typename T>requires requires(T a, T b) {a + b;}auto f4(T x);

То самое requires requires, первое знамением clause, второе же expression.

Модули


В C++ проглядывается долгосрочная тенденция, которая выражена в постепенном исключении препроцессора. Считается, что это избавит от целого ряда трудностей:

  • заголовки, зависящие от порядка включения;
  • утечка макросов из заголовочных файлов;
  • повторная компиляция одного и того же кода;
  • циклические зависимости;
  • плохая инкапсуляция деталей реализации.

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

Вот как выглядит модульный Hello World!..

<i>//module.cpp</i>export module speech;export const char* get_phrase() {return Hello, world!;}<i>//main.cpp</i>import speech;import <iostream>;int main() {std::cout << get_phrase() << '\n';}

Сопрограммы


Сопрограммой называется функция, которая может остановить выполнение, чтобы быть возобновлённой позже. Такая функция не имеет стека, она приостанавливает выполнение, возвращаясь к вызывающей инструкции. C++ 20 предоставляет практически самый низкоуровневый API, оставляя все прочее на усмотрение пользователя.

Функция является сопрограммой, если в её определении используется одно из следующих действий.

  • оператор co_await для приостановки выполнения до возобновления;

task<> tcp_echo_server() {char data[1024];for (;;) {size_t n = co_await socket.async_read_some(buffer(data));co_await async_write(socket, buffer(data, n));}}


  • ключевое слова co_yield для приостановки выполнения, возвращающего значение;

generator<int> iota(int n = 0) {while(true)co_yield n++;}


  • ключевое слова co_return для завершения выполнения, возвращающего значение.

lazy<int> f() {co_return 7;}


Сопрограммы не могут использовать простые операторы return, типы auto, или Concept и переменные аргументы.

Оператор KK


В C++ 20 появился оператор трехстороннего сравнения <=> и сразу получил прозвище spaceship operator, что означает оператор космический корабль. Данный оператор для двух переменных a и b определяет одно из трех: a > b, a=b или a < b. Оператор <=> можно задать самостоятельно, или компилятор автоматически создаст его для вас.

Проще всего понять на примере для чего именно нужен новый оператор трехстороннего сравнения.

#include <set>struct Data{int i;int j;bool operator<(const Data& rhs) const {return i < rhs.i || (i == rhs.i && j < rhs.j);}};int main(){std::set<Data> d;d.insert(Data{ 1,2 });}


Возникает такое впечатление, что многовато кода bool operator< для простого оператора ради того, чтобы не возникло ошибок компиляции. Ну, а если нужны и другие операторы: >, ==, , неудобно каждый раз выводить весь этот блок. Теперь же благодаря оператору <=> то же самое мы получаем более простым способом.

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

#include <set>#include <compare>struct Data{int i;int j;auto operator<=>(const Data& rhs) const = default;};int main(){Data d1{ 1, 4 };Data d2{ 3, 2 };d1 == d2;d1 < d2;d1 <= d2;std::set<Data> d;d.insert(Data{ 1,2 });}




Наши серверы можно использовать для тестирования и продакшена на плюсах.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Хочу больше годных профстатей, Хабр

21.06.2021 10:15:25 | Автор: admin

Листая страницы Хабра, поймал себя на мысли, что я воспринимаю Хабр как новостную ленту в социальной сети. То есть как нечто, что прямого отношения лично ко мне не имеет и касается меня очень косвенным путем. Нечто полуразвлекательное-полупознавательное.

Ну, судите сами. Вот примерный список тем, которые превалируют на Хабре.

  1. Что там новенького у Илона Петровича Маска.

  2. Как с помощью Arduino, говна и палок сделать годный фаллоимитатор радиоприемник.

  3. Как я ушел с прошлой работы, и как мне было там плохо.

  4. Как я нашел свою текущую работу, и какая она крутая.

  5. Как живется специалисту X в стране Y.

  6. Какой путь нужно проделать фельдшеру из Ангарска, чтобы стать тестировщиком мобильных приложений в Ирландии.

  7. Обсуждение новомодной платформы для веб-разработки, которая через 3 года станет старомодной.

  8. Промываем косточки крупным компаниям.

  9. Исторические экскурсы в IT/технологии/медицину.

  10. Реклама компаний.

  11. Мнения обо всем отвлеченном на свете.

  12. И т. д.

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

Я давно программирую на С++/qt. Думаю, что могу создать с нуля любой программный продукт (desktop) . Могу набирать команды, могу выстраивать отношения с заказчиками и т.д. Периодически приходится запускать (создавать) новые направления/программные продукты. Однако время, затрачиваемое мной и командой на каждый новый продукт, остается примерно постоянным. Вернее, не совсем так. Время суммарной работы оказывается в прямой пропорциональной зависимости от сложности продукта (объема кодовой базы). То есть постоянной величиной оказывается производительность работ, или эффективность труда. На всякий случай оговорюсь, что речь не идет о новичках, в команде только опытные толковые сотрудники.

Так вот, эту самую производительность труда очень хотелось бы увеличить. Как и в чем мне может помочь Хабр? Или я зря надеюсь?

Мне хотелось бы поднимать свой профессиональный уровень серьезными профессиональными статьями по проектированию/созданию/ведению больших продуктов. Статьями, которые по легкости восприятия были бы такого же классного уровня, как и сейчас большинство статей на Хабре. Но статьями, которые по глубине и полезности были бы как классические книги Скотта Майерса, банды четырех, Алана Купера, Роберта Мартина и др. Знаете, читая эти книги, я прибавлял каждый раз в квалификации. К сожалению, читая статьи на Хабре, я этого не чувствую. Даже более того: не могу припомнить случая, когда я хотел изучить какой-то новый для меня (и обычно нетривиальный) нюанс и находил бы его на Хабре. Я находил его где угодно, но только не на Хабре. Или вообще не находил.

Посему я очень жду и буду приветствовать появление на Хабре статей по следующим направлениям.

Новые шаблоны проектирования (С++)

Да, я знаю, что шаблоны не догма и не панацея, и всё всегда можно придумать и самому. Но я также знаю, что это проверенная годами экономия времени архитекторов и программистов. Это кругозор, который (при его наличии) позволяет делать сложную работу быстрее, а то и вообще моментально. У меня сложилось ощущение, что в мире С++ развитие шаблонов практически остановилось на известной книге банды четырех. Там описано 23 шаблона, и еще примерно столько же можно накопать в интернете. Я хочу больше! В моей практике были случаи, когда приходилось создавать шаблоны, разительно непохожие на известные. И приходилось тратить довольно много времени на приведение их к товарному использованию, хотя бы даже на продумывание такой терминологии, которая бы естественно бы воспринималась коллегами. Уверен, что если бы мы имели возможность в нужный момент найти и прочитать описание свежего шаблончика, наша работа местами была бы намного быстрее.

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

Кстати, по шаблонам есть фундаментальный труд POSA: 5-томник на 2000+ страниц, перевода на русский язык до сих пор нет. Чем не непаханное поле?

Шаблоны ведения проектов в Git

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

  1. Я веду маленький проект на 20 тысяч строк кода, в нем задействовано 5 человек, с системой контроля версий мы работаем вот так (и описать конкретно, вплоть до команд командной строки).

  2. Я веду неплохой проект на 100 тысяч строк кода, в нем задействовано 10 человек, и мы работаем вот так: схемы, команды git.

  3. Мы с тремя командами по 10 человек развиваем проект на 1 миллион строк кода, продаем продукт разным клиентам в разных конфигурациях, и всё свое хозяйство мы покрыли регрессионным тестированием. И для этого мы делаем вот это: схемы, команды git.

  4. У нас работает 200 человек, и у нас 10 миллионов строк кода в монорепе на 5 продуктов, и каждый продукт ещё поставляется в трех разных версиях. Мы опытным путем пришли, что только так: схемы, команды git.

  5. А у нас очень много кода и много микросервисов, каждый в своем репозитории, впрочем, есть и подрепозитории (подмодули). Часть кода покрыта тестами, часть нет. Раньше мы работали вот так, а теперь перешли на вот это (схемы, команды), но по эффективности труда одинаково приемлемо.

GUI

Дизайн GUI, новые подходы, примеры удачных и неудачных виджетов, примеры плохих и хороших интерфейсов/решений. Эта область очень динамично развивается в плане того, что мы видим на экранах своих смартфонов и мониторов (причем на смартфонах более динамично, чем на мониторах). Однако в плане научного описания, осмысления и систематизации всё как-то вяло. То есть решений полно, но ими никто не делится. А если и делится, то как-то не очень доходит до конкретных исполнителей, и те пишут с нуля. Думаю, ситуация бы улучшилась, если бы появилась серия статей в таком духе:

  1. Камрады, я внедрил к себе гамбургер-меню на 500 строк (qt) вот отсюда (ссылка). Вот, смотрите: скриншот, gif-анимация. Работает чётко! Лицензия LGPL. Короче, пользуйтесь все.

  2. Я создал свой виджет ввода паролей. Он круче, чем другие по таким-то причинам. Делюсь! Ссылка на репозиторий, скриншоты.

  3. Я смог объединить панели меню и тулбаров в одной панели. Получился принципиально новый виджет, похожий одновременно и на меню, и на тулбары. Область применения вот такая, описываю. Скриншоты, описание даю, а вот код нет!

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

  5. Раньше у нас был такой интерфейс, и такие задачи. А потом добавилась еще одна, и мы в корне переделали интерфейс. Рассказываю, почему прошлый интерфейс был оптимален, а текущий супероптимален.

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

  7. Я супермегадизайнер, и на примере 30 известных приложений за последний год объясню вам, что попало в тренд, что не попало, а что создает будущий тренд.

Успешный опыт решения важных и нетривиальных задач из самой близкой действительности

Знаете, меня не сильно цепляют новости, о том, какие планы у Джеффа Безоса на космос или как Boston Dynamics обучает своего пса. Это не увеличивает мою зарплату. Я хочу чего-то более близкого и понятного мне, но самое главное применимого в моей работе.

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

Сложные ли это задачи? Непростые, но решаемые. И некоторые из них решены многократно самыми разными производителями софта.

Давайте еще пример. Я запускаю приложение X и параллельно еще парочку для полного занятия вычислительных ресурсов (например, конвертор видео). И вот в приложении X вижу слайд-анимацию, который замечательно себя ведет (нисколько не тормозит) при том, что соседние приложения на 100% заняли мой процессор. Это неплохой результат для X, и, черт возьми, я хочу знать, как они этого добились.

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

Вместо послесловия

Изложенные мной хотелки уже присутствуют в некотором небольшом количестве на Хабре, а помимо этого, подобную информацию по крупинкам можно собирать на конференциях, искать в книгах и некоторых научных журналах. Но трудозатраты слишком велики! Бывает, читаешь книгу в 500 страниц, чтобы найти хоть намек на решение, а его там не оказывается. Или, того хуже, оказывается, но такой, что лучше бы его не было Статьи в научных журналах часто оказываются написанными не с целью поделиться со всем миром своими достижениями, а просто формально отчитаться за очередной грант. Да и вообще, кажется, что печатные научные журналы по программированию эволюционно проигрывают интернет-платформам.

В общем, хочется надеяться, что Хабр сможет подобрать это непростое знамя нести серьезную, неразвлекательную науку в профессиональные сообщества, но делать это легко и изящно. И создавать условия для современных Скоттов Майерсов, готовых писать статьи для русскоязычной аудитории и потом компилировать их них свои бестселлеры. Надеюсь, жду, верю И всем не болеть!

Подробнее..

Бьярне Страуструп о контроле над языком, удалении старых функций, заимствованиях у Rust и многом другом

28.10.2020 12:04:04 | Автор: admin

Этим летом на онлайн-конференции C++ Russia побывал самый почетный гость, какой только может быть на мероприятии по C++: создатель этого языка Бьярне Страуструп. Мы поговорили с ним о контроле над языком, нововведениях, удалении старых функций, о том, почему у языка нет четкой экосистемы, и как так получилось, что у С++ нет стандартной сетевой библиотеки.


А поскольку сообщество C++ вечно обсуждает Rust, не обошлось и без вопроса о заимствованиях между этими языками.



Бьярне не нуждается в особых представлениях, но у него столько активностей и заслуг, что попробуем перечислить:
написал множество публикаций, в том числе книги Язык программирования С++ и Программирование: Принципы и практика использования C++
работает управляющим директором в отделе технологий компании Морган Стенли в Нью-Йорке
преподает в Колумбийском университете
принимает активное участие в работе комитета по стандартизации C++
состоит в Национальной академии инженерии США, IEEE, Ассоциации по вычислительной технике. Исследует программирование в области распределенных вычислений, инструменты развития ПО и языки программирования.
Магистр и заслуженный профессор Орхусского университета и доктор по направлению Информатика Кембриджского университета и почетный член научного сообщества Колледжа Черчилля, заслуженный профессор Санкт-Петербургского ИТМО


Вопросы ему задавали С++ программист, организатор С++ Siberia Сергей Платонов и руководитель разработки бэкенда Яндекс.Лавки Сергей Федоров. Под катом перевод, а также видеозапись интервью для тех, кто хочет послушать на английском.



Об истории


Сергей Платонов: Спасибо, что присоединились к нам. Говорить с вами большая честь. У нас есть несколько вопросов от сообщества, но хотелось бы начать с тех, что проще и короче. Начнем с истории, поскольку об этом часто спрашивают. В первую очередь хотелось бы сказать, что вы самый упоминаемый спикер. Каждый раз, когда мы спрашиваем у зрителей, кого следует пригласить на конференцию, первым называют ваше имя. Зрителям хотелось бы поучаствовать в обсуждениях, в живом общении. Не могли бы вы в паре предложений рассказать, как вы стали создателем C++? С чего все началось? Мне кажется, все читали Дизайн и эволюцию, но если возможно, хотелось бы услышать ее укороченную версию.


Бьярне: Я пришел к C++, когда захотел создать систему для Bell Labs, которая бы стала первым UNIX-кластером. Это была бы система для распределенных вычислений, объединенная в общую сеть из нескольких компьютеров. Мне предстояло написать код, который бы работал на нижнем уровне, на уровне железа: диспетчеры памяти, планировщики процессов, сетевые интерфейсы и все в таком духе. И при этом нужно было уйти от железа. Мне нужно было перейти на высший уровень, чтобы заставить один код взаимодействовать с другим, используя определенный протокол передачи данных определенным образом.


Итак, мне нужен был язык программирования, который бы работал и на низшем, и на высшем уровне. В то время было как минимум с десяток языков, которые справлялись с задачами низшего уровня, C был одним из них. Лаборатория Денниса Ритчи и Брайана Кернигана находилась в одном коридоре с моей, поэтому дальнейшие действия казались очевидными, но на тот момент я подумал, что этот язык работал со слишком низким уровнем. Мне были нужны механизмы абстракции. У меня был опыт работы с Симулой, о которой я узнал от Кристена Нюгора. И я взял классы из Симулы, перенес их в C, сделав его достаточно эффективным для программирования сложных систем. На второй неделе работы над проектом я добавил конструкторы и деструкторы. Тогда они были нужны, в том числе для управления ресурсами. Затем я добавил то, что сейчас называется прототипами функций и проверку аргументов функции.


Основой C++ стала его возможность эффективной абстракции, ядром которой являются статическая проверка типов, конструкторы и деструкторы, что в совокупности позже начало именоваться как RAII (Resource Acquisition Is Initialization, получение ресурса есть инициализация). В 80-е у меня было совсем мало времени на изобретение хороших названий для хороших технологий, лучше, чем RAII, я ничего не придумал, так что уж имеем то, что имеем.


Контроль над языком


Сергей П.: Вы не помните тот момент, когда C++ ушел из-под вашего контроля в руки сообщества?


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


Потом, где-то спустя 10 лет, люди из IBM и Sun, а также представители HP пришли ко мне в офис и сказали: Знаешь, Бьярне, надо сделать так, чтобы C++ соответствовал нормам ISO. Я тогда объяснил, что это невозможно. Было слишком рано, я не был готов. После часового разговора я согласился, так как выбора у нас не было. Они объявили, что не могут позволить своим компаниям зависеть от чего-то, что зависит от их потенциального конкурента, например, AT&T. Помню, они сказали: Мы, конечно, тебе доверяем, но скорее всего мы назначим тебе начальника. И тогда мы начали стандартизацию.


В целом, процесс стандартизации очень непрост и сильно раздражает. Многие стороны со своими потребностями должны прийти к единому мнению относительно вещей, на которые у всех разные взгляды. Спустя годы мне кажется, мы все сделали совсем недурно. Сегодня у нас есть проблемы посерьезнее. Тогда собиралось 60 человек, все друг друга знали, было доверие. Сейчас у нас 230240 человек, мы не можем хорошо узнать друг друга. Работа сейчас строится иначе, и она стала труднее. И, конечно же, в этом году нам приходится связываться через Интернет, потому что собрания запрещены. Пройди встреча комитета по C++ в течение недели в одном небольшом помещении, мы бы точно потеряли часть людей языку это бы не пошло на руку.


Работа в комитете


Сергей Федоров: Не могли бы вы рассказать, какую позицию в комитете занимаете на данный момент?


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


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


Итак, корабль следовало построить к сроку. Они успели. После создания чего-либо необходимо это что-либо испытать. Боевые корабли проходили испытания по следующему принципу: команда корабля в полном составе встает у одного борта. Затем по команде бежит к другому борту, добежав, разворачивается и бежит обратно. И так 14 раз. Если корабль не переворачивается, значит, он прошел испытания. Команде удалось сделать 7 пробегов, а затем испытание остановили из страха, что он опрокинется. Собственно, испытания провели не полностью, сэкономив на этом. Очень зря. Как бы там ни было, в день спуска на воду ничего не предвещало беды: развевающиеся флаги, духовой оркестр, прекрасные статуи, гром холостых выстрелов. Корабль проходит через бухту в Стокгольме, которая является серединой маршрута. Но вдруг порыв ветра переворачивает корабль, который в итоге идет ко дну. Спустя 350 лет его достали со дна и разместили в красивом музее в Стокгольме. Если когда-нибудь будете в этом городе, обязательно посмотрите на корабль Ваза. Он очень красив и выглядит впечатляюще.



Несколько лет назад мы собирали комитет по стандартизации в Стокгольме. Тогда мы все дружно пошли в музей, чтобы посмотреть на корабль, и тогда же на долгие годы вперед закрепился наш девиз: Не забывайте, что случилось с "Вазой". Правильно проработайте базу, не экономьте на испытаниях и не заказывайте себе слишком много красивых статуй. Этот девиз начал постепенно забываться, и я пытаюсь напоминать о нем людям. Может быть, нам стоит еще раз собраться в Стокгольме.


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


Нововведения


Сергей Ф.: К слову о красивых новых пушках, в новых стандартах C++ присутствуют функции, которые вас особенно раздражают?


Бьярне: Да нет. Мне кажется, C++20 будет отличным стандартом, мы наконец-то воплотили в жизнь концепции. Я бы хотел внедрить их еще в 19861988 годах, когда я создал шаблоны, но не знал, как это сделать. Мы попытались в стандарте C++11, но у нас не получилось все сделать как надо. Я надеялся, что к стандарту С++17 все будет готово, поскольку у нас появилось необходимое понимание, но в итоге, они появятся лишь в С++20. У нас наконец-то появится нормальное обобщенное программирование. Благодаря этому выражения станут четче, проще, и все будет работать намного быстрее. Добавьте к этому constexpr, и вы получите сильно упрощенное обобщенное программирование и сильно ускоренное метапрограммирование во время компиляции.


Другое нововведение, которым я очень доволен, это сопрограммы. Сопрограммы были в основе C++ в первые 10 лет. Я тогда писал симуляции и то, что сегодня называют многопоточными программами, с помощью сопрограмм. Мои симуляции были меньше и быстрее любых аналогов. А также они были меньше и быстрее потоков. Здорово, что мы вернули сопрограммы. Мы когда-то ушли от них потому, что компания Sun не хотела внедрять их в архитектуру Spark, поскольку очевидная реализация управляет стековыми фреймами. Сегодня же мы имеем более быстрые сопрограммы.


И теперь перейдем к последней функции, которая, скорее всего, будет играть важнейшую роль в долгосрочной перспективе. Это модули. По сути, это относится к организации программы, а не к написанию кода. Если вернуться во времена Windows DNA, я помню, как я мечтал о нормальном методе разнесения программ по частям. Нам приходилось пользоваться заголовочными файлами. Тогда в 80-е и 90-е мы знали, как лучше. Введение новых возможностей в язык с несколькими сотнями миллиардов строчек кода не так просто, но мы сделаем это. Итак, модули. Они значительно повысят скорость компиляции. Я видел, как скорость возрастает в 50 раз, если сравнивать с методом использования заголовочных файлов. Надеюсь, что большинство программ ускорится, скажем, в семь раз. И, наконец, мы нашли способ предотвратить распространение макросов по всей системе. Вы помещаете макрос в заголовочный файл, кто-то затем включает ваш заголовочный файл в свой файл, затем этот файл включается в следующий файл, и так ваш макрос присутствует в конечном файле, создатель которого даже не знает о существовании вашего заголовочного файла, из которого появился макрос. С появлением модулей эта проблема уйдет. Теперь код чище, и именно благодаря этому повышается скорость компиляции. Думаю, эти три функции самые важные, они будут в значительной мере влиять на написание кода в следующие десять лет.


Есть небольшая проблема отсутствия поддержки стандартной библиотеки. Сегодня не существует одного стандартного набора модулей, кроме того, сопрограммы не поддерживаются библиотеками в полной мере. В стандартной библиотеке есть некоторые концепции, поэтому, я считаю, у нас все будет хорошо. Надеюсь, в стандарт С++23 будут включены все новые возможности. Трудно обеспечить библиотеками то, что пока не внедрено. Мы решили, что сначала введем новые функции в язык, чтобы опробовать их в деле в разных масштабах. Нам нужны программы с миллионами строчек кода в них новые возможности будут использоваться в полной мере. Затем, набравшись опыта, мы доработаем их в стандарте С++23, и их использование станет проще и, наверное, быстрее.


Удаление старых функций


Сергей П.: Что насчет функций, которые не используются в языке программирования? Вам не кажется, что некоторые из них можно было бы удалить? И поскольку у нас есть трехсекундная задержка, я позволю себе перефразировать вопрос. У комитета есть какие-то возможности по удалению старых функций из языка? Я спрашиваю потому, что язык становится все объемнее, а следовательно, труднее для изучения. Людям все труднее поспевать за появляющимися нововведениями.


Бьярне: Этот вопрос касается большого и сложного дела. Не думаю, что в C++ есть какая-либо основополагающая функция, которую мы могли бы удалить, не разозлив несколько сотен тысяч или даже миллион пользователей. Такого не должно произойти. Если посмотреть на этот вопрос философски, я могу сказать, что основные функции языка хороши, другое дело мелкие детали. Но с этим мы ничего поделать не можем. Мы разрабатываем язык, смотрим на то, как он функционирует, опираясь на отзывы. Однако, как бы то ни было, когда мы вводим что-то масштабное, убрать это нововведение не так просто. Несколько раз мы пытались удалить старые возможности, и каждый раз у нас не получалось. В рамках стандарта ISO мы можем сократить функционал языка с помощью так называемой депрекации, объявляя функции устаревшими. Мы объявляем пользователям, что одна из функций языка больше не будет использоваться и мы удалим ее через 3510 лет. Этот подход не работает с основополагающими функциями.


В течение десятилетия нам никак не удавалось убрать триграфы. Сейчас мы от них избавились, при этом большинство даже никогда не слышало о триграфах. Оказалось, что каждая машина в IBM не может запуститься без триграфов. Убрать триграфы языка убить IBM. Такого не должно произойти, да и в IBM любят цепляться за старые фишки. И снова нам приходится сообщать о том, что мы удаляем возможности, что ими не стоит пользоваться. Нам было очень сложно избавиться от правила implicit int, хотя в C++ его никогда и не было, оно пошло от C.


Все мечтают избавиться от тех вещей, которые им не нравятся, от всего устаревшего и прочего. Но договориться о том, что устарело, что не нужно, никто не может. Получается, что не существует согласия между 4,5 млн программистов, пишущих на C++. Более того, его никогда и не будет. Добавьте к вышесказанному и то, что после удаления функции из языка, поставщики компиляторов будут вынуждены поддерживать ее еще лет 2030. В общем, что-то убрать почти невозможно. Даже если мы придем к согласию, все равно удаление не пройдет легко.
Сегодня мы не можем поменять или убрать основополагающие функции, при этом тратить время на удаление каких-то мелких функций кажется совсем смешным. Так как же заставить людей двигаться дальше? Во-первых, обучить их писать на современном C++. Сегодня все еще много тех, кто начинает изучать C и загоняет себя в ловушку бессмысленного распределения памяти, вместо того, чтобы обратить внимание на векторы, RAII и прочее. Во-вторых, нам нужно поддерживать современный подход к программированию, который проще и благодаря которому создаются более быстрые и менее объемные коды. Когда кто-то начинает писать операции над строками в стиле из C, он обычно получает баги и медленный код. Эксперты справляются лучше, но у экспертов, как правило, есть дела поважнее.


Я работаю с друзьями над C++ Core Guidelines. Если им следовать, можно сказать, набору правил, а не беспорядочно использовать все функции языка, ваш код будет лучше и вы пойдете спать раньше. Да, это правила. Нам всем не нравятся правила, их трудно запомнить, поэтому для них нужна поддержка. Правила отчасти зависят от стандартной библиотеки и от 10 небольших компонентов из GSL (guidelines support library), которые мы хотим перенести в стандарты, чтобы избавиться от необходимости в GSL.


Например, мы добавили span в стандарт C++20. Span это простая абстракция, по сути, указатель плюс размер. Если нужна ссылка на непрерывный набор элементов, используете span, который знает расположение элементов и их количество, поэтому вы можете задать диапазон и, при желании, произвести проверку диапазона. При этом у вас нулевой шанс ошибиться.
Предположим, у нас есть массив, мы берем span, присваиваем ему значение массива. Span обратится к массиву, поймет, какие в него входят элементы, они получат тип span-а. Далее он узнает количество элементов в массиве, возьмет этот размер. Все, вы получаете правильную абстракцию непрерывной последовательности битов, как с объектами, и это вам необходимо для коммуникационных программ и прочих низкоуровневых дел. Деннис Ритчи хотел сделать что-то похожее в C, но ему сказали, что ничего не выйдет. Это все не ново, но полезно. Это одна из мелочей, которая реально помогает в работе.


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


А при включении другой статической проверки, первая будет предотвращать переполнение буфера. Например, при использовании указателей вы рано или поздно придете к переполненному буферу. При использовании общих указателей рано или поздно дело придет к висячему указателю. Например, согласно нашим правилам, вы должны использовать span вместо указателя. Используете RAII, затем включаете проверку (сейчас она развертывается в Visual Studio, поэтому то, чем я говорю, реальность) и избавляете себя от всех проблем с памятью. Проверка обнаружит все проблемы.


Я бы хотел, чтобы проверка была доступна для использования вне Visual Studio, чтобы этот инструмент можно было использовать на любой платформе. К сожалению, такого пока нет. Мы пришли к соглашению, что однажды эта проверка будет доступна для любой платформы, правда, пока не ясно, когда это случится. Сейчас же приходится запускать Visual Studio и прогонять программы через нее. Всегда лучше работать с несколькими компиляторами, чтобы защититься от багов самого компилятора.


Об экосистеме


Сергей Ф.: Почему, по вашему мнению, у С++ нет четкой экосистемы?


Бьярне: Потому что у языка нет законного владельца. Нет одной компании, которая бы все контролировала. Каждый создает что-то свое. Мы начинали вообще без финансирования, если говорить об исследовательских проектах в Bell Labs. Я самостоятельно делал 98% работы по первому коммерческому запуску языка: составлял документацию, учебные библиотеки и прочее. Наш рекламный бюджет тогда был 5000 долларов на три года, и тогда я вернул 1000, потому что не знал, как правильно распорядиться средствами.


Многие не понимают, что у C++ никогда не было бюджета. Вместо денег было сообщество, причем не одно сообщество, а множество. Проблема состоит не в отсутствии графического интерфейса, а в том, что их 25. Проблема не в том, что у нас нет системы сборки, а в том, что их 7. Это я еще преуменьшаю. Люди не могут прийти к единому мнению. Все принадлежат к разным сообществам. Все хотят делать по-своему, никто не хочет слияний. Некоторые проблемы лежат в коммерческой плоскости, некоторые части имеют открытый код, аспекты вызывают проблемы из-за большого самомнения некоторых лиц. В итоге у нас демократический хаос, и я не знаю, как его прекратить.
Возможно, если бы я обладал лучшими навыками организации, мы могли бы еще что-то сделать, когда это было возможно, а возможно это было в 80-е. Правда, в 80-е и особенно в 90-е все хотели создать свое сообщество, организованное вокруг своего решения и своего подхода. Тогда в игре были Microsoft, IBM и еще пара-тройка серьезных компаний. Все пытались создать что-то вне конкуренции, чтобы заполучить побольше пользователей. Компании тогда приходили к решениям, которые были несовместимы с решениями конкурентов, чтобы потребители вдруг не ушли к последним. У нас же было наибольшее количество инструментов, и в большинстве случаев, это не является преимуществом. Как определиться с системой сборки? Как определиться с графическим интерфейсом? Сложно. Подумайте, каково было новичку.


Сергей П.: У нас нет простой системы сборки, которая будет без проблем работать в любой системе.


Бьярне: Нет-нет, мы можем предложить вам как минимум три, которые плюс-минус подходят под ваши критерии.


О рейтингах и популярности языка


Сергей П.: Вам важно то, какое место C++ занимает в рейтингах? Следите ли вы за популярностью языка, за какой-либо статистикой? У нас на конференции будет отдельный разговор о C++ и его месте в мире.


Бьярне: Не думаю, что до конца понял вопрос. Попытаюсь ответить на него и, если я уйду не в ту сторону, поправьте меня. Я стараюсь сосредоточить внимание на самом языке и стандартной библиотеке. Я не лучший собеседник по темам вселенной C++, плеяды доступных инструментов, рекламных аспектов или открытого кода. Слишком много информации для одного человека. И я не обращал внимания на статистику. Ее очень сложно получить. Сложно считать пользователей.
Хорошо, что кто-то этим занимается. Проводятся опросы, часть информации доходит и до меня. Поэтому-то я и оперирую цифрой в 4,55 миллионов разработчиков C++. Но кто такой разработчик? Кто такой программист? Учитывать ли студентов? А всех студентов или только очников? Все эти цифры очень условны, но я, как по мне, опираюсь на самые скромные подсчеты. Я бы мог назвать и число побольше, но не собираюсь, поскольку тогда мы будем учитывать тех, кто пользуется C++ не постоянно, а это не лучший способ подсчета.


Можно спросить у тех, кто может сказать, сколько у них пользователей потому, что они как минимум могут подсчитать количество заплативших за продукт или число скачиваний, что, в целом, тоже не лучший метод подсчета. Я раньше руководствовался такой логикой: если люди не жалуются, значит, они не пользуются C++. Конечно, у нее есть свои минусы. Это я тоже знал. Компания AT&T решила провести опрос пользователей об их опыте, поскольку была необходимость следить за распространением языка, хотя он и был бесплатным. Они обнаружили, что 4 из 5 разработчиков используют C++. Это расходилось с моими подсчетами, в которых я не учитывал тех, кто не жаловался. В общем, считать проблематично.


Сетевая библиотека


Сергей Ф.: Вы говорили, что начали разрабатывать C++, когда вам понадобилось средство, чтобы преодолеть сложность организации сетевой системы. Как же так получилось, что у C++ нет стандартной сетевой библиотеки?


Бьярне: Опять же, проблема не в том, что нет сетевой библиотеки. У нас нет стандарта. С середины 80-х появлялось много библиотек. Сегодня многие пользуются библиотекой Asio, которую технически можно назвать стандартной. Думаю, что в C++23 мы добавим её. Но начинаются разговоры, что нужно больше поддержки. Помимо Asio, нужно добавить то, чего не дает Asio. И это обыденный хаос в мире C++. У нас даже нет стандарта для места хранения библиотек. Никто как следует не позаботился о создании хорошего сообщества C++. Даже удивительно, как этот язык пережил десятилетия конкуренции. Я написал статью о последних 15 годах развития C++ для конференции по истории языков программирования. Эта статья стала доступна буквально две недели назад на сайте Ассоциации по вычислительной технике. Поищите его там. Или можете пройти на мою страничку и найти ее в разделе со статьями. В статье я объясняю все события, произошедшие за последние 15 лет. Она называется Thriving in a Crowded and Changing World. В ней описываются последние 15 лет развития C++, работа над стандартами 11, 14, 17 и 20, а также некоторые проблемы сообщества. На большую часть заданных сегодня вопросов можно найти более глубокие ответы именно в статье. Сейчас я не могу отвечать в той же степени пространно. В интервью все рассуждения так или иначе абстрактны, нет возможности подтвердить слова документацией, но большая часть сказанного мною встретится вам в названной статье, оцененной другими учеными. Она не короткая. В ней 168 страниц, 30 из которых занимает список источников. На ее прочтение стоит выделить выходные.


О заимствовании фишек


Сергей П.: Как вы думаете, Rust оказывает влияние на C++ или, может быть, начнет в обозримом будущем? Может ли случиться так, что C++ позаимствует некоторые функции Rust?


Бьярне: По-моему, это скорее Rust заимствует фишки C++, а его разработчики забывают об этом рассказать. RAII был в C++ на протяжении 40 лет. Меня иногда обвиняют в том, что я своровал эту концепцию. Сообщество, конечно, не сильно опирается на академические труды или историю. То же самое и с линейной логикой и семантикой владения, над которыми я работал на протяжении 20 лет. Это фундаментальные вещи, описанные в ключевых рекомендациях. Там же описано и то, что я задумывался об этих вещах еще с середины нулевых. Думаю, мы лучше, чем Rust, просто люди этого не понимают. Я не видел в этом языке ни одной функции, которую я бы очень хотел добавить в C++. Некоторые вещи нельзя просто включить в язык. Ключевые рекомендации могут как-то показать схожесть языка с Rust, но они были написаны задолго до него. Я знаю историю Rust и проблемы заимствования (borrowing), но это слишком сложная тема.
Я экспериментировал с этим в середине нулевых, и в результате экспериментов, код превращался во что-то страшное, слишком много всего приходилось обходить. Кроме того, С++ соотносится с C, что накладывает некоторые ограничения на совместимость, от этого заимствовать только сложнее.


Я обращаю внимание на любой появляющийся язык программирования, ладно, не на любой их слишком много. Я обращаю внимание на языки программирования с интересными фишками. Если какая-то функция мне нравится и ее можно добавить в C++, я сделаю это и дам ссылку на источник. Люди могут сказать, что это кража идеи; на самом деле, нет, ведь ты сообщаешь, у кого ты ее позаимствовал. Rust не особо привлекателен для заимствований. При этом уже поступают предложения ввести в Rust что-то вроде механизмов для объявления функций устаревшими. Как по мне, это говорит о том, что они еще не пробовали удалять ненужные функции. Да и зачем? Язык еще слишком молод. Они и не могли попробовать удалить что-то устаревшее. А со временем язык будет шириться, и это сильно осложнит задачу. Со временем появляются пользователи и достаточное количество строчек кода. Кстати, был опрос, в котором у людей спросили, на чем им больше всего нравится писать программы. Rust выиграл с отрывом. При этом, из тех, кто отдает предпочтение Rust, лишь три процента написали программу на этом языке. Я тогда удивился. Это же абсурд, какого не ожидаешь.


Сергей П.: Какой язык, кроме C++, вам нравится? Может быть, приходит в голову какой-нибудь редкий язык? Или может быть, вы расскажете об идеях, воплощение которых в другом языке вам пришлось по душе?


Бьярне: Я стараюсь избегать сравнений языков программирования. Не хочу много говорить об этом. У нас есть планы на C++23: мы хотим завершить работу над поддержкой модулей, сопрограмм и еще поработать над концепциями. Если мы все сделаем, то сможем заняться вещами на будущее. Мы, очевидно, позаимствуем из другого языка сопоставление образцов из функционального программирования. Мне нравится эта возможность, я предлагал ее добавить еще в 2015 в C++17. Мы пока собрали рабочую группу, работаем над этим. Возможно, эта возможность появится в C++23. Благодаря этой функции ускорится проверка типов и появится код курсора. Такая версия C++ будет работать и с алгебраическими типами и открытыми иерархиями, и это станет альтернативой dynamic_castу. Очевидно, что все эти идеи пришли к нам так или иначе из ML.


Еще одна важная вещь, на которую мы обратили внимание, но вряд ли сможем сделать к C++23 статическая рефлексия. Рефлексия отличная штука, которая, к сожалению, очень медленно работает, является очевидной слабостью системы типов и порождает ошибки. Мы уже работали над статической рефлексией. Суть её работы заключается в том, что код узнает от компилятора информацию о программе и на ее основе создает новый код. Тогда бы можно было сказать: дайте несколько программ для чтения и записи файлов формата JSON определенных типов. Когда этот процесс завершится во время компиляции, будет сгенерирована точная поддержка среды для определенного приложения. Сравните это с представлением вашей программы в виде графа, в случае если у кого-то появится необходимость в нем. Все предложения по нововведениям можно найти на сайте WG21. WG21 это официальное название от ISO для рабочей группы. Все предложения и статьи от 1990 года выгружаются на сайт группы.


Виртуальные функции


Сергей Ф.: Благодарю. Я большой поклонник статической типизации. Что вы думаете о виртуальных концептах или добавлении любого другого type erasure механизма?


Бьярне: Я не могу сказать, что готов ответить на этот вопрос. Я был соавтором статьи о виртуальных концептах, написанной в середине нулевых, но не думаю, что они могут сыграть ключевую роль в развитии C++. И я не большой поклонник type erasure. У нас есть виртуальные функции, некоторые считают их type erasure механизмом. И я думаю, этого достаточно. Наверное, отчасти я говорю так потому, что мы не можем воспринимать сообщество программистов как единое целое. Существует множество сообществ со своими потребностями. Конечно, гибкий и простой код хотели бы многие, но если есть нужда, например, запрограммировать тормозную систему автомобиля или органы управления самолетом или электростанцией, я бы доверил эту работу программистам, работающим рука об руку с инженерами с серьезной подготовкой и инструментами, которые настроены специально под решение их задач и предлагают производительность и надежность высшего уровня. Если речь идет о программировании обработки сигнала для описанных выше технических решений, на первое место встают производительность и надежность. Мне кажется, что 1520% участников Core Working Group (NB: подгруппа Комитета Стандартизации, занимающаяся ключевыми вопросами развития языка) принадлежат к миру высокой производительности и безотказной надежности, то есть такому, которому отлично подходит C++.


Нам не нужно пытаться сделать из C++ конкурента Python, чтобы завлечь людей, не являющихся профессиональными программистами, но являющиеся экспертами в других областях. Если же нужно добиться скорости и надежности, лучше заменить Python на C++. То же самое действует и в обратном порядке: хотите экспериментировать с C++ не ослабляйте систему типов и не ослабляйте внимание при написании выражений. Я не буду делать со своим кодом все, что мог бы с ним сделать, не зная наверняка, что мне нужно. Мне кажется, C++ не создан для 8085% программистов. Всегда стоит помнить об изначальной задумке создания языка: этот язык распределенная система с прямым доступом к железу и немного повышенным уровнем сложности. Если смотреть на вещи в таком ключе, то любая вещь, нацеленная на то, на что нацелен C++, должна обладать высокой надежностью.


Этот мир сильно отличается от того, в котором существует разработчик приложений. JavaScript хорош в решении тех задач, для которых был создан, а у меня есть правило: не говорить ничего плохого о приложениях, взявших свое начало от C++ как, например, движки JavaScript. Это два разных мира, но иногда они пересекаются. Зачастую С++ ложится в основу и дальше дополняется чем-нибудь еще.


О планах по изменению языка


Сергей П.: Есть ли планы по улучшению целостности языка ценой удаления совместимости с си? Например, можно ли избавиться от препроцессора, чтобы сделать C++ прекрасным?


Бьярне: Основная стратегия и, по-моему, единственная рабочая предоставить превосходящую альтернативу и надеяться, что устаревшие функции потеряют актуальность. Старые функции нельзя удалить из языка, но их можно удалить из собственного кода. Они могут быть, так скажем, перемещены в запас. Вы можете однажды предложить друзьям или поставщикам перестать пользоваться этими функциями, или пусть они инкапсулируют их. Модули инкапсулируют все старье. В модуле у вас может быть программа, замусоренная макросами и файлами включения. Мне, в общем-то, нет никакого дела до этих файлов включения или макросов, если я не вижу их у себя в коде. Так что эта идея очень важна. По-моему, это единственный способ двигаться вперед. То же можно проделать и с отдельными функциями. Мне, например, не нравятся приведения (casts), я редко ими пользуюсь и, если мне нужно использовать их где-то ближе к уровню железа, на программном уровне я инкапсулирую их во что-то, что использует правильные типы и следует правилам.


Сергей Ф.: Не могли бы вы рассказать, почему бы в C++ не сделать семантику перемещения деструктивной?


Бьярне: На этот вопрос будет трудно ответить в рамках обычного разговора. В целом, это не работало и более того, ломало многие работающие вещи, поэтому не может идти речи о развертывании этого в крупных масштабах. Если даже это сработает, придется приложить много дополнительных усилий. Я не могу здесь вдаваться в подробности. Для начала, мне потребуется как минимум доска. К тому же, придется обсудить несколько версий этой идеи Нет, придется слишком глубоко копать.


Сергей П.: Люди пишут, что компилятор C++ стал очень сложным. Могли бы вы дать совет, как изучать современные компиляторы, как понимать теорию и где набраться практического опыта? Компиляторы становятся все больше.


Бьярне: Компиляторы объемны и сложны. Мне кажется, сегодня все пытаются улучшить свои компиляторы, ничего при этом не сломав, как это обычно бывает. В данный момент и в Microsoft, и в GCC идет работа по модернизации промежуточного представления. Толчком к этому стали модули, которые должны быть представлены и представлены хорошо. Если проделать эту работу на совесть и представлять модули в виде минимального типизированного промежуточного представления, их работа станет намного быстрее. Если же представить это в виде множества битов, придется перекомпилировать это множество при каждом использовании модулей, что сводит на нет все их плюсы. Да, биты инкапсулированы, но они все еще не дают работать быстро.
Мне кажется всем известно, что GCC и Microsoft улучшают их промежуточные представления. Я знаю, что в Microsoft над этим работает мой старый друг, Гэбриэль Дос Рейс. Когда мы были еще профессорами в Техасе, около 10 лет назад, мы создали представление для C++, которое имело минимальный размер и требовало минимума взаимодействий. Мы дали ему название IPR. Это представление и легло в основу внедрения модулей в Microsoft. По-моему, оно становится все популярнее. Оно в разы меньше и быстрее любой альтернативы. Однако чтобы оно работало с существующими компиляторами, придется добавить кое-какие функции, отсутствующие в минималистичном дизайне этого представления, например отладочную информацию.


Я снова отправлю вас на свой сайт, чтобы вы поискали на нем статью, которая называется A Principled, Complete, and Efficient Representation of C++. Название IPR, как вы могли догадаться, придумал я в свойственной мне креативной манере, полностью оно звучит так: Internal Program Representation. Мне не нравится, когда его называют AST (Abstract Syntax Tree) потому, что это не синтаксис, не абстрактный и не древо. Но люди все называют это таким образом.


О неопределенном поведении


Сергей Ф.: C++ знаменит своим неопределенным поведением. Есть ли у комитета по стандартизации C++ стратегия, которая позволила бы снизить количество случаев неопределенного поведения?


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


Кстати, Гэбриэль, о котором я упоминал ранее, также работает и над неопределенным поведением. Он стоит во главе рабочей группы, так что да, мы работаем над этим. Просто не стоит верить людям, заявляющим, что любое неопределенное поведение плохо. Альтернативы могут быть много хуже. Наверное, стоит воспринимать неопределенное поведение как нежелательное, но помнить о том, что альтернативы будут хуже.


Списки инициализаторов


Сергей П.: Спрошу о том, что многие хотели бы не видеть в стандарте. Сейчас речь пойдет о новой функции. Во время нескольких конференций C++ Russia мы обсуждали плохую работу списков инициализаторов. Есть ли планы по решению этой проблемы?


Бьярне: Мы пытаемся их немного улучшить. Мне бы хотелось знать больше о том, что люди думают об этих сбоях. Направьте их ко мне, это сильно поможет в работе. Списки инициализаторов работают не совсем так, как я задумывал. Их улучшали, однако, как по мне, улучшения не были улучшениями. Мы постепенно делали их менее склонными к выдаче ошибок, более мощными, более полезными. Думаю, мы продолжим работу в этом направлении. Если у кого-то есть реальные примеры плохой работы, было бы интересно узнать об этом больше: всегда сложно угадывать, что беспокоит людей. Нужно встречаться, обсуждать. Поэтому-то мне и не нравятся видеоконференции. Я не могу поговорить с вами до и после конференции, не могу узнать вашего мнения относительно не оговоренных проблем, не могу попытаться вам помочь, подкинуть какие-нибудь идеи. Живое общение очень важно, я очень скучаю по возможности встречаться с людьми.


Сергей П.: Мы тоже очень хотим проводить живые встречи.


Бьярне: Да, кстати, у вас в России есть комитет по стандартизации C++. Присоединяйтесь к нему. Вы сможете посещать встречи и использовать их как канал передачи ваших идей. Учтите, мне больше нравятся проблемы, чем их решения потому, что зная проблему, я могу разработать решение. А многие, в свою очередь, смотря на свою проблему, хотят увидеть конкретное решение. При этом проблемы у всех разные, сложно решать каждую из них. Нам нужно получить информацию о большом количестве проблем и работать над решением, которое бы подходило для решения множества задач.


Сергей П.: У рабочей группы в России есть сайт stdcpp.ru, который также является форумом, где можно обсудить свою проблему с другими участниками и проголосовать за то, чтобы ее обсудили позже в комитете. У нас в рабочей группе-21 три участника представляют вашу проблему. Если она наберет популярность, они могут помочь вам с подготовкой статьи. Такое уже случалось. В комитете уже прорабатывали предложения от сообщества, например, wide_int, которое поступило через сайт. Кстати, эту идею предложил студент, а не профессиональный разработчик. Позже он стал соавтором статьи. Сейчас этот вопрос обсуждается. Мы активно просим всех участвовать в работе, которая обычно доступна для публичного просмотра. Более того, мы организуем встречи в Москве и приглашаем принять в них участие.


Бьярне: Это великолепно. Однако, я бы хотел попросить вас напоминать людям, что большинство языков программирования умерли. У нас было как минимум 10 000 языков, которые полностью удовлетворяли своих создателей. Они в итоге исчезли. Успех языка программирования не равен победе в конкурсе популярности. Нужно думать о большем сообществе. Нужно понимать, что с вашей проблемой столкнулись и программисты в Калифорнии, Ухани, Бангалоре и других местах. Ваша проблема может быть такой же проблемой и в других сферах применения языка. Более того, многие пишут на C и C++ потому, что это старые языки. Код, написанный 10 лет назад, все еще исполняется. Это дает людям уверенность, что код, который они пишут сегодня, будет исполняться через 10 лет. Многие пользователи, большие компании думают о выживании своего кода. Поэтому я делаю акцент на совместимости. Мы не можем прийти к улучшениям ценой нарушения совместимости. Миллионы сегодня хотят и новых фишек, и избавления от старья, и чтобы код работал.


Инструменты Бьярне


Сергей Ф.: Справедливо. Какими инструментами вы обычно пользуетесь? Вы пишете программы?


Бьярне: Пишу программы каждый день. У меня есть два набора инструментов. У меня два экрана. Один от компьютера на Linux, иногда я пользуюсь кластером Linux, там GCC, система сборки и прочее. Второй компьютер на Windows, там у меня Visual Studio и остальная мелочь. Я говорю мелочь, но компьютеры не маленькие в последнем установлен i7. Я еще пользовался Mac, но мне никак не удавалось продуктивно работать с тремя разными системами, поэтому остался с двумя. Как по мне, пользоваться одной системой очень опасно для программиста и особенно дизайнера языка. Вам кажется, что эти инструменты отличный выбор, с ними все в порядке и все должны ими пользоваться. Опасный подход.


Сергей Ф.: Если у вас есть кластер на Linux, почему вы не используете систему непрерывной интеграции, чтобы пользоваться разными наборами инструментов?


Бьярне: Может, я и использую, это корпоративные машины. Кстати, компьютер на Windows может взаимодействовать с миром без каких-либо проблем с безопасностью. Я работаю в банке. Многие хотели бы взломать банк, в конце концов, там лежат деньги, поэтому о безопасности нужно думать. Если же мне взломают персональный компьютер, я расстроюсь, но миру это не нанесет серьезного ущерба.


Есть еще одна машина, на которой я могу запустить любую систему, могу загружать в нее. На ней запускается компилятор от Microsoft, Clang и GCC. Ладно, не буду продолжать. Хотелось бы еще машину на Intel и еще парочку, но поддерживать их в работе все равно что устроиться на полную ставку. У меня помимо этого есть работа, преподавание и C++.


Сергей П.: Вопрос, наверное, заключался немного в другом. Пользуетесь ли вы, например, AddressSanitizer, статическим анализатором и остальными инструментами? Как разработчик ПО, пользуетесь ли вы отладчиком? Проводите ли вы отладку своего ПО? Используете ли вы GDB или аналог, или Godbolt?


Бьярне: Да, я часто использую Godbolt. Еще я использую статический анализатор от Microsoft. Он работает по Core Guidelines, я не использую clang-tidy, поскольку не рассчитан на работу в области моих интересов. Отладчиками я пользуюсь нечасто. Есть cppreference, этого достаточно.


Последние проекты


Сергей П.: Если не секрет, не могли бы вы рассказать, над чем вы работаете в последние 23 года?


Бьярне: Моя работа видна по моим предложениям, вносимым в комитет по стандартизации я сильно занят подготовкой C++20. Вся работа излагается довольно подробно. Модули, сопрограммы, концепции. Также я работал с заказчиками, но там не сложилось. Кроме того, я писал статьи об истории развития языка в последние 15 лет, о направлении его развития. Также я постоянно работаю над ключевыми рекомендациями. Участвую во встречах с представителями Microsoft, Facebook, Red Hat.


Я не могу много рассказать о работе в банке, но в целом, это обычная работа с распределенной системой, похожая на то, чем я занимался последние 40 лет. В банковской сфере существуют серьезные ограничения по надежности и производительности, отчего работать интереснее. В бытность учёным я считал микросекунды. Сейчас приходится считать наносекунды, но суть работы не меняется код стареет, его необходимо обновлять. Все как везде.


Уже совсем скоро, 11-14 ноября, в онлайне пройдёт новая C++ Russia. В её программе тоже есть два интервью: с Мэттом Годболтом (вы же наверняка пользуетесь golbolt.org?) и с Тайтусом Уинтерсом (о том, стоит сломать обратную совместимость C++ во имя производительности). А кроме интервью, будут доклады на самые разные темы: современный CMake, SIMD-инструкции, лямбды В общем, если это интервью было вам интересным на новой конференции наверняка тоже найдёте много интересного.
Подробнее..

Попытка использовать современный C и паттерны проектирования для программирования микроконтроллеров

31.01.2021 18:12:32 | Автор: admin
Всем привет!

Проблема использования С++ в микроконтроллерах терзала меня довольно долгое время. Дело было в том, что я искренне не понимал, как этот объектно ориентированный язык может быть применим к встраиваем системам. Я имею ввиду, как выделять классы и на базе чего составлять объекты, то есть как именно применять этот язык правильно. Спустя некоторое время и прочтения n-ого количества литературы, я пришёл к кое каким результатам, о чем и хочу поведать в этой статье. Имеют ли какую либо ценность эти результаты или нет остается на суд читателя. Мне будет очень интересно почитать критику к моему подходу, чтобы наконец ответить себе на вопрос: Как же правильно использовать C++ при программировании микроконтроллеров?.

Предупреждаю, в статье будет много исходного кода.

В этой статье, я, на примере использования USART в МК stm32 для связи с esp8266 постараюсь изложить свой подход и его основные преимущества. Начнем с того, что главное преимущество использование C++ для меня это возможность сделать аппаратную развязку, т.е. сделать использование модулей верхнего уровня независимым от аппаратной платформы. Это будет вытекать в то, что система станет легко модифицирована при каких либо изменениях. Для этого я выделил три уровня абстракции системы:

  1. HW_USART аппаратный уровень, зависит от платформы
  2. MW_USART средний уровень, служит для развязки первого и третьего уровней
  3. APP_ESP8266 уровень приложения, ничего не знает о МК

HW_USART


Самый примитивный уровень. Я использовал камень stm32f411, USART 2, также выполнил поддержку DMA. Интерфейс реализован в виде всего трех функций: инициализировать, отправить, получить.

Функция инициализации выглядит следующим образом:

bool usart2_init(uint32_t baud_rate){  bool res = false;    /*-------------GPIOA Enable, PA2-TX/PA3-RX ------------*/  BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN) = true;    /*----------GPIOA set-------------*/  GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1);  GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR2 | GPIO_OSPEEDER_OSPEEDR3);  constexpr uint32_t USART_AF_TX = (7 << 8);  constexpr uint32_t USART_AF_RX = (7 << 12);  GPIOA->AFR[0] |= (USART_AF_TX | USART_AF_RX);            /*!---------------USART2 Enable------------>!*/  BIT_BAND_PER(RCC->APB1ENR, RCC_APB1ENR_USART2EN) = true;    /*-------------USART CONFIG------------*/  USART2->CR3 |= (USART_CR3_DMAT | USART_CR3_DMAR);  USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);  USART2->BRR = (24000000UL + (baud_rate >> 1))/baud_rate;      //Current clocking for APB1    /*-------------DMA for USART Enable------------*/     BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN) = true;    /*-----------------Transmit DMA--------------------*/  DMA1_Stream6->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream6->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.tx));  DMA1_Stream6->CR = (DMA_SxCR_CHSEL_2| DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC | DMA_SxCR_DIR_0);       /*-----------------Receive DMA--------------------*/  DMA1_Stream5->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream5->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.rx));  DMA1_Stream5->CR = (DMA_SxCR_CHSEL_2 | DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC);    DMA1_Stream5->NDTR = MAX_UINT16_T;  BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;  return res;}

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

Тогда функция отправки выглядит следующим образом:

bool usart2_write(const uint8_t* buf, uint16_t len){   bool res = false;   static bool first_attempt = true;      /*!<-----Copy data to DMA USART TX buffer----->!*/   memcpy(usart2_buf.tx, buf, len);      if(!first_attempt)   {     /*!<-----Checking copmletion of previous transfer------->!*/     while(!(DMA1->HISR & DMA_HISR_TCIF6)) continue;     BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF6) = true;   }      first_attempt = false;      /*!<------Sending data to DMA------->!*/   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = false;   DMA1_Stream6->NDTR = len;   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = true;      return res;}

В функции есть костыль, в виде переменной first_attempt, которая помогает определить самая ли первая это отправка по DMA или нет. Зачем это нужно? Дело в том, что проверку о том, успешна ли предыдущая отправка в DMA или нет я сделал ДО отправки, а не ПОСЛЕ. Сделал я так, чтобы после отправки данных не тупо ждать её завершения, а выполнять полезный код в это время.

Тогда функция приема выглядит следующим образом:

uint16_t usart2_read(uint8_t* buf){   uint16_t len = 0;   constexpr uint16_t BYTES_MAX = MAX_UINT16_T; //MAX Bytes in DMA buffer      /*!<---------Waiting until line become IDLE----------->!*/   if(!(USART2->SR & USART_SR_IDLE)) return len;   /*!<--------Clean the IDLE status bit------->!*/   USART2->DR;      /*!<------Refresh the receive DMA buffer------->!*/   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = false;   len = BYTES_MAX - (DMA1_Stream5->NDTR);   memcpy(buf, usart2_buf.rx, len);   DMA1_Stream5->NDTR = BYTES_MAX;   BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF5) = true;   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;      return len;}

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

На этом предлагаю закончить с низким уровнем и перейти непосредственно к C++ и паттернам.

MW_USART


Здесь я реализовал базовый абстрактный класс USART и применил паттерн прототип для создания наследников (конкретных классов USART1 и USART2). Я не буду описывать реализацию паттерна прототип, так как его можно найти по первой ссылке в гугле, а сразу приведу исходный код, и пояснения приведу ниже.

#pragma once#include <stdint.h>#include <vector>#include <map>/*!<========Enumeration of USART=======>!*/enum class USART_NUMBER : uint8_t{  _1,  _2};class USART; //declaration of basic USART classusing usart_registry = std::map<USART_NUMBER, USART*>; /*!<=========Registry of prototypes=========>!*/extern usart_registry _instance; //Global variable - IAR Crutch#pragma inline=forced static usart_registry& get_registry(void) { return _instance; }/*!<=======Should be rewritten as========>!*//*static usart_registry& get_registry(void) {   usart_registry _instance;  return _instance; }*//*!<=========Basic USART classes==========>!*/class USART{private:protected:     static void add_prototype(USART_NUMBER num, USART* prot)  {    usart_registry& r = get_registry();    r[num] = prot;  }    static void remove_prototype(USART_NUMBER num)  {    usart_registry& r = get_registry();    r.erase(r.find(num));  }public:  static USART* create_USART(USART_NUMBER num)  {    usart_registry& r = get_registry();    if(r.find(num) != r.end())    {      return r[num]->clone();    }    return nullptr;  }  virtual USART* clone(void) const = 0;  virtual ~USART(){}    virtual bool init(uint32_t baudrate) const = 0;  virtual bool send(const uint8_t* buf, uint16_t len) const = 0;  virtual uint16_t receive(uint8_t* buf) const = 0;};/*!<=======Specific class USART 1==========>!*/class USART_1 : public USART{private:  static USART_1 _prototype;    USART_1()   {      add_prototype( USART_NUMBER::_1, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_1; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};/*!<=======Specific class USART 2==========>!*/class USART_2 : public USART{private:  static USART_2 _prototype;    USART_2()   {      add_prototype( USART_NUMBER::_2, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_2; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};

Сначала файла идёт перечисление enum class USART_NUMBER со всеми доступными USART, для моего камня их всего два. Затем идёт опережающее объявление базового класса class USART. Далее идёт объявление контейнер а всех прототипов std::map<USART_NUMBER, USART*> и его реестра, который реализован в виде синглтона Мэйерса.

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

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

Также объявлен чисто виртуальный метод clone, и чисто виртуальные методы инициализации, отправки и получения.

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

Код определения методов привожу ниже:

#include "MW_USART.h"#include "HW_USART.h"usart_registry _instance; //Crutch for IAR/*!<========Initialization of global static USART value==========>!*/USART_1 USART_1::_prototype = USART_1();USART_2 USART_2::_prototype = USART_2();/*!<======================UART1 functions========================>!*/bool USART_1::init(uint32_t baudrate) const{ bool res = false; //res = usart_init(USART1, baudrate);  //Platform depending function return res;}bool USART_1::send(const uint8_t* buf, uint16_t len) const{  bool res = false;    return res;}uint16_t USART_1::receive(uint8_t* buf) const{  uint16_t len = 0;    return len;} /*!<======================UART2 functions========================>!*/bool USART_2::init(uint32_t baudrate) const{ bool res = false; res = usart2_init(baudrate);   //Platform depending function return res;}bool USART_2::send(const uint8_t* buf, const uint16_t len) const{  bool res = false;  res = usart2_write(buf, len); //Platform depending function  return res;}uint16_t USART_2::receive(uint8_t* buf) const{  uint16_t len = 0;  len = usart2_read(buf);       //Platform depending function  return len;}

Здесь реализованы методы НЕ пустышки только для USART2, так как его я и использую для общения с esp8266. Соответственно, наполнение может быть любое, также оно может быть реализовано с помощью указателей на функции, которые принимают свое значение исходя из текущего чипа.

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

APP_ESP8266


Определяю базовый класс для ESP8266 по паттерну одиночка. В нем определяю указатель на базовый класс USART*.

class ESP8266{private:  ESP8266(){}  ESP8266(const ESP8266& root) = delete;  ESP8266& operator=(const ESP8266&) = delete;    /*!<---------USART settings for ESP8266------->!*/  static constexpr auto USART_BAUDRATE = ESP8266_USART_BAUDRATE;  static constexpr USART_NUMBER ESP8266_USART_NUMBER = USART_NUMBER::_2;  USART* usart;    static constexpr uint8_t LAST_COMMAND_SIZE = 32;  char last_command[LAST_COMMAND_SIZE] = {0};  bool send(uint8_t const *buf, const uint16_t len = 0);    static constexpr uint8_t ANSWER_BUF_SIZE = 32;  uint8_t answer_buf[ANSWER_BUF_SIZE] = {0};    bool receive(uint8_t* buf);  bool waiting_answer(bool (ESP8266::*scan_line)(uint8_t *));    bool scan_ok(uint8_t * buf);  bool if_str_start_with(const char* str, uint8_t *buf);public:    bool init(void);    static ESP8266& Instance()  {    static ESP8266 esp8266;    return esp8266;  }};

Здесь же есть constexpr переменная, в которой и хранится номер используемого USART. Теперь для изменения номера USART нам достаточно только лишь поменять её значение! Связывание же происходит в функции инициализации:

bool ESP8266::init(void){  bool res = false;    usart = USART::create_USART(ESP8266_USART_NUMBER);  usart->init(USART_BAUDRATE);    const uint8_t* init_commands[] =   {    "AT",    "ATE0",    "AT+CWMODE=2",    "AT+CIPMUX=0",    "AT+CWSAP=\"Tortoise_assistant\",\"00000000\",5,0",    "AT+CIPMUX=1",    "AT+CIPSERVER=1,8888"  };    for(const auto &command: init_commands)  {    this->send(command);    while(this->waiting_answer(&ESP8266::scan_ok)) continue;  }      return res;}

Строка usart = USART::create_USART(ESP8266_USART_NUMBER); связывает наш уровень приложения с конкретным USART модулем.

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

Достучаться до небес, или FSM на шаблонах

05.02.2021 02:04:48 | Автор: admin

Здравствуйте! Меня зовут Александр, я работаю инженером-программистом микроконтроллеров.

Пишу на С/С++, причем предпочитаю плюсы, ибо верую в их эволюционную неизбежность в embedded.

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

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

Некоторое время назад я посмотрел мощный доклад Сергея Федорова про построение конечного автомата с таблицей переходов на шаблонах.

Если внезапно: "а что такое конечный автомат?"

Конечный автомат, или FSM(finite state maсhine) - один из самых востребованных и популярных приемов в программировании на МК. В свое время за кратким и практическим руководством по готовке FSM я ходил заброшенные, земли.

Одна из идей доклада - определить состояния, эвенты и действия через пользовательские типы, а таблицу переходов реализовать через шаблонный параметр, меня очень

впечатлила
// Transition table definitionusing transitions =  transition_table<  /*  State       Event       Next       */  tr< initial,    start,      running    >,  tr< running,    stop,       terminated >>;};// State machine objectusing minimal = state_machine<transitions>;minimal fsm;//...and then callfsm.process_event(start{});fsm.process_event(stop{});

А если добавить к этому перенос части функциональности кода в компайл тайм, заявленную автором потокобезопасность, улучшенные по сравнению с Boost::MSM выразительность, читаемость кода и скорость сборки, header only модель библиотеки, то - надо брать, решил я.

Вот только попытка собрать и запустить даже простейший пример на STM-ке закончилась матерком компилятора: "cannot use 'typeid' with "-fno-rtti" и "exception handling disabled".

Да, все так. Более того, помимо отключенной поддержки RTTI и исключений, у меня также выставлены флаги -fno-cxa-atexit, -fno-threadsafe-static. А еще в линкере применены настройки --specs=nano.specs (используем урезанную версию стандартной библиотеки с++ newlib-nano), --specs=nosys.specs (применяем легковесные заглушки для системных вызовов).

Зачем же таскать на себе вериги?

Embedded разработчикам хорошо известны особенности и ограничения при разработке встроенного ПО, а именно:

  • лимитированная память с недопустимостью фрагментации;

  • детерменированность времени выполнения;

  • штатно исполняющаяся программа никогда не выходит из main

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

Как закружить в гармоничном танце С++ и bare metal отлично разъяснено у этого автора. Также порекомендую этот доклад.

Исходники проекта докладчика, включая зависимости, - это двадцать файлов со смертоноснейшей шаблонной магией. Перекроить их так, чтобы не юзать typeid и exceptions, простому смертному в моем лице - too much.

Делать нечего, поступимся принципами, включим поддержку RTTI, а вместо throw в исходниках автора проставим заглушки.

На этот раз все собралось. Вот только при использовании тулчейна gcc-arm-none-eabi-9-2020-q2-update и уровне оптимизации -O3, размер исполняемого файла превысил 200Кб.

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

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

Итак, с наскоку взять высоту не удалось, и я на некоторое время переключился на другие задачи. Но красота идеи меня не отпускала, и на днях я все-таки решился "достучаться до небес" - написать extra light embedded версию FSM из упомянутого доклада самостоятельно.

Уточню свои хотелки:

  • Оперировать состояниями, эвентами и действиями как пользовательскими типами.

  • Таблицу переходов реализовать в виде шаблонного параметра

  • Перетащить что возможно в компайл тайм

  • Асинхронно и атомарно постить эвенты

  • Переключать состояния за константное время

  • Выйти в итоге на приемлемый по меркам встроенного ПО размер кода

  • Повторить header only модель библиотеки

Забегая вперед, скажу, что в итоге что-то получилось и даже взлетело.

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

Первым делом опишем базовые сущности:

Состояние/State
struct StateBase{};template <base_t N, typename Action = void>struct State : StateBase{  static constexpr base_t idx = N;  using action_t = Action;  };

Здесь и далее base_t - платформозависимый тип, машинное слово. В моем случае это unsigned int.

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

Цель статического члена idx уточню далее по тексту.

Событие/Event
struct EventBase{};template <base_t N>struct Event : EventBase{  static constexpr base_t idx = N;};

Элементарная структура, все ясно.

Действие при наступлении события и смене состояний:

Action
struct action{  void operator()(void){    // do something};

Безусловно, сигнатура operator() может и должна варьироваться от задач приложения, пока же для упрощения остановимся на самом легковесном варианте.

Сторож состояния:

Guard
enum class Guard : base_t{  OFF,  CONDITION_1,  CONDITION_2,  //etc.};

Идея сторожа - допустить переход в новое состояние, только если в данный момент выполнения программы текущее значение сторожа соответствует заданному пользователем значению в типе перехода/transition-a. Если такого соответствия нет, то переход в новое состояние не происходит. Но тут возможны варианты. Например, все же переходить, но не выполнять действие, переданное в состояние. Up to you.

Итак, пока все тривиально. Идем дальше.

Переход:

Transition
struct TrBase{};template <typename Source,          typename Event,          typename Target,          typename Action,          Guard G,          class =          std::enable_if_t<std::is_base_of_v<StateBase, Source>&&          std::is_base_of_v<EventBase, Event> &&          std::is_base_of_v<StateBase, Target>>          >  struct Tr : TrBase{  using source_t = Source;  using event_t  = Event;  using target_t = Target;  using action_t = Action;    static constexpr Guard guard = G;};

Структура Tr тоже элементарна. Она параметризуется типом исходного состояния - Source, типом события Event, по наступлению которого произойдет переход в целевое состояние Target, и типом Guard.

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

Таблица переходов:

Transition table
struct TransitionTableBase{};template<typename... T>struct TransitionTable : TransitionTableBase{    using test_t = typename NoDuplicates<Collection<T...>>::Result;    static_assert(std::is_same_v<test_t, Collection<T...>>,                "Repeated transitions");    using transition_p = type_pack<T...>;    using state_collection = typename NoDuplicates   <Collection<typename T::source_t... ,typename T::target_t...>   >::Result;    using event_collection = typename NoDuplicates  <Collection<typename T::event_t...>    >::Result;    using state_v = decltype(get_var(state_collection{}));  using event_v = decltype(get_var(event_collection{}));  using transition_v = std::variant<T...>;};

Нуу, тут я набросил на вентилятор, конечно. Хотя все не настолько пугающе, как выглядит.

Структура TransitionTable параметризуется списком переходов/transition-ов, которые собственно и описывают всю логику конечного автомата.

Первым делом нам необходимо подстраховать себя от копипаста и просигналить при компиляции, что у нас повторы в списке. Исполняем это с помощью алгоритма NoDuplicates из всем известной библиотеки Loki. Результирующий тип под псевдонимом test_t сравниваем в static_assert-e с исходным списком переходов.

Далее, допуская что static_assert пройден, параметризуем некую структуру type_pack списком переходов и выведенному типу назначаем псевдоним transition_p. Структура type_pack, а также современные алгоритмы и методы по работе со списками типов собраны в файле typelist.h. Данный хедер написан под чутким руководством этого продвинутого парня.

Тип transition_p понадобится нам далее в конструкторе класса StateMachine.

Следом проходим по списку переходов, вытаскиваем, очищаем от повторов и сохраняем в отдельные коллекции состояния и эвенты. Эти коллекции alias-им как state_collection и event_collection соответственно.

К чему эта эквилибристика?

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

Удобным вариантом для этой цели является std::variant (тафтология умышленна).

Последовательно параметризуем std::variant списком переходов (выведенному типу назначим псевдоним transition_v); списком состояний и списком эвентов и назначаем для удобства псевдонимы state_v и event_v соответственно.

Тут нюанс. Чтобы вывести transition_v нам достаточно пробросить в шаблонный параметр std::variant variadic pack (T...) из шаблонного параметра класса TransitionTable.

А вот чтобы вывести state_v и event_v мы используем

вспомогательную constexpr функцию
template<typename... Types>constexpr auto get_var (th::Collection<Types...>){return std::variant<Types...>{};}

Далее мы инстанцируем получившиеся типы в конструкторе класса StateMachine и сохраним их для дальнейшего использования в подходящих контейнерах, о чем совсем скоро.

Оставшихся к этому моменту читателей я не обрадую - начинается основной замес.

Целиком приводить класс StateMachine не буду, он громоздок, прокомментирую его для удобства восприятия по частям.

Контейнер transitions
template<typename Table>class StateMachine{//other stuffprivate:using map_type =std::unordered_map < Key, transition_v, KeyHash, KeyEqual>;Key key;map_type transitions;};

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

Объект типа Key хранит у себя значения индексов состояния и эвента:

Key
struct Key{  base_t state_idx = 0;  base_t event_idx = 0;};

Теперь стало понятно назначение статических членов idx в базовых сущностях. Я просто не знаю, как писать хэшеры для пустых структур. Тащить в строку название самого типа через typeid и _cxa_demangle для нас не вариант, мы же условились, что не пользуем RTTI.

Контейнер events
template<typename Table>class StateMachine{//other stuffprivate:using queue_type =  RingBufferPO2 <EVENT_STACK_SIZE, event_v, Atomic>;    queue_type events;};

events - очередь, в которую будут прилетать эвенты. Так как это чисто рантаймовая история, необходимо избежать динамических аллокаций. Поэтому реализуем ее на базе статического кольцевого буффера RingBufferPO2, который я позаимствовал здесь (отличная для своего времени работа!).

Помимо указанных контейнеров, в объекте типа StateMachine мы будем хранить текущее состояние/state и значение сторожа/guard:

state and guard
template<typename Table>class StateMachine{//other stuffprivate:state_v current_state;Guard guard = Guard::OFF;};

Саспенс уже не за горами.

Конструктор
template<typename Table>class StateMachine{public:using transition_pack = typename Table::transition_p;StateMachine(){  set(transition_pack{});} // other stuff};

В конструкторе метод set принимает аргументом объект с информацией о списке переходов, пробегается по нему, достает инфо о каждом состоянии и эвенте, заполняет контейнер transitions, а также запоминает начальные состояние и значение сторожа:

Метод set
template <class... Ts>void set (type_pack<Ts...>){(set_impl(just_type<Ts>{}), ...);};template <typename T>void set_impl (just_type<T> t){using transition = typename decltype(t)::type;using state_t = typename transition::source_t;using event_t = typename transition::event_t;Guard g = transition::guard;Key k;k.state_idx = state_t::idx;k.event_idx = event_t::idx;transitions.insert( {k, transition{}} );if (0 == key.state_idx) {key.state_idx = k.state_idx;guard = g;current_state = state_t{};}}

Итак, объект StateMachine сконструирован, пора его как-то шевелить.

Но перед этим забудем как страшный сон суммируем что уже рассмотрели к этому моменту:

  • Определили типы компонентов конечного автомата: состояние/state, событие/event, действие/action, сторож/guard

  • Определили тип переход/transition, который должен параметризоваться типами source state, event, target state, guard.

  • Определили тип таблицы переходов. В качестве шаблонных параметров ему передается список переходов/transition-ов, который и определяет алгоритмы работы автомата.

  • При компиляции в классе TransitionTable, на основе std::variant выводятся типы-коллекции переходов, состояний и эвентов, которые впоследствии при конструировании объекта StateMachine инстанцируются и сохраняются в контейнеры, с которыми уже можно работать в рантайме.

Стержневая идея моей имплементации автомата такова (вдохнули): при наступлении события, мы достаем из его типа индекс (idx), объединяем его с индексом текущего состояния в объекте Key, по которому в контейнере transitions находим нужный нам переход, где получаем знания о целевом состоянии, стороже и действии, которое требуется выполнить в этом переходе, а также сверяем значения сторожа с текущим, для подтверждения или отмены перехода/действия(выдохнули).

Теперь рассмотрим методы API нашего автомата, реализующие эту логику.

Переключать состояния мы можем двумя способами: вызывать немедленный переход методом fsm.on_event(event{}) (шаблонная версиия fsm.on_event<Event>() если тип события известен на этапе проектирования), или можем складывать события в очередь методом fsm.push_event(event{}), чтобы потом, например в основном цикле, разобрать ее методом fsm.process(). Также, если в состояние передано какое-то действие, то мы можем вызывать его методом fsm.state_action().

Рассмотрим их детальнее, начиная с последнего

Метод state action
template <typename... Args>void state_action (const Args&... args){state_v temp_v{current_state};    auto l = [&](const auto& arg){      using state_t =  std::decay_t<decltype(arg)>;    using functor_t = typename state_t::action_t;        if constexpr (!std::is_same_v<functor_t, void>){    functor_t{}(args...);      }  };    std::visit(l, temp_v);}  

В методе мы создаем локальную переменную типа std::variant<State...> temp_v и инициализируем ее текущим состоянием. Далее определяем лямбду, которая послужит аргументом в методе std::visit.

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

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

Метод on_event
template <typename Event,class = std::enable_if_t<std::is_base_of_v<EventBase, Event>>>void on_event(const Event& e){Key k;  k.event_idx = e.idx;  k.state_idx = key.state_idx;  on_event_impl(k);}void on_event_impl (Key& k){transition_v tr_var = transitions[k];    Key &ref_k = key;  Guard &ref_g = guard;  state_v &ref_state = current_state;    auto l = [&](const auto& arg){    using tr_t =  std::decay_t<decltype(arg)>;    using functor_t = typename tr_t::action_t;        if ( GuardEqual{}(ref_g, tr_t::guard) ){          using target_t = typename tr_t::target_t;            ref_k.state_idx = target_t::idx;      ref_state = target_t{};            functor_t{}();      }   };      std::visit(l, tr_var);}

Здесь, как я уже описывал, мы достаем индекс из эвента, объединяем его в Key с индексом текущего состояния, и в качестве ключа передаем в приватный метод on_event_impl(Key& k).

Там мы по принятому ключу достаем из контенера transitions объект типа std::variant<Tr...> и инициализируем им локальную переменную tr_var. Ну а далее - логика, схожая с предыдущим примером. Вызываем std::visit c tr_var и лямдой l, в которой из типа Tr получаем сведения о состоянии, в которое нужно перейти (target_t), стороже (tr_t::guard)и типе действия (functor_t) к исполнению.

Сверив значение сторожа перехода с текущим сторожем, мы или оcуществляем переход, инстанцируя и вызывая functor_t, и сохраняя target_t в переменную с текущим состоянием(current_state), или возвращаемся в исходное состояние. Где ждем смены значения сторожа и нового события.

Метод push_event
template <unsigned int N>void push_event (const Event<N>& e){  events.push_back(e);}

Тут все просто.

Метод set_guard
void set_guard (const Guard& g){  guard = g;}

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

Метод process
void process (void){    state_action();    auto it = transitions.begin();    Key k;  k.state_idx = key.state_idx;    for (uint32_t i = 0; i != events.size(); ++i){        auto v = events.front();     auto l = [&](const auto& arg){      using event_t =  std::decay_t<decltype(arg)>;      k.event_idx = event_t::idx;      it = transitions.find(k);    }        std::visit(l, v);        if ( it != transitions.end() ){            events.pop_front();      on_event_impl(k);      return;        } else {      events.push_back(v);      events.pop_front();    }  }}

При вызове метода мы первым делом выполняем некое полезное действие (если не void), переданное в состояние, state_action().

Ну а далее пробегаемся по очереди эвентов и просто воспроизводим логику, уже описанную для метода fsm.on_event(event{}).

Разумеется, работу с событиями можно значительно ускорить, при этом расширив функционал автомата. Тип Event модернизируем

так
template <base_t N, base_t Priority>struct Event : EventBase{  static constexpr base_t idx = N;  static constexpr base_t pri = Priority;};

Теперь мы можем не пушить все события в одну очередь, а завести, скажем, std::array<queue_t, PRIRITY_NUM>, где индексом ячейки будет служить приоритет события. Тогда у нас получится приняв эвент, вытащить его приоритет, по нему, как по индексу за константное время попасть в нужную очередь событий, которая будет гораздо меньше, чем общая и быстрее в обработке.

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

На самом деле в следующей версии своего FSM у меня так и реализовано, но здесь я привожу упрощенный вариант.

Хорошо, каков же будет практический результат этой разнузданной шаблонной вакханалии?

Детектор нейтрино(нет)
struct green_a {/*toogle green led every 50ms*/}struct yellow_a {/*toogle yellow led every 50ms*/}struct red_a {/*toogle red led every 50ms*/}struct green_f {/*toogle green led every 150ms*/}struct yellow_f {/*toogle yellow led every 150ms*/}struct red_f {/*toogle red led every 150ms*/}using STATE_A(green_s, green_f);using STATE_A(yellow_s, yellow_f);using STATE_A(red_s, red_f);using EVENT(green_e);using EVENT(yellow_e);using EVENT(red_e);using fsm_table = TransitionTable    <    Tr<green_s, yellow_e, yellow_s, yellow_a, Guard::NO_GUARD>,    Tr<yellow_s, red_e, red_s, red_a, Guard::NO_GUARD>,    Tr<red_s, green_e, green_s, green_a, Guard::NO_GUARD>    >;int main(void){  //some other stuff  StateMachine<fsm_table> fsm;  fsm.push_event(red_e{});  fsm.push_event(yellow_e{});  fsm.push_event(green_e{});  while (1){    fsm.process();  }}

В этом примере структуры типа color_a(ction) - это действия при переходе; color_f(unctor) - функторы, которые будут выполняться каждый раз при заходе в стейт, ну и далее понятно.

Объявляем стейты, эвенты, переходы, таблицу переходов. Конструируем из класса StateMachine<fsm_table> наш конечный автомат fsm. Пушим события, заходим в while и наблюдаем аквасветодискотеку на нашей отладке.

Обращу еще ваше внимание на макросы, через которые организована декларация состояний и событий. Задача была исхитриться и не делать так:

using even_t = Event<1, 15>;

using state_t = State<1, state_functor>;

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

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

Как-то так
#define STATE_A(str, act) str = State<name(#str), act>#define EVENT(str) str = Event<name(#str)>constexpr base_t name (const char* n){    base_t  res = 0;    for (base_t i = 0; n[i] != '\0'; i++){        char data = n[i];        for (base_t j = sizeof (char) * 8; j > 0; j--){            res = ((res ^ data) & 1) ? (res >> 1) ^ 0x8C : (res >> 1);      data >>= 1;    }  }  return res;};

После крайнего проекта на работе у меня на руках осталась отладка NUCLEO-H743ZI2, на ней я и запилил тестовый вариант (забирайте здесь).

С оптимизацией -O3 реализация приведенного примера (только сам FSM) заняла 6,8Кб, с HAL-ом и моргалками - 14,4Кб.

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

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

Спасибо за внимание!

Подробнее..

Recovery mode Сравнение скорости работы сортировок на С

08.02.2021 22:18:49 | Автор: admin
Начнем с того, что данному вопросу уделяется мало времени и приходится гуглить данный вопрос.
Код программы используемый в данной статье, я переписывал пару раз. Всегда было интересно насколько одна сортировка будет быстрее другой. Их как бы все студенты проходят, но в основном как переписывание псевдоалгоритма на лекции в код на каком-нибудь языке. Может быть данная статья будет полезна для какого-нибудь начинающего программиста.
Рассмотрим 5 сортировок. Это пузырьковая(bubble), шейкерная(shake), пирамидальная(heap), вставками(insertion) и быстрая(quick).

Для анализа их скорости будет использоваться функция clock() до сортировки и она же после, потом берется их разность и мы узнаем время работы сортировки. Я использовал 100 итераций по 1000 значений заданных в векторах и одном листе для тестирования встроенной функции sort() из stl. Каждой сортировке даются одинаково разбросанные по массивам числа на каждой итерации. После чего время записывается в переменную mean каждой сортировки и делится по итогу на количество итераций. Так мы узнаем среднее время работы каждой сортировки и сможем в итоге их сравнить по скорости при одинаковых исходных данных. Данные вносятся в массивы функцией rand().
Файл Sorts.h:
#pragma once#include <iostream>#include <list>#include <vector>#include <iterator>template <typename T> class Sorts{public:std::list<T> arrayList;std::vector<T> bubbleArray,insertionArray,heapArray,shakeArray;float BubbleSort(){std::cout <<"Time to Bubble>" << std::endl;unsigned int start_time = clock(); // начальное времяint size = bubbleArray.size();for (int i = 1; i < size; i++)for (int j = size-1; j >=i; j--)if (bubbleArray[j-1] > bubbleArray[j])swap(&bubbleArray, j - 1, j);unsigned int end_time = clock(); // конечное времяunsigned int search_time = end_time - start_time; // искомое времяstd::cout << (float)search_time / CLOCKS_PER_SEC << std::endl;return (float)search_time / CLOCKS_PER_SEC;}float InsertionSort(){std::cout << "Time to Insertion>" << std::endl;unsigned int start_time = clock(); // начальное времяint size = insertionArray.size();for (int i = 1; i < size; i++){T tmp = insertionArray[i];int j = i;while (j > 0 && insertionArray[j - 1] > tmp){insertionArray[j] = insertionArray[j - 1];j = j - 1;}insertionArray[j] = tmp;}unsigned int end_time = clock(); // конечное времяunsigned int search_time = end_time - start_time; // искомое времяstd::cout << (float)search_time / CLOCKS_PER_SEC << std::endl;return (float)search_time / CLOCKS_PER_SEC;}void swap(std::vector<T> *v, int n, int m){T tmp = (*v)[n];(*v)[n] = (*v)[m];(*v)[m] = tmp;}float HeapSort(){std::cout << "Time to Heap>" << std::endl;unsigned int start_time = clock(); // начальное времяint size = heapArray.size();for (int j = 0; j < size; j++){for (int i = size / 2 - 1 - j / 2; i > -1; i--){if (2 * i + 2 <= size - 1 - j){if (heapArray[2 * i + 1] > heapArray[2 * i + 2]){if (heapArray[i] < heapArray[2 * i + 1]){swap(&heapArray, i, 2 * i + 1);}}elseif (heapArray[i] < heapArray[2 * i + 2]){swap(&heapArray, i, 2 * i + 2);}}elseif (2 * i + 1 <= size - 1 - j)if (heapArray[i] < heapArray[2 * i + 1])swap(&heapArray, i, 2 * i + 1);}swap(&heapArray, 0, size - 1 - j);}unsigned int end_time = clock(); // конечное времяunsigned int search_time = end_time - start_time; // искомое времяstd::cout << (float)search_time / CLOCKS_PER_SEC << std::endl;return (float)search_time / CLOCKS_PER_SEC;}float ShakeSort(){std::cout << "Time to Shake>" << std::endl;unsigned int start_time = clock(); // начальное времяint size = shakeArray.size();int left = 0;int right = size - 1;do {for (int i = left; i < right; i++) {if (shakeArray[i] > shakeArray[i + 1])swap(&shakeArray,i,i+1);}right--;for (int i = right; i > left; i--) {if (shakeArray[i] < shakeArray[i - 1])swap(&shakeArray, i-1, i);}left++;} while (left < right);unsigned int end_time = clock(); // конечное времяunsigned int search_time = end_time - start_time; // искомое времяstd::cout << (float)search_time / CLOCKS_PER_SEC << std::endl;return (float)search_time / CLOCKS_PER_SEC;}void PrintArray(int num){switch (num){case 0:for (typename std::list<T>::iterator it = arrayList.begin(); it != arrayList.end(); it++)std::cout << (*it) << " ";break;case 1:for (typename std::vector<T>::iterator it = bubbleArray.begin(); it != bubbleArray.end(); it++)std::cout << (*it) << " ";break;case 2:for (typename std::vector<T>::iterator it = shakeArray.begin(); it != shakeArray.end(); it++)std::cout << (*it) << " ";break;case 3:for (typename std::vector<T>::iterator it = heapArray.begin(); it != heapArray.end(); it++)std::cout << (*it) << " ";break;case 4:for (typename std::vector<T>::iterator it = insertionArray.begin(); it != insertionArray.end(); it++)std::cout << (*it) << " ";break;default:break;}std::cout << std::endl;}};

Замечу что можно использовать не только целые числа, но и вещественные и символы.
Файл основной программы:
#include "Sorts.h"int main(){std::vector<float> vq, vb, vs, vh, vi;float meanq = 0, meanb = 0, means = 0, meanh = 0, meani = 0;const int N = 100;srand(time(0));for (int i = 0; i < N; i++){std::cout << i+1 << " iteration" << std::endl;const int iSize = 1000;auto sort = new Sorts<int>();for (int i = 0; i < iSize; i++){int num = rand() % iSize;sort->arrayList.push_back(num);sort->bubbleArray.push_back(num);sort->shakeArray.push_back(num);sort->heapArray.push_back(num);sort->insertionArray.push_back(num);}std::cout << "Time to Quick sort from stl>" << std::endl;unsigned int start_time = clock(); // начальное времяsort->arrayList.sort();unsigned int end_time = clock(); // конечное времяunsigned int search_time = end_time - start_time; // искомое времяstd::cout << (float)search_time / CLOCKS_PER_SEC << std::endl;vq.push_back((float)search_time / CLOCKS_PER_SEC);vb.push_back(sort->BubbleSort());vs.push_back(sort->ShakeSort());vh.push_back(sort->HeapSort());vi.push_back(sort->InsertionSort());meanq += vq[i];meanb += vb[i];means += vs[i];meanh += vh[i];meani += vi[i];//sort->PrintArray(0);//sort->PrintArray(1);//sort->PrintArray(2);//sort->PrintArray(3);//sort->PrintArray(4);sort->arrayList.clear();sort->bubbleArray.clear();sort->shakeArray.clear();sort->heapArray.clear();sort->insertionArray.clear();std::cout << "end of "<< i + 1 <<" iteration" << std::endl;}std::cout << "Results:" << std::endl;std::cout << "Mean quick=" << (float)meanq / N << std::endl;std::cout << "Mean bubble=" << (float)meanb / N << std::endl;std::cout << "Mean shake=" << (float)means / N  << std::endl;std::cout << "Mean heap=" << (float)meanh / N  << std::endl;std::cout << "Mean insertion=" << (float)meani / N << std::endl;return 0;}

Каковы же результаты?
С большим отрывом идет sort из stl, потом вставками, пирамидальная, шейкерная и заканчивает пузырьковая.
Quick 0.00225 ms
Insertion 0.04482 ms
Heap 0.07025 ms
Shake 0.14186 ms
Bubble 0.14324 ms
В принципе слишком большие массивы данных долго сортируются, но quicksort справляется на порядки быстрее остальных.
Подробнее..
Категории: C++ , С++ , Сортировки

Вебинар Стандарт С20 обзор новых возможностей C, введённых Стандартом C20

17.02.2021 14:23:34 | Автор: admin
25 февраля Яндекс.Практикум проводит открытый вебинар Стандарт С++20. Приглашаем разработчиков С++, которые хотят использовать последние возможности языка, а также программистов на других языках, которые хотят узнать, какие преимущества даёт разработка на C++.

На вебинаре максимально кратко и содержательно расскажем о новых фичах Стандарта: зачем они нужны, насколько они круты, когда и для чего их можно будет использовать в своих программах. Особое внимание уделим модулям, концептам, диапазонам (Ranges), корутинам и трёхстороннему сравнению. Также поговорим и об остальных нововведениях.

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



В программе


  1. Краткая история Стандартов. Что привело к C++20.
  2. Модули как новая эпоха языка:
    Долгожданный документ принят панацея или ошибка?
    Как С++ преодолевает 30-летнее отставание.
    Как сломать систему сборки.
  3. Оператор космического корабля:
    Один за шестерых, но это ещё не всё.
    Порядки в C++ выходят на новый уровень.
  4. Концепты и констрейнты. Средство от криптографических ошибок на 10000 строк или нечто большее?
  5. Ranges. Просто адекватная замена парам итераторов или новый стиль в программировании?
  6. Корутины:
    Что это, и почему их время ещё не пришло.
    Как сломать отладчик.
  7. Приятные и важные мелочи. Краткий обзор других фич Стандарта:
    Designated initializers, инициализаторы в Ranged-for, span, календарь, format, шаблонные лямбды и другое.
    Что ещё дал нам C++20.
  8. Будущее. Чего ожидать от C++ дальше.

Ведущий


Вебинар проведёт Георгий Осипов автор факультета Разработчик C++ в Яндекс.Практикуме, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ.

Вебинар пройдёт 25 февраля в 19.30 (Мск).
Бесплатная регистрация.
Подробнее..

Макросы в С и С

14.03.2021 16:17:43 | Автор: admin

Макросы - один из моих самых любимых инструментов в языках С и С++. Умные люди и умные книжки советуют по максимуму избегать использования макросов, по возможности заменяя их шаблонами, константами и inline-функциями, и на то есть веские основания. С помощью макросов можно создавать не только изящный код, но и плодить не менее изящные баги, которые потом будет очень сложно отловить и пофиксить. Но если соблюдать ряд несложных правил при работе с макросами, они становятся мощным оружием, которое не стреляет по твоим собственным коленям. Но сперва давай разберемся, что вообще такое макросы в С и С++?

Что есть макросы?

В языках С и С++ есть такой механизм, как препроцессор. Он обрабатывает исходный код программы ДО того, как она будет скомпилированна. У перпроцессора есть свои директивы, такие как #include, #pragma, #if и тд. Но нам интересна только директива #define.

В языке Си довольно распространенной практикой является объявление глобальных констант с помощью директивы #define:

#define PI 3.14159

А потом, на этапе препроцессинга все использования PI будут заменены указанным значением:

double area = 2 * PI * r * r;

После препроцессинга, который по сути является банальной подстановкой, это выражение превратится в:

double area = 2 * 3.14159 * r * r;

PI - макрос, в самом простом его исполнении. Естественно, макросы в таком виде не работают как переменные. Им нельзя присваивать новое значение или использовать их адрес.

// Так нельзя:PI = 3; // после препроцессинга: 3.14159 = 3int *x = &PI;    // после препроцессинга: int *x = &3.14159

О макросах важно понимать, что область видимости у них такая же, как у нестатических функций в языке Си, то есть они видны везде, куда их "заинклюдили". Однако в отличии от функций, объявление макроса можно отменить:

#undef PI

После этой строчки обращаться к PI будет уже нельзя.

Макросы с параметрами

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

#define MAX(a, b) a >= b ? a : b

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

#define SWAP(type, a, b) type tmp = a; a = b; b = tmp;

Поскольку мы первым параметром передаем тип, данный макрос будет работать с переменными любого типа:

SWAP(int, num1, num2)SWAP(float, num1, num2)

Макросы так же можно записывать в несколько строк, но тогда каждая строка, кроме последней, должна заканчиваться символом '\':

#define SWAP(type, a, b) type tmp = a; \ a = b; \b = tmp;

Параметр макроса можно превратить в строку, добавив перед ним знак '#':

#define PRINT_VAL(val) printf("Value of %s is %d" #val, val);int = 5;PRINT_VAL(x)  // -> Value of x is 5

А еще параметр можно приклеить к чему-то еще, чтобы получился новый идентификатор. Для этого между параметром и тем, с чем пы его склеиваем, нужно поставить '##':

#define PRINT_VAL (number) printf("%d", value_##number);int value_one = 10, value_two = 20;PRINT_VAL(one)  // -> 10PRINT_VAL(two)  // -> 20

Техника безопасности при работе с макросами

Есть несколько основных правил, которые нужно соблюдать при работе с макросами.

1. Параметрами макросов не должны быть выражения и вызовы функций.

Ранее я уже объявлял макрос MAX. Но что получится, если попытаться вызвать его вот так:

int x = 1, y = 5;int max = MAX(++x, --y);

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

int max = ++x >= --y ? ++x : --y;

В итоге переменная max будет равна не 4, как мы ожидали, а 3. Потом можно уйму времени потратить, отлавливая эту ошибку. Так что в качестве аргумента макроса нужно всегда передавать уже конечное значение, а не какое-то выражение или вызов функции. Иначе выражение или функция будут вычислены столько раз, сколько используется этот параметр в теле макроса.

2. Все аргументы макроса и сам макрос должны быть заключены в скобки.

Это правило я уже нарушил при написании макроса MAX. Что получится, если мы захотим использовать этот макрос в составе какого-то математического выражения?

int result = 5 + MAX(1, 4);

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

int result = 5 + 1 > 4 ? 1 : 4;

И переменная result внезапно примет значение 1. Чтобы такого не происходило, макрос MAX должен быть объявлен следующим образом:

#define MAX(a, b) ((a) >= (b) ? (a) : (b))

В таком случае все действия произойдут в нужном порядке.

3. Многострочные макросы должны иметь свою область видимости.

Например у нас есть макрос, который вызывает две функции:

#define MACRO() doSomething(); \ doSomethinElse();

А теперь попробуем использовать этот макрос в таком контексте:

if (some_condition) MACRO()

После макроподстановки мы увидим вот такую картину:

if (some_condition) doSomething();doSomethinElse();

Нетрудно заметить, что под действие if попадет только первая функция, а вторая будет вызываться всегда. Именно для того, чтобы избежать подобных багов, у макросов должна быть объявлена своя область видимости. Для удобства в этих целях принято использовать цикл do {} while (0); .

#define MACRO() do { \ doSomething(); \           doSomethinElse(); \         } while(0)

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

Еще немного примеров

В языке Си при помощи макросов можно эффективно избавляться от дублирования кода. Банальный пример - объявим несколько функций сложения для работы с разными типами данных:

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

DEF_SUM(int)DEF_SUM(float)DEF_SUM(double)int main() {     sum_int(1, 2);  sum_float(2.4, 6,3); sum_double(1.43434, 2,546656);}

Таким образом у нас получился аналог шаблонов из С++. Но стоит сразу обратить внимание, что данный способ не подойдет для типов, название которых состоит более чем из одного слова, например long long или unsigned short, потому что не получится нормально склеить название функции (sum_##type). Для этого сперва придется объявить для них новый тип, состоящий из одного слова.

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

Мой блог в Телеграме

Подробнее..
Категории: C++ , С++ , C , Си , Macro , Макросы , Define

Альтернативное собеседование на позицию разработчика ПО

07.04.2021 12:05:29 | Автор: admin

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

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

Предлагалось сделать следующее:

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

  2. Провести code-review, указать на подозрительные и плохие места и предложить, как можно их улучшить или переделать. Можно задавать любые вопросы и гуглить все что угодно.

Код на C++, хотя большинство ошибок смогут найти даже разработчики на других языках. Код был примерно такой:

class SomeServiceClient{ public:  SomeServiceClient();  virtual ~SomeServiceClient();  bool CallAsync(const std::string& uri,                 const std::string& param,                 const misc::BusServiceClient::ResponseCB& callback);  bool CallSync(const std::string& uri,                const std::string& param,                const misc::BusServiceClient::ResponseCB& callback); private:  misc::BusServiceClient ss_client_;  static const int kSleepMs = 100;  static const int kSleepCountMax = 50;};class SpecificUrlFetcher : public UrlFetcher { public:  SpecificUrlFetcher();  virtual ~SpecificUrlFetcher();  SomeData FetchData(const URL& url, const UrlFetcher::ResponseCB& callback); private:  bool SsResponse_returnValue{false};  char SsResponse_url[1024];  void SsResponseCallback(const std::string& response);  SomeServiceClient* ss_client_;};...static const char ss_getlocalfile_uri[] =    "bus://url_replace_service";namespace net {pthread_mutex_t g_url_change_callback_lock = PTHREAD_MUTEX_INITIALIZER;SomeBusServiceClient::SomeBusServiceClient()    : ss_client_(misc::BusServiceClient::PrivateBus) {}SomeBusServiceClient::~SomeBusServiceClient() {}bool SomeBusServiceClient::CallAsync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  bool bRet;  bRet = ss_client_.callASync(uri, param, callback);  return bRet;}bool SomeBusServiceClient::CallSync(    const std::string& uri,    const std::string& param,    const misc::BusServiceClient::ResponseCB& callback) {  boold bRet  bRet = false;  int counter;  pthread_mutex_lock(&g_url_change_callback_lock);   ss_client_.callASync(uri, param, callback);  counter = 0;  for (;;) {    int r = pthread_mutex_trylock(&g_url_change_callback_lock);    if (r == 0) {      bRet = true;      pthread_mutex_unlock(&g_url_change_callback_lock);    } else if (r == EBUSY) {      usleep(kSleepMs);      counter++;      if (counter >= kSleepCountMax) {        pthread_mutex_unlock(&g_url_change_callback_lock);        break;      } else        continue;    }    break;  }  return bRet;}/**************************************************************************/SpecificUrlFetcher::SpecificUrlFetcher() {}SpecificUrlFetcher::~SpecificUrlFetcher() {}void SpecificUrlFetcher::SsResponseCallback(const std::string& response) {  std::unique_ptr<lib::Value> value(lib::JSONReader::Read(response));  if (!value.get() || !value->is_dict()) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  lib::DictionaryValue* response_data =      static_cast<lib::DictionaryValue*>(value.get());  bool returnValue;  if (!response_data->GetBoolean("returnValue", &returnValue) || !returnValue) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  std::string url;  if (!response_data->GetString("url", &url)) {    pthread_mutex_unlock(&g_url_change_callback_lock);    return;  }  SsResponse_returnValue = true;  size_t array_sz = arraysize(SsResponse_url);  strncpy(SsResponse_url, url.c_str(), array_sz);  SsResponse_url[array_sz - 1] = 0;  pthread_mutex_unlock(&g_url_change_callback_lock);}SomeData SpecificUrlFetcher::FetchData(const URL& url, const UrlFetcher::ResponseCB& callback) {lib::DictionaryValue dictionary;std::string ss_request_payload;misc::BusServiceClient::ResponseCB response_cb =lib::Bind(&SpecificUrlFetcher::SsResponseCallback, this);SomeBusServiceClient* ss_client_ =new SomeBusServiceClient();dictionary.SetString("url", url.to_string());lib::JSONWriter::Write(dictionary, &ss_request_payload);SsResponse_returnValue = false;SsResponse_url[0] = 0x00;ss_client_->CallSync(ss_getlocalfile_uri, ss_request_payload, response_cb);URL new_url;if (SsResponse_returnValue) {  new_url = URL::from_string(SsResponse_url);}delete ss_client_;return UrlFetcher::FetchData(new_url, callback);}}  // namespace net

Ответы будут под спойлером, нажимайте на него осознанно, пути назад уже нет.

Итак, ответы.
  1. У нас есть какой-то класс UrlFetcher, задача которого, судя по всему -- получать какие-то данные по какому-то URL'у. Унаследованный у него класс делает то же самое, только перед запросом обращается по какой-то шине сообщений к какому-то внешнему сервису, отправляя ему запрошенный URL, и вместо него получает от этого сервиса некий другой URL, который и используется дальше. Этакий паттерн Decorator.

  2. Сначала по мелочам:

    1. ss_getlocalfile_uri - глобальная переменная. Зачем? Можно было объявить ее внутри одного из классов.

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

    3. Странный стиль именования переменных и полей, например SsResponse_returnValue Далее по-серьезнее:

    4. Используется pthread-функции, при том что есть стандартные std::thread, которых в данном случае более чем достаточно.

    5. Используются Си-строки с методами типа strncpy(); по факту тут можно использовать std::string без каких-либо проблем.

    6. ss_client_ хранится в сыром указателе и удаляется вручную. Лучше использовать std::unique_ptr.

    7. Вместо usleep() лучше все-таки использовать std::this_thread::sleep()

    Еще серьезнее:

    8. В цикле в SomeBusServiceClient::CallSync если колбэк с ответом придет менее чем за kSleepMs до kSleepCountMax, то мы откинем ответ и не выполним задачу. Это плохо.

    А теперь еще серьезнее:

    9. Мы отправляем асинхронный запрос в message bus и ждем. Отправленный запрос по истечении таймаута не отменяется. Неизвестно, как работает этот message bus, но если вдруг у класса работы с ним есть какой-то таймаут по умолчанию, то стоит использовать его как kSleepCountMax*kSleepMs, а если ничего такого нет, то нужно как-то отменять уже отправленный запрос когда он нам стал не нужен (возможно callASync возвращает какой-нибудь id запроса?). Потому что если вдруг по какой-то причине ответ придет сильно позже, когда мы уже не ждем, а начали получать следущий URL, то случится полный бардак.

    9. В функции FetchData нет проверки на ошибку, new_url в любом случае передается в метод базового класса, даже если он пустой.

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

    11. Судя по логике работы (вызов FetchUrl синхронный и блокирует тред), SsResponseCallback должен выполниться в другом треде. При этом получается, что мы разблокируем мьютекст не в том потоке, где мы его блокировали. Для pthread это явный undefined behavior.

Ответы и замечания от кандидата позволяли составить представление о его уровне владения современными стандартами C++ и хорошими практиками, понимании асинхронности и многопоточности, дотошности на ревью и умении "отлаживать" код в голове. Ну и задать темы для дальнейшего разговора по душам.

Подробнее..

Книга C для профи

19.04.2021 16:09:25 | Автор: admin
image Привет, Хаброжители! С++ популярный язык для создания ПО. В руках увлеченного программиста С++ становится прекрасным инструментом для создания лаконичного, эффективного и читаемого кода, которым можно гордиться.

C++ для профи адресован программистам среднего и продвинутого уровней, вы продеретесь сквозь тернии к самому ядру С++. Часть 1 охватывает основы языка С++ от типов и функций до жизненного цикла объектов и выражений. В части II представлена стандартная библиотека C ++ и библиотеки Boost. Вы узнаете о специальных вспомогательных классах, структурах данных и алгоритмах, а также о том, как управлять файловыми системами и создавать высокопроизводительные программы, которые обмениваются данными по сети.


Об этой книге

Современные программисты на C++ имеют доступ к ряду очень качественных книг, например Эффективный современный C++ Скотта Мейерса1 и Язык программирования C++ Бьёрна Страуструпа, 4-е издание2. Однако эти книги написаны для достаточно продвинутых программистов. Доступны также некоторые вводные тексты о C++, но они часто пропускают важные детали, потому что ориентированы на абсолютных новичков в программировании. Опытному программисту непонятно, где можно погрузиться в язык C++.

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

Кому будет интересна эта книга?

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

В ПОЗНАКОМИТЕСЬ С ОСНОВНМИ ФИШКАМИ СОВРЕМЕННОГО С++:

Базовые типы, ссылочные и пользовательские типы.
Полиморфизм во время компиляции и полиморфизм во время выполнения.
Жизненный цикл объекта, включая длительность хранения, стек вызовов, управление памятью, исключения и парадигму RAII.
Продвинутые выражения, операторы и функции.
Умные указатели, структуры данных, дата и время, числовые данные и др.
Контейнеры, итераторы, строки и алгоритмы.
Потоки и файлы, многозадачность, сетевое программирование и разработка приложений.

Отслеживание жизненного цикла объекта


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

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

Листинг 4.5. Класс Tracer и его конструктор с деструктором

#include <cstdio>struct Tracer {    Tracer(const char* name1) : name{ name }2 {       printf("%s constructed.\n", name); 3    }    ~Tracer() {       printf("%s destructed.\n", name); 4    }private:    const char* const name;};

Конструктор принимает один параметр 1 и сохраняет его в члене name 2. Затем он печатает сообщение, содержащее name 3. Деструктор 4 также выводит сообщение с name.

Рассмотрим программу в листинге 4.6. Четыре различных объекта Tracer имеют разную длительность хранения. Просматривая порядок вывода программы Tracer, вы можете проверить полученные знания о длительности хранения.

Листинг 4.6. Программа, использующая класс Tracer в листинге 4.5 для иллюстрации длительности хранения

#include <cstdio>struct Tracer {    --пропуск--};static Tracer t1{ "Static variable" }; 1thread_local Tracer t2{ "Thread-local variable" }; 2int main() {  const auto t2_ptr = &t2;  printf("A\n"); 3  Tracer t3{ "Automatic variable" }; 4  printf("B\n");  const auto* t4 = new Tracer{ "Dynamic variable" }; 5  printf("C\n");}

Листинг 4.6 содержит Tracer со статической 1, локальной поточной 2, автоматической 4 и динамической 5 длительностью хранения. Между каждой строкой в main выводится символ A, B или C для ссылки 3.

Запуск программы приводит к результату в листинге 4.7.

Листинг 4.7. Пример вывода из листинга 4.6

Static variable constructed.Thread-local variable constructed.A 3Automatic variable constructed.BDynamic variable constructed.CAutomatic variable destructed.Thread-local variable destructed.Static variable destructed.

Перед первой строкой main 3 статические и потоковые локальные переменные t1 и t2 были инициализированы 1 2. Это можно увидеть в листинге 4.7: обе переменные напечатали свои сообщения инициализации до A. Как и для любой автоматической переменной, область видимости t3 ограничена включающей функцией main. Соответственно t3 создается в месте инициализации сразу после A.

После B вы можете видеть сообщение, соответствующее инициализации t4 5. Обратите внимание, что соответствующее сообщение, генерируемое динамическим деструктором Tracer, отсутствует. Причина в том, что вы (намеренно) потеряли память для объекта, на который указывает t4. Поскольку команды delete t4 не было, деструктор никогда не будет вызван.

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

Наконец, статические и локальные поточные переменные t1 и t2 уничтожаются непосредственно перед выходом из программы, в результате чего получаются последние два сообщения в листинге 4.7.

Исключения


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

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

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

Ключевое слово throw

Чтобы вызвать исключение, используйте ключевое слово throw, за которым следует бросаемый объект.

Большинство объектов являются бросаемыми. Однако рекомендуется использовать одно из исключений, доступных в stdlib, например std::runtime_error в заголовке <stdеxcept>. Конструктор runtime_error принимает const char* с нулевым символом в конце, описывающий природу состояния ошибки. Это сообщение можно получить с помощью метода what, который не принимает параметров.

Класс Groucho в листинге 4.8 создает исключение всякий раз при вызове метода forget с аргументом, равным 0xFACE.

Листинг 4.8. Класс Groucho

#include <stdexcept>#include <cstdio>struct Groucho {   void forget(int x) {      if (x == 0xFACE) {         throw1 std::runtime_error2{ "I'd be glad to make an exception." };      }      printf("Forgot 0x%x\n", x);    }};

Чтобы вызвать исключение, в листинге 4.8 используется ключевое слово throw 1, за которым следует объект std::runtime_error 2.

Использование блоков try-catch

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

Листинг 4.9 показывает использование блока try-catch для обработки исключений, генерируемых объектом Groucho.

В методе main создается объект Groucho, а затем устанавливается блок try-catch 1. В части try вызывается метод forget класса groucho с несколькими различными параметрами: 0xC0DE 2, 0xFACE 3 и 0xC0FFEE 4. Внутри части catch обрабатываются любые исключения std::runtime_error 5, выводя сообщение в консоли 6.

Листинг 4.9. Использование try-catch для обработки исключений класса Groucho

#include <stdexcept>#include <cstdio>struct Groucho {      --пропуск--};int main() {   Groucho groucho;   try { 1       groucho.forget(0xC0DE); 2       groucho.forget(0xFACE); 3       groucho.forget(0xC0FFEE); 4    } catch (const std::runtime_error& e5) {       printf("exception caught with message: %s\n", e.what()); 6    }}

При запуске программы в листинге 4.9 вы получите следующий вывод:

Forgot 0xc0deexception caught with message: I'd be glad to make an exception.

При вызове forget с параметром 0xC0DE 2 groucho выводит Forgot0xc0de и завершает выполнение. При вызове forget с параметром 0xFACE 3 groucho выдает исключение. Это исключение остановило нормальное выполнение программы, поэтому forget никогда больше не вызывается 4. Вместо этого исключение в полете перехватывается 5, а его сообщение выводится в консоль 6.

Классы исключений stdlib

Можно организовать классы в родительско-дочерние отношения, используя наследование. Наследование оказывает большое влияние на то, как код обрабатывает исключения. Существует простая и удобная иерархия существующих типов исключений, доступных для использования в stdlib. Стоит попытаться использовать эти типы для простых программ. Зачем изобретать велосипед?

Стандартные классы исключений

stdlib предоставляет стандартные классы исключений в заголовке <stdеxcept>. Они должны стать вашим первым причалом при программировании исключений. Суперклассом для всех стандартных классов исключений является класс std::exception. Все подклассы в std::exception могут быть разделены на три группы: логические ошибки (logic_error), ошибки выполнения (runtime_error) и ошибки языковой поддержки. Ошибки языковой поддержки обычно не относятся к вам как к программисту, но вы наверняка столкнетесь с логическими ошибками и ошибками выполнения. Рисунок 4.1 обобщает их отношения.

image


КРАТКИЙ КУРС ПО НАСЛЕДОВАНИЮ

Прежде чем вводить исключения stdlib, нужно понять простое наследование классов C++ на очень высоком уровне. Классы могут иметь подклассы, которые наследуют функциональность своих суперклассов. Синтаксис в листинге 4.10 определяет это отношение.

Листинг 4.10. Определение суперклассов и подклассов

struct Superclass {    int x;};struct Subclass : Superclass { 1    int y;    int foo() {      return x + y; 2    }};

В Superclass нет ничего особенного. Но вот объявление Subclass 1 является особенным. Оно определяет отношения наследования с использованием синтаксиса: Superclass. Subclass наследует члены от Superclass, которые не помечены как private. Это можно увидеть в действии, когда Subclass использует поле x 2. Это поле принадлежит Superclass, но поскольку Subclass наследует от Superclass, x доступно.

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

Логические ошибки

Логические ошибки происходят из класса logic_error. Как правило, можно избежать эти исключения путем более тщательного программирования. Основной пример логическое предусловие класса не выполняется, например, когда инвариант класса не может быть установлен. (Вспомните из главы 2, что инвариант класса это особенность класса, которая всегда верна.)

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

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

  • domain_error сообщает об ошибках, связанных с допустимым диапазоном ввода, особенно для математических функций. Например, квадратный корень поддерживает только неотрицательные числа (в реальном случае). Если передается отрицательный аргумент, функция квадратного корня может выдать domain_error.
  • Исключение invalid_argument сообщает, как правило, о неожиданных параметрах.
  • Исключение length_error сообщает, что какое-либо действие нарушит ограничение максимального размера.
  • Исключение out_of_range сообщает, что некоторое значение не находится в ожидаемом диапазоне. Каноническим примером является индексирование с проверкой границ в структуре данных.

Ошибки выполнения

Ошибки выполнения происходят из класса runtime_error. Эти исключения помогают сообщать об ошибках, которые выходят за рамки программы. Как и logic_error, runtime_error имеет несколько подклассов, которые могут оказаться полезными:

  • system_error сообщает, что операционная система обнаружила некоторую ошибку. Такого рода исключения могут тысячи раз встретиться на вашем пути. Внутри заголовка <system_error> находится большое количество кодов ошибок и их состояний. Когда создается system_error, информация об ошибке упаковывается, чтобы можно было определить природу ошибки. Метод .code() возвращает enumclass типа std::errc, который имеет большое количество значений, таких как bad_file_descriptor, timed_out и license_denied,
  • overflow_error и underflow_error сообщают об арифметическом переполнении и потере значимости соответственно.


Другие ошибки наследуются напрямую от exception. Распространенным является исключение bad_alloc, которое сообщает, что new не удалось выделить необходимую память для динамического хранения.

Ошибки языковой поддержки

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

Обработка исключений

Правила обработки исключений основаны на наследовании классов. Когда выбрасывается исключение, блок catch обрабатывает его, если тип выброшенного исключения соответствует типу исключения обработчика или если тип выброшенного исключения наследуется от типа исключения обработчика.

Например, следующий обработчик перехватывает любое исключение, которое наследуется от std::exception, включая std::logic_error:

try {   throw std::logic_error{ "It's not about who wrong "                          "it's not about who right" };} catch (std::exception& ex) {   // Обрабатывает std::logic_error. Поскольку он наследуется от std::exception}

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

try {  throw 'z'; // Don't do this.} catch (...) {  // Обрабатывает любое исключение, даже 'z'}

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

Можно обрабатывать различные типы исключений, происходящих из одного и того же блока try, объединяя операторы catch, как показано здесь:

try {  // Код, который может вызвать исключение  --пропуск--} catch (const std::logic_error& ex) {  // Запись исключения и завершение работы программы; найдена программная ошибка!  --пропуск--} catch (const std::runtime_error& ex) {  // Делаем все, что можно  --пропуск--} catch (const std::exception& ex) {  // Обработка любого исключения, наследуемого от std:exception,  // которое не является logic_error или runtime_error.  --пропуск--} catch (...) {  // Паника; было сгенерировано непредвиденное исключение  --пропуск--}

Обычно такой код можно увидеть в точке входа в программу.

ПЕРЕБРАСВАНИЕ ИСКЛЮЧЕНИЯ
В блоке catch можно использовать ключевое слово throw, чтобы возобновить поиск подходящего обработчика исключений. Это называется перебрасыванием исключения. Есть несколько необычных, но важных случаев, когда вы, возможно, захотите дополнительно проверить исключение, прежде чем обработать его, как показано в листинге 4.11.

Листинг 4.11. Перебрасывание ошибки

try {  // Код, который может вызвать system_error  --пропуск--} catch(const std::system_error& ex) {   if(ex.code()!= std::errc::permission_denied){   // Ошибка, не связанная с отказом в доступе     throw; 1}  // Восстановление после ошибки   --пропуск--}

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

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

Листинг 4.12. Перехват конкретного исключения, но не перебрасывание


try {  // Генерация исключения PermissionDenied  --пропуск--} catch(const PermissionDenied& ex) {  // Восстановление после ошибки EACCES (отказано в доступе) 1  --пропуск--}

Если генерируется std::system_error, обработчик PermissionDenied 1 не поймает его. (Конечно, обработчик std::system_error все равно можно оставить, чтобы перехватывать такие исключения, если это необходимо.)

Пользовательские исключения

Программист может при необходимости определить свои собственные исключения; обычно эти пользовательские исключения наследуются от std::exception. Все классы из stdlib используют исключения, которые происходят от std::exception. Это позволяет легко перехватывать все исключения, будь то из вашего кода или из stdlib, с помощью одного блока catch.

Ключевое слово noexcept

Ключевое слово noexcept еще один термин, связанный с исключениями, который следует знать. Можно и нужно пометить любую функцию, которая в теории не может вызвать исключение, ключевым словом noexcept, как показано ниже:

bool is_odd(int x) noexcept {  return 1 == (x % 2);}

Функции с пометкой noexcept составляют жесткий контракт. При использовании функции, помеченной как noexcept, вы можете быть уверены, что функция не может вызвать исключение. В обмен на это вы должны быть предельно осторожны, когда помечаете собственную функцию как noexcept, так как компилятор не может это проверить. Если код выдает исключение внутри функции, помеченной как noexcept, это плохо. Среда выполнения C++ вызовет функцию std::terminate, которая по умолчанию завершит работу программы через abort. После такого программа не может быть восстановлена:

void hari_kari() noexcept {   throw std::runtime_error{ "Goodbye, cruel world." };}

Пометка функции ключевым словом noexcept позволяет оптимизировать код, полагаясь на то, что функция не может вызвать исключение. По сути, компилятор освобождается для использования семантики переноса, что может быть выполнено быстрее (подробнее об этом в разделе Семантика перемещения, с. 184).
ПРИМЕЧАНИЕ
Ознакомьтесь с правилом 14 Эффективного использования C++ Скотта Мейерса, чтобы подробно обсудить noexcept. Суть в том, что некоторые конструкторы переноса и операторы присваивания переноса могут выдавать исключение, например если им нужно выделить память, а система не работает. Если конструктор переноса или оператор присваивания переноса не указывает иное, компилятор должен предполагать, что перенос может вызвать исключение. Это отключает определенные оптимизации.

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

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

image

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

Стеки вызовов и обработка исключений

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

Выбрасывание исключений из деструктора

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

Допустим, генерируется исключение и во время размотки стека другое исключение генерируется деструктором во время обычной очистки. Теперь у вас есть два исключения в состоянии полета. Как среда выполнения C++ должна справляться с такой ситуацией?

У вас может быть свое мнение на этот счет, но среда выполнения вызовет функцию terminate (завершение). Рассмотрим листинг 4.13, который показывает, что может произойти при выбрасывании исключений из деструктора:

Листинг 4.13. Программа, где показана опасность создания исключения в деструкторе

#include <cstdio>#include <stdexcept>struct CyberdyneSeries800 {  CyberdyneSeries800() {   printf("I'm a friend of Sarah Connor."); 1  }  ~CyberdyneSeries800() {    throw std::runtime_error{ "I'll be back." }; 2}};  int main() {    try {      CyberdyneSeries800 t800; 3      thro std::runtime_error{ "Come with me if you want to live." }; 4    } catch(const std::exception& e) { 5      printf("Caught exception: %s\n", e.what()); 6    }}----------------------------------------------------------------------I'm a friend of Sarah Connor. 

ПРИМЕЧАНИЕ
Листинг 4.13 вызывает std::terminate, поэтому в зависимости от операционной среды может быть показано всплывающее окно с уведомлением.

Во-первых, был объявлен класс CyberdyneSeries800, который имеет простой конструктор, который выводит сообщение 1, и воинственный деструктор, который генерирует необработанное исключение 2. В main определяется блок try, в котором инициализируется CyberdyneSeries800 под именем t800 3, и выбрасывается runtime_error 4. В лучшем случае блок catch 5 обработает это исключение, выведет его сообщение 6 и все выйдет изящно. Поскольку t800 это автоматическая переменная в блоке try, она разрушается во время обычного процесса поиска обработчика для исключения, которое было выброшено 4. А поскольку t800 создает исключение в своем деструкторе 2, программа вызывает std::terminate и внезапно завершается.

Как правило, обращайтесь с деструкторами так, как если бы они были noexcept.

Класс SimpleString


Используя расширенный пример, давайте рассмотрим, как конструкторы, деструкторы, члены и исключения объединяются. Класс SimpleString в листинге 4.14 позволяет добавлять строки в стиле C и выводить результат.

Листинг 4.14. Конструктор и деструктор класса SimpleString

#include <stdexcept>struct SimpleString {  SimpleString(size_t max_size) 1    : max_size{ max_size }, 2      length{} { 3    if (max_size == 0) {      throw std::runtime_error{ "Max size must be at least 1." }; 4    }    buffer = new char[max_size]; 5    buffer[0] = 0; 6    }   ~SimpleString() {     delete[] buffer; 7    }--пропуск--private:    size_t max_size;    char* buffer;    size_t length;};

Конструктор 1 принимает один параметр max_size. Это максимальная длина строки, которая включает символ завершения строки. Инициализатор члена 2 сохраняет эту длину в переменной-члене max_size. Это значение также используется в выражении new массива для выделения буфера для хранения данной строки 5. Полученный указатель сохраняется в buffer. Длина инициализируется нулем 3, и это гарантирует, что по крайней мере буфер будет достаточного размера для хранения нулевого байта 4. Поскольку строка изначально пуста, первый байт буфера заполняется нулем 6.
ПРИМЕЧАНИЕ
Поскольку max_size это size_t, он не имеет знака и не может быть отрицательным, поэтому не нужно проверять это фиктивное условие.

Класс SimpleString владеет ресурсом памятью, на которую указывает буфер, которая должна быть освобождена при прекращении использования. Деструктор содержит одну строку 7, которая освобождает buffer. Поскольку распределение и освобождение buffer связаны конструктором и деструктором SimpleString, память никогда не будет потеряна.

Этот шаблон называется получение ресурса есть инициализация (RAII), или получение конструктора освобождение деструктора (CADRe).
ПРИМЕЧАНИЕ
Класс SimpleString все еще имеет неявно определенный конструктор копирования. Несмотря на то что память не может быть потеряна, при копировании класс потенциально освободится вдвое. Вы узнаете о конструкторах копирования в разделеСемантике копирования, с. 176. Просто знайте, что листинг 4.14 это обучающий инструмент, а не рабочий код.

Добавление и вывод

Класс SimpleString пока не очень полезен. В листинг 4.15 добавлена возможность выводить строку и добавлять набор символов в конец строки.

Листинг 4.15. Методы print и append_line для SimpleString

#include <cstdio>#include <cstring>#include <stdexcept>struct SimpleString {  --пропуск--  void print(const char* tag) const { 1    printf("%s: %s", tag, buffer);  }  bool append_line(const char* x) { 2  const auto x_len = strlen3(x);  if (x_len + length + 2 > max_size) return false; 4  std::strncpy(buffer + length, x, max_size - length);  length += x_len;  buffer[length++] = '\n';  buffer[length] = 0;  return true; } --пропуск--};


Первый метод print 1 выводит строку. Для удобства можно предоставить строку tag, чтобы можно было сопоставить вызов print с результатом. Этот метод является постоянным, потому что нет необходимости изменять состояние SimpleString.

Метод append_line 2 принимает строку с нулем в конце и добавляет ее содержимое плюс символ новой строки в buffer. Он возвращает true, если был успешно добавлен, и false, если не было достаточно места. Во-первых, append_line должен определить длину x. Для этого используется функция strlen 3 из заголовка <сstring>, которая принимает строку с нулевым символом в конце и возвращает ее длину:

size_t strlen(const char* str);

strlen используется для вычисления длины x и инициализации x_len с результатом. Этот результат используется для вычисления того, приведет ли добавление x (символов новой строки) и нулевого байта к текущей строке к получению строки с длиной, превышающей max_size 4. Если это так, append_line возвращает false.

Если для добавления x достаточно места, необходимо скопировать его байты в правильное место в buffer. Функция std::strncpy 5 из заголовка <сstring> является одним из подходящих инструментов для этой работы. Она принимает три параметра: адрес назначения, адрес источника и количество символов для копирования:

char* std::strncpy(char* destination, const char* source, std::size_t num);

Функция strncpy будет копировать до num байтов из source в destination. После завершения она вернет значение destination (которое будет отброшено).

После добавления количества байтов x_len, скопированных в buffer, к length работа завершается добавлением символа новой строки \n и нулевого байта в конец buffer. Функция возвращает true, чтобы указать, что введенный х был успешно добавлен в виде строки в конец буфера.
ПРЕДУПРЕЖДЕНИЕ
Используйте strncpy очень осторожно. Слишком легко забыть символ конца строки в исходной строке или не выделить достаточно места в целевой строке. Обе ошибки приведут к неопределенному поведению. Мы рассмотрим более безопасную альтернативу во второй части книги.

Использование SimpleString
Листинг 4.16 показывает пример использования SimpleString, где добавляются несколько строк и промежуточные результаты выводятся в консоль.

Листинг 4.16. Методы SimpleString

#include <cstdio>#include <cstring>#include <exception>struct SimpleString {   --пропуск--}int main() {   SimpleString string{ 115 }; 1   string.append_line("Starbuck, whaddya hear?");   string.append_line("Nothin' but the rain."); 2   string.print("A"); 3   string.append_line("Grab your gun and bring the cat in.");   string.append_line("Aye-aye sir, coming home."); 4   string.print("B"); 5   if (!string.append_line("Galactica!")) { 6      printf("String was not big enough to append another message."); 7   }}

Сначала создается SimpleString с max_length=115 1. Метод append_line используется дважды 2, чтобы добавить некоторые данные в строку, а затем вывести содержимое вместе с тегом A 3. Затем добавляется больше текста 4 и снова выводится содержимое, на этот раз с тегом B 5. Когда append_line определяет, что SimpleString исчерпал свободное пространство 6, возвращается false 7. (Вы как пользователь SimpleString несете ответственность за проверку этого условия.)

Листинг 4.17 содержит выходные данные запуска этой программы.

Листинг 4.17. Результат выполнения программы в листинге 4.16

A: Starbuck, whaddya hear? 1Nothin' but the rain.B: Starbuck, whaddya hear? 2Nothin' but the rain.Grab your gun and bring the cat in.Aye-aye sir, coming home.String was not big enough to append another message. 3


Как и ожидалось, строка содержит Starbuck, whaddya hear?\nNothin' but the rain.\nвA 1. (Вспомните из главы 2, что \n это специальный символ новой строки.) После добавления Grab your gun and bring the cat in. и Aye-aye sir, coming home. вы получите ожидаемый результат в B 2.

Когда листинг 4.17 пытается добавить Galactica! в string, append_line возвращает false, поскольку в buffer недостаточно места. Это вызывает вывод сообщения String was not big enough to append another message 3.

Составление SimpleString

Рассмотрим, что происходит при определении класса с членом SimpleString, как показано в листинге 4.18.

Как предполагает инициализатор члена 1, string полностью построена, и ее инварианты класса назначаются после выполнения конструктора SimpleStringOwner. Здесь демонстрируется порядок членов объекта во время создания: члены создаются перед вызовом конструктора окружающего объекта. Смысл есть, а иначе как можно установить инварианты класса без знаний об инвариантах его членов?

Листинг 4.18. Реализация SimpleStringOwner

#include <stdexcept>struct SimpleStringOwner {   SimpleStringOwner(const char* x)     : string{ 10 } { 1     if (!string.append_line(x)) {       throw std::runtime_error{ "Not enough memory!" };    }    string.print("Constructed");  }  ~SimpleStringOwner() {    string.print("About to destroy"); 2  }private:  SimpleString string;};

Деструкторы работают в обратном порядке. Внутри ~SimpleStringOwner() 2 нужно хранить инварианты класса строки, чтобы можно было напечатать ее содержимое. Все члены уничтожаются после вызова деструктора объекта.


В листинге 4.19 используется SimpleStringOwner.

Листинг 4.19. Программа, содержащая SimpleStringOwner

--пропуск--int main() {   SimpleStringOwner x{ "x" };   printf("x is alive\n");}--------------------------------------------------------------------Constructed: х 1x is aliveAbout to destroy: х 2

Как и ожидалось, член string в x 1 создается надлежащим образом, потому что конструкторы членов объекта вызываются перед конструктором объекта, в результате чего появляется сообщение Constructed: x. Как автоматическая переменная x уничтожается непосредственно перед выходом из main, и вы получаете сообщение About to destroy: x 2. Член string все еще доступен в этот момент, потому что деструкторы членов вызываются после деструктора вмещающего объекта.

Размотка стека вызовов

Листинг 4.20 демонстрирует, как обработка исключений и размотка стека работают вместе. Блок try-catch устанавливается в main, после чего выполняется серия вызовов функций. Один из этих вызовов вызывает исключение.

Листинг 4.20. Программа, где используется SimpleStringOwner и размотка стека вызовов

--пропуск--void fn_c() {   SimpleStringOwner c{ "cccccccccc" }; 1}void fn_b() {  SimpleStringOwner b{ "b" };  fn_c(); 2}int main() {  try { 3   SimpleStringOwner a{ "a" };   fn_b(); 4   SimpleStringOwner d{ "d" }; 5 } catch(const std::exception& e) { 6  printf("Exception: %s\n", e.what()); }}

В листинге 4.21 показаны результаты запуска программы из листинга 4.20.

Листинг 4.21. Результат запуска программы из листинга 4.20

Constructed: aConstructed: bAbout to destroy: bAbout to destroy: aException: Not enough memory!

Вы установили блок try-catch 3. Первый экземпляр SimpleStringOwner, a, создается без инцидентов, и в консоль выводится сообщение Constructed: а. Далее вызывается fn_b 4. Обратите внимание, что вы все еще находитесь в блоке try-catch, поэтому любое выброшенное исключение будет обработано. Внутри fn_b другой экземпляр SimpleStringOwner, b, успешно создается, и Constructed: b выводится на консоль. Затем происходит вызов еще одной функции, fn_c 2.

Давайте на минуту остановимся, чтобы разобраться, как выглядит стек вызовов, какие объекты живы и как выглядит ситуация обработки исключений. Сейчас у нас есть два живых и действительных объекта SimpleStringOwner: a и b. Стек вызовов выглядит как main() fn_ () fn_c(), и в main настроен обработчик исключений для обработки любых исключений. Эта ситуация показана на рис. 4.3.

В 1 возникает небольшая проблема. Напомним, что SimpleStringOwner имеет член SimpleString, который всегда инициализируется с max_size 10. При попытке создания c конструктор SimpleStringOwner выдает исключение, потому что вы пытались добавить cccccccccc, который имеет длину 10, что выходит за рамки, потому что нужно еще добавить символы новой строки и завершения строки.

Теперь в полете находится одно исключение. Стек будет раскручиваться до тех пор, пока не будет найден соответствующий обработчик, и все объекты, выпадающие из области видимости в результате этого раскручивания, будут уничтожены. Обработчик доходит до стека 6, поэтому fn_c и fn_b разматываются. Поскольку SimpleStringOwner b это автоматическая переменная в fn_b, она разрушается и в консоль выводится сообщение About to destroy: b. После fn_b автоматические переменные внутри try {} уничтожаются. Это включает в себя SimpleStringOwner a, поэтому в консоль выводится About to destroy: a.

image

Как только исключение происходит в блоке try{}, дальнейшие операторы не выполняются. В результате d никогда не инициализируется 5 и конструктор d не вызывается и не выводится в консоль. После размотки стека вызовов выполнение сразу переходит к блоку catch. В итоге в консоль выводится сообщение Exception: Not enough memory! 6.

Исключения и производительность

Обработка исключений обязательна в программах; ошибки неизбежны. При правильном использовании исключений ошибок не возникает, код работает быстрее, чем код, проверенный вручную. Если ошибка все-таки есть, обработка исключений может иногда выполняться медленнее, но у этого есть огромные преимущества в надежности и удобстве обслуживания по сравнению с альтернативными вариантами. Курт Гантерот, автор Оптимизации программ на C++, хорошо пишет об этом: Использование обработки исключений приводит к программам, которые работают быстрее при нормальном выполнении и ведут себя лучше в случае неудачи. Когда программа на C++ выполняется нормально (без исключений), при проверке исключений не возникает никаких издержек во время выполнения. Вы платите только за исключение.

Надеюсь, вы убедились в центральной роли, которую играют исключения в идиоматических программах на C++. К сожалению, иногда нет возможности использовать исключения. Одним из примеров является встроенная разработка, где требуются гарантии в реальном времени. Инструменты просто не существуют (пока) для этих настроек. Если повезет, это скоро изменится, но сейчас приходится обходиться без исключений в большинстве встроенных контекстов. Другой пример некоторый устаревший код. Исключения изящны из-за того, как они вписываются в объекты RAII. Когда деструкторы отвечают за очистку ресурсов, раскрутка стека является прямым и эффективным способом защиты от утечек памяти. В устаревшем коде можно найти ручное управление ресурсами и обработку ошибок вместо объектов RAII. Это делает использование исключений очень опасным, поскольку размотка стека безопасна только для объектов RAII. Без них можно с легкостью допустить утечку ресурсов.

Альтернативы для исключений

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

struct HumptyDumpty {   HumptyDumpty();   bool is_together_again();  --пропуск--};

В идиоматическом C++ вы бы просто сгенерировали исключение в конструкторе, но здесь следует помнить о проверке и обработке ситуации как условии ошибки в вызывающем коде:

bool send_kings_horses_and_men() {  HumptyDumpty hd{};  if (hd.is_together_again()) return false;  // Использование инвариантов класса hd гарантировано.  // HumptyDumpty с треском проваливается.  --пропуск--  return true;}

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

Листинг 4.22. Фрагмент кода с объявлением структурированной привязки

struct Result { 1   HumptyDumpty hd;   bool success;   };  Result make_humpty() { 2    HumptyDumpty hd{};    bool is_valid;    // Проверка правильности hd и установка соответствующего значения is_valid    return { hd, is_valid };   }bool send_kings_horses_and_men() {   auto [hd, success] = make_humpty();    if(!success) return false;   // Установка инвариантов класса   --пропуск--   return true;}

Сначала объявляется POD, который содержит HumptyDumpty и флаг success 1. Затем определяется функция make_humpty 2, которая создает и проверяет HumptyDumpty. Такие методы называются фабричными, поскольку их целью является инициализация объектов. Функция make_humpty оборачивает его и флаг success в Result при возврате. Синтаксис в точке вызова 3 показывает, как можно распаковать Result, получив несколько переменных с определением типа при помощи auto.
ПРИМЕЧАНИЕ
Более подробное описание структурированных привязок приведено в подразделе Структурированные привязки, с. 289.


Об авторе

Джош Лоспинозо (Josh Lospinoso) доктор философии и предприниматель, прослуживший 15 лет в армии США. Джош офицер, занимающийся вопросами кибербезопасности. Написал десятки программ для средств информационной безопасности и преподавал C++ начинающим разработчикам. Выступает на различных конференциях, является автором более 20 рецензируемых статей и стипендиатом Родса, а также имеет патент. В 2012 году стал соучредителем успешной охранной компании. Джош ведет блог и активно участвует в разработке ПО с открытым исходным кодом.

О научном редакторе

Кайл Уиллмон (Kyle Willmon) разработчик информационных систем с 12-летним опытом в C++. В течение 7 лет работал в сообществе по информационной безопасности, используя C++, Python и Go в различных проектах. В настоящее время является разработчиком в команде Sony Global Threat Emulation.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону C++

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Перевод Как в Runescape ловят пользователей ботов, и почему они не поймали меня

19.04.2021 18:14:05 | Автор: admin

Автоматизация игроков всегда была большой проблемой в глобальных многопользовательских онлайновых ролевых играх (MMORPG), таких как World of Warcraft и Runescape, и этот вид взлома игр значительно отличается от традиционных читов, например в стрелялках. Однажды в выходные я решил взглянуть на системы обнаружения, созданные компанией Jagex для предотвращения автоматизации игроков в Runescape и вот что из этого вышло.


Использование ботов

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

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

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

Эвристика!

Я начал с анализа клиента Runescape, чтобы подтвердить эту теорию, и быстро заметил глобально вызываемую переменную hhk, которая задаётся вскоре после запуска.

const auto module_handle = GetModuleHandleA(0);hhk = SetWindowsHookExA(WH_MOUSE_LL, rs::mouse_hook_handler, module_handle, 0);

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

Обработчик мыши Runescape довольно прост по своей сути (следующий псевдокод красиво переписан вручную):

LRESULT __fastcall rs::mouse_hook_handler(int code, WPARAM wParam, LPARAM lParam){  if ( rs::client::singleton )  {      // Call the internal logging handler      rs::mouse_hook_handler_internal(rs::client::singleton->window_ctx, wParam, lParam);  }  // Pass the information to the next hook on the system  return CallNextHookEx(hhk, code, wParam, lParam);}void __fastcall rs::mouse_hook_handler_internal(rs::window_ctx *window_ctx, __int64 wparam, _DWORD *lparam){  // If the mouse event happens outside of the Runescape window, don't log it.  if (!window_ctx->event_inside_of_window(lparam))  {    return;  }  switch (wparam)  {    case WM_MOUSEMOVE:      rs::heuristics::log_movement(lparam);      break;case WM_LBUTTONDOWN:case WM_LBUTTONDBLCLK:case WM_RBUTTONDOWN:case WM_RBUTTONDBLCLK:case WM_MBUTTONDOWN:case WM_MBUTTONDBLCLK:  rs::heuristics::log_button(lparam);  break;  }}

С учётом пропускной способности эти функции rs::heuristics::log_* используют простые алгоритмы для пропуска данных событий, которые похожи на предыдущие зарегистрированные события.

Эти данные события позже анализируются функцией rs::heuristics::process, которая вызывается каждым фреймом в основном цикле рендеринга.

void __fastcall rs::heuristics::process(rs::heuristic_engine *heuristic_engine){  // Don't process any data if the player is not in a world  auto client = heuristic_engine->client;  if (client->state != STATE_IN_GAME)  {    return;  }  // Make sure the connection object is properly initialised  auto connection = client->network->connection;  if (!connection || connection->server->mode != SERVER_INITIALISED)  {    return;  }  // The following functions parse and pack the event data, and is later sent  // by a different component related to networking that has a queue system for  // packets.  // Process data gathered by internal handlers  rs::heuristics::process_source(&heuristic_engine->event_client_source);  // Process data gathered by the low level mouse hook  rs::heuristics::process_source(&heuristic_engine->event_hook_source);}

Вдали от клавиатуры?

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

Запретив игре вызывать функцию rs::heuristics::process, я сразу ничего не заметил, но ровно через пять минут вышел из игры. По-видимому, Runescape принимает решение о неактивности игрока просто по эвристическим данным, отправленным клиентом на сервер, хотя вы можете просто отлично играть в эту игру. Это породило новый вопрос: если сервер не считает, что я играю, то считает ли он, что я использую бота?

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

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

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

Другие профессии и курсы

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Отладочный вывод на микроконтроллерах как Concepts и Ranges отправили мой printf на покой

09.05.2021 22:19:11 | Автор: admin

Здравствуйте! Меня зовут Александр и я работаю программистом микроконтроллеров.

Начиная на работе новый проект, я привычно набрасывал в project tree исходники всяческих полезных утилит. И на хедере app_debug.h несколько подзавис.

Дело в том, что в декабре прошлого года у GNU Arm Embedded Toolchain вышел релиз 10-2020-q4-major, включающий все GCC 10.2 features, а значит и поддержку Concepts, Ranges, Coroutines вкупе с другими, менее "громкими" новинками С++20.

Воодушевленное новым стандартом воображение рисовало мой будущий С++ код ультрасовременным и лаконично-поэтичным. И старый, добрый printf("Debug message\n") в это благостное видение не очень-то вписывался.

Хотелось бескомпромиссной плюсовой функциональности и стандартных удобств!

float raw[] = {3.1416, 2.7183, 1.618};array<int, 3> arr{123, 456, 789};cout << int{2021}       << '\n'     << float{9.806}    << '\n'     << raw             << '\n'     << arr             << '\n'     << "Hello, Habr!"  << '\n'     << ("esreveR me!" | views::take(7) | views::reverse ) << '\n';

Ну а если хочется хорошего, зачем же себе отказывать?

Реализуем на С++20 интерфейс потока для отладочного вывода МК, поддерживающий любой подходящий протокол, предусмотренный вендром камня. Легковесный и быстрый, без бойлерплейта. Поддерживающий как блокирующий посимвольный вывод - для нечувствительных к времени выполнения участков кода, так и неблокирующий, для быстрых функций.

Зададим для комфортного чтения кода несколько удобных алиасов:

using base_t = std::uint32_t;using fast_t = std::uint_fast32_t;using index_t = std::size_t;

Как известно, в микроконтроллерах неблокирующие алгоритмы передачи данных реализуются на прерываниях и DMA. Для идентификации режимов вывода заведем enum:

enum class BusMode{BLOCKING,IT,DMA,};

Опишем базовый класс, реализующий логику протоколов, ответственных за отладочный вывод:

class BusInterface
template<typename T>class BusInterface{public:using derived_ptr = T*;    static constexpr BusMode mode = T::mode;void send (const char arr[], index_t num) noexcept {if constexpr (BusMode::BLOCKING == mode){derived()->send_block(arr, num);} else if (BusMode::IT == mode){derived()->send_it(arr, num);} else if (BusMode::DMA == mode){derived()->send_dma(arr, num);}}private:derived_ptr derived(void) noexcept{return static_cast<derived_ptr>(this);}void send_block (const char arr[], const index_t num) noexcept {}void send_it (const char arr[], const index_t num) noexcept {}void send_dma (const char arr[], const index_t num) noexcept {}};

Класс реализован по паттерну CRTP, что дает нам преимущества полиморфизма времени компиляции. Класс содержит единственный публичный метод send(), в котором на этапе компиляции, в зависимости от режима вывода, выбирается нужный метод. В качестве аргументов метод принимает указатель на буфер с данными и его полезный размер. На моей практике это самый распространенный формат аргументов в HAL-функциях вендоров МК.

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

class Uart
template<BusMode Mode>class Uart final : public BusInterface<Uart<Mode>> {private:static constexpr BusMode mode = Mode;void send_block (const char arr[], const index_t num) noexcept{HAL_UART_Transmit(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num),base_t{5000});}    void send_it (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_IT(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}void send_dma (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_DMA(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}friend class BusInterface<Uart<BusMode::BLOCKING>>;friend class BusInterface<Uart<BusMode::IT>>;friend class BusInterface<Uart<BusMode::DMA>>;};

По аналогии можно реализовать классs и других протоколов, поддерживаемых микроконтроллером, заменив в методах send_block(), send_it() и send_dma() соответствующие функции HAL. Если протокол передачи данных поддерживает не все режимы, тогда соответствующий метод просто не определяем.

И в завершении этой части заведем короткие алиасы итогового класса Uart:

using UartBlocking = BusInterface<Uart<BusMode::BLOCKING>>;using UartIt = BusInterface<Uart<BusMode::IT>>;using UartDma = BusInterface<Uart<BusMode::DMA>>;

Отлично, теперь разработаем класс потока вывода:

class StreamBase
template <class Bus, char Delim>class StreamBase final: public StreamStorage{public:using bus_t = Bus;  using stream_t = StreamBase<Bus, Delim>;static constexpr BusMode mode = bus_t::mode;StreamBase() = default;~StreamBase(){ if constexpr (BusMode::BLOCKING != mode) flush(); }  StreamBase(const StreamBase&) = delete;StreamBase& operator= (const StreamBase&) = delete;stream_t& operator << (const char_type auto c){if constexpr (BusMode::BLOCKING == mode){bus.send(&c, 1);} else {*it = c;it = std::next(it);}return *this;}stream_t& operator << (const std::floating_point auto f){if constexpr (BusMode::BLOCKING == mode){auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data());bus.send(ptr, cnt);} else {auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data() + std::distance(buffer.begin(), it));it = std::next(it, cnt);}return *this;}stream_t& operator << (const num_type auto n){auto [ptr, cnt] = NumConvert::to_string_integer( n, &buffer.back() );if constexpr (BusMode::BLOCKING == mode){bus.send(ptr, cnt);} else {auto src = std::prev(buffer.end(), cnt + 1);it = std::copy(src, buffer.end(), it);}return *this;}stream_t& operator << (const std::ranges::range auto& r){        std::ranges::for_each(r, [this](const auto val) {                        if constexpr (char_type<decltype(val)>){                            *this << val;            } else if (num_type<decltype(val)> || std::floating_point<decltype(val)>){                *this << val << Delim;            }        });return *this;}private:void flush (void) {bus.send(buffer.data(), std::distance(buffer.begin(), it));it = buffer.begin();}std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};bus_t bus;}; 

Рассмотрим подробнее его значимые части.

Шаблон класса параметризуется классом протокола, значением Delim типа char и наследуется от класса StreamStorage. Единственная задача последнего - предоставить доступ к массиву char, в котором будут формироваться строки вывода в неблокирующем режиме. Имплементацию здесь не привожу, она вторична к рассматриваемой теме; оставляю на ваше усмотрение или утяните из моего примера в конце статьи. Для удобной и безопасной работы с этим массивом (в примере - storage) мы заведем два приватных члена класса:

std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};

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

Публичные методы класса - это четыре перегрузки operator<<. Три из них - для вывода базовых типов, с которыми наш интерфейс будет работать (char, float и integral type), а четвертая - для вывода содержимого массивов и стандартных контейнеров.

Вот здесь начинается самая вкуснота.

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

template <typename T>concept char_type = std::same_as<T, char>;template <typename T>concept num_type = std::integral<T> && !char_type<T>;

... и концепты из стандартной библиотеки - std::floating_point и std::ranges::range.

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

Логика внутри каждого оператора вывода базового типа проста. В зависимости от режима вывода (блокирующий / не блокирующий) мы или сразу отправляем символ на печать, либо формируем в буфере потока строку. И в момент выхода из функции объект нашего потока разрушается, вызывается деструктор, где приватный метод flush() отправляет заготовленную строку на печать в режиме IT или DMA.

При конвертации числового значения в массив char-ов я отказался от известной идиомы с snprintf() в пользу наработок neiver. Автор в своих публикациях показывает заметное превосходство предложенных им алгоритмов конвертации чисел в строку как в размере бинарника, так и в скорости преобразования. Позаимствованный у него код я инкапсулировал в классе NumConvert, содержащем методы to_string_integer() и to_string_float().

В перегрузке оператора вывода данных массива/контейнера мы с помощью стандартного алгоритма std::ranges::for_each() пробегаемся по содержимому рэйнджа и если элемент удовлетворяет концепту char_type, выводим строку слитно. Если же удовлетворяет концептам num_type или std::floating_point, разделяем значения с помощью заданного значения Delim.

Ну хорошо, мы тут наворотили шаблонов, концептов и прочей плюсовой тяжелой артиллерии. Это ж какой длины мы получим ассемблерную портянку на выходе? Посмотрим два примера:

int main() {    using StreamUartBlocking = StreamBase<UartBlocking, ' '>;    StreamUartBlocking cout;    cout << 'A'; // 1  cout << ("esreveR me!" | std::views::take(7) | std::views::reverse); // 2    return 0;}

Выставим флаги компилятора: -std=gnu++20 -Os -fno-exceptions -fno-rtti. Тогда на первом примере мы получим следующий ассемблерный листинг:

main:        push    {r3, lr}        movs    r0, #65        bl      putchar        movs    r0, #0        pop     {r3, pc}

На втором:

.LC0:        .ascii  "esreveR me!\000"main:        push    {r3, r4, r5, lr}        ldr     r5, .L4        movs    r4, #5.L3:        subs    r4, r4, #1        bcc     .L2        ldrb    r0, [r5, r4]    @ zero_extendqisi2        bl      putchar        b       .L3.L2:        movs    r0, #0        pop     {r3, r4, r5, pc}.L4:        .word   .LC0

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

Конечно же, при выводе числовых значений, добавится еще код конвертации числа в строку.

Потестировать онлайн можно здесь (hardware dependent код заменил для наглядности на putchar() ).

Рабочий код проекта смотрите/забирайте отсюда. Там реализован пример из начала статьи.

Это стартовый вариант, для уверенного использования еще требуются некоторые доработки и тесты. Например, нужно предусмотреть механизм синхронизации при неблокирующем выводе - когда, скажем, вывод данных предыдущей функции еще не завершен, а мы в следующей функции уже переписываем буфер новой информацией. Также нужно еще внимательно поэкспериментровать с алгоритмами std::views. Например std::views::drop() при применении ее к строковому литералу или массиву char-ов, взрывается ошибкой "inconsistent directions for distance and bound". Ну что ж, стандарт новый, со временем освоим.

Как это работает можно посмотреть здесь. Проект поднят на двухядерном STM32H745; с одного ядра (480МГц) вывод идет в блокирующем режиме через отладочный интерфейс SWO, код примера выстреливается за 9,2 мкс, со второго(240МГц) - через Uart в режиме DMA, примерно за 20 мкс.

Как-то так.

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

Подробнее..

Перевод Linux-контейнеры в паре строчек кода

11.11.2020 16:12:52 | Автор: admin
В продолжение прошлой статьи о KVM публикуем новый перевод и разбираемся, как работают контейнеры на примере запуска Docker-образа busybox.

Эта статья о контейнерах является продолжением предыдущей статьи о KVM. Я бы хотел показать, как именно работают контейнеры, запустив Docker-образ busybox в нашем собственном небольшом контейнере.

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

BusyBox Docker


Нашей главной целью будет запустить обычный образ busybox для Docker, но без Docker. Docker использует btrfs в качестве файловой системы для своих образов. Давайте попробуем скачать образ и распаковать его в директорию:

mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -


Теперь у нас есть файловая система образа busybox, распакованная в папку rootfs. Конечно, можно запустить ./rootfs/bin/sh и получить рабочую shell-оболочку, но если мы посмотрим на список процессов, файлов, или сетевых интерфейсов, увидим, что у нас есть доступ ко всей ОС.

Итак, давайте попробуем создать изолированную среду.

Clone


Поскольку мы хотим контролировать то, к чему имеет доступ дочерний процесс, мы будем использовать clone(2) вместо fork(2). Clone делает почти то же самое, но позволяет передавать флаги, указывая, какие ресурсы вы хотите разделять (с хостом).

Разрешены следующие флаги:

  • CLONE_NEWNET изолированные сетевые устройства
  • CLONE_NEWUTS имя хоста и домена (система разделения времени UNIX)
  • CLONE_NEWIPC объекты IPC
  • CLONE_NEWPID идентификаторы процессов (PID)
  • CLONE_NEWNS точки монтирования (файловые системы)
  • CLONE_NEWUSER пользователи и группы.

В нашем эксперименте мы попытаемся изолировать процессы, IPC, сетевые и файловые системы. Итак, начинаем:

static char child_stack[1024 * 1024];int child_main(void *arg) {  printf("Hello from child! PID=%d\n", getpid());  return 0;}int main(int argc, char *argv[]) {  int flags =      CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;  int pid = clone(child_main, child_stack + sizeof(child_stack),                  flags | SIGCHLD, argv + 1);  if (pid < 0) {    fprintf(stderr, "clone failed: %d\n", errno);    return 1;  }  waitpid(pid, NULL, 0);  return 0;}

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

Эксперимент дает любопытный результат: дочерний PID равен 1. Нам хорошо известно, что PID 1 обычно у процесса init. Но в этом случае дочерний процесс получает свой собственный изолированный список процессов, где он стал первым процессом.

Рабочая оболочка


Чтобы упростить изучение новой среды, запустим shell-оболочку в дочернем процессе. Давайте запускать произвольные команды, наподобие docker run:

int child_main(void *arg) {  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Теперь запуск нашего приложения с аргументом /bin/sh открывает настоящую оболочку, в которой мы сможем вводить команды. Такой результат доказывает, насколько мы ошибались, говоря об изолированности:

# echo $$
1
# ps
PID TTY TIME CMD
5998 pts/31 00:00:00 sudo
5999 pts/31 00:00:00 main
6001 pts/31 00:00:00 sh
6004 pts/31 00:00:00 ps


Как мы видим, сам процесс shell-оболочки имеет PID равный 1, но, на самом деле, он может видеть и получать доступ ко всем другим процессам основной ОС. Причина в том, что список процессов читается из procfs, которая все еще наследуется.

Итак, размонтируем procfs:

umount2("/proc", MNT_DETACH);

Теперь при запуске shell-оболочки ломаются команды ps, mount и другие, потому что procfs не смонтирована. Однако это все равно лучше, чем утечка родительской procfs.

Chroot


Для создания корневого каталога обычно применяется chroot, но мы воспользуемся альтернативой pivot_root. Этот системный вызов переносит текущий корень системы в подкаталог и назначает другую директорию корнем:

int child_main(void *arg) {  /* Unmount procfs */  umount2("/proc", MNT_DETACH);  /* Pivot root */  mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");  mkdir("./rootfs/oldrootfs", 0755);  syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");  chdir("/");  umount2("/oldrootfs", MNT_DETACH);  rmdir("/oldrootfs");  /* Re-mount procfs */  mount("proc", "/proc", "proc", 0, NULL);  /* Run the process */  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Имеет смысл смонтировать tmpfs в /tmp, sysfs в /sys и создать действующую файловую систему /dev, но для краткости я пропущу этот шаг.

Теперь мы видим только файлы из образа busybox, как будто мы использовали chroot:

/ # ls
bin dev etc home proc root sys tmp usr var

/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)

/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
4 root 0:00 ps

/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /bin/sh
5 root 0:00 ps ax


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

Сеть


Создать новое сетевое пространство имен было только началом! Нужно назначить ему сетевые интерфейсы и настроить их для правильной пересылки пакетов.

Если у вас нет интерфейса br0, необходимо создать вручную (brctl является частью пакета bridge-utils в Ubuntu):

brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0 -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE

В моем случае, wlp3s0 был основным сетевым интерфейсом WiFi, а 172.16.x.x сетью для контейнера.

Наша программа запуска контейнеров должна создать пару интерфейсов, veth0 и veth1, связать их с br0 и настроить маршрутизацию внутри контейнера.

В функции main() мы запустим перед клонированием эти команды:

system("ip link add veth0 type veth peer name veth1");system("ip link set veth0 up");system("brctl addif br0 veth0");

По окончании вызова clone() мы добавим veth1 в новое дочернее пространство имен:

char ip_link_set[4096];snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",         pid);system(ip_link_set);

Теперь, если мы запустим ip link в оболочке контейнера, мы увидим интерфейс loopback и некоторый интерфейс veth1@xxxx. Но сеть по-прежнему не работает. Зададим уникальное имя хоста в контейнере и настроим маршруты:

int child_main(void *arg) {  ....  sethostname("example", 7);  system("ip link set veth1 up");  char ip_addr_add[4096];  snprintf(ip_addr_add, sizeof(ip_addr_add),           "ip addr add 172.16.0.101/24 dev veth1");  system(ip_addr_add);  system("route add default gw 172.16.0.100 veth1");  char **argv = (char **)arg;  execvp(argv[0], argv);  return 0;}

Посмотрим, как это выглядит:

/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff

/ # hostname
example

/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...

Работает!

Вывод


Полный исходный код доступен по ссылке. Если вы обнаружили ошибку или у вас есть какое-то предложение, оставьте, пожалуйста, комментарий!

Безусловно, Docker способен на гораздо большее! Но удивительно, сколько подходящих API имеет ядро Linux и как легко их использовать, чтобы достичь виртуализации на уровне ОС.

Надеюсь, вам понравилась статья. Вы можете найти проекты автора на Github и подписаться на Twitter, чтобы следить за новостями, а также через rss.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru