C++ Embedded
Открыть в Telegram
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Больше446
Подписчики
Нет данных24 часа
+27 дней
+430 день
Архив постов
446
rule A9-5-1
Томаш мне определенно не нравился. Собрав вокруг себя клубок единомышленников, он в очередной раз устроил набег на мой пулл-реквест. Весь код был испещрен комментариями, казавшимися мне нестерпимо глупыми, пошлыми и издевательскими настолько, что заставляли глаз непроизвольно дергаться. Мне определенно нужна была своя банда, чтоб отбиваться вместе.
Позвонил нашему новичку, Марчину, уж его-то будет проще вытащить на мою орбиту. Однако небольшой тест на лояльность не помешает.
"Привет, Марчин!" — сказал я как можно беззаботней. — "Подскажи, что значит: chuj ci w oko?"
Мой коллега забавно округлил глаза и недоуменно произнес: "Привет, а зачем тебе?".
"Да вот, Томаш мне утром сказал", — продолжал я с невинным видом, — "говорит, это что-то среднее между 'будь здоров' и 'очень ценю ваше мнение'".
"Кгхммм..." — кажется, Марчин начал задыхаться, на очах его выступили слезы, — "нуууу... раз он так сказал, то так и есть".
"Спасибо! Я знал, что ты не подведешь! Но пасаран!" - отключив сконфуженно улыбающегося Марчина, я с горечью подумал: "Этакая ты бобр курва, не выйдет у нас с тобой объединения".
А что же AUTOSAR говорит по поводу объединений?
Правило
A9-5-1 гласит:
Объединения не должны использоваться.Лапидарно, доходчиво. Обоснование приводится подобающее: объединения ни разу не типобезопасны, и их использование может легко заморочить голову разработчику, который, скорее всего, поймет вашу хитрую конструкцию неправильно. Однако какое же правило без исключений?! Строгие законы разрешают использовать меченый
union, если std::variant пока недоступен. Вернее, если у вас такой ретроградный проект, что стандарт C++17 так же недоступен, как и девицы на огромных белых БМВ.
Под мечеными объединениями имеется в виду, что мы где-то будем хранить номер или тип активного члена объединения. Выглядеть это должно примерно так:
struct Tagged
{
enum class Type {
UINT,
FLOAT
};
union {
uint32_t u;
float f;
};
Type which;
};
Рядом с union есть еще enum, который описывает возможные состояния объединения: если значение which равно UINT, то активен uint32_t u, если FLOAT, то float f.
Авторы стандарта предлагают использовать это подобным образом:
Tagged un; un.u = 12; un.which = Tagged::TYPE::UINT; un.u = 3.14f; un.which = Tagged::TYPE::FLOATУдобство использования просто поражает воображение, зато это совершенно легально! Но, я думаю, все уже догадались, что это и есть примитивный
std::variant. Поэтому, как только в наш компилятор завозят это чудо стандартной библиотеки, нужно бегом переходить на него.
Это, конечно, не полный эквивалент объединения. Например, теперь вы не можете задать пустое объединение:
union Empty {}; // Ok
std::variant<> v; // Error
Но зачем бы такое могло понадобиться, ума не приложу. Зато теперь при разрушении объекта std::variant разрушается и инициализированный объект внутри. Не нужно выковыривать его вручную.
А что же внутри у std::variant? Да тот же union и индекс активного элемента, но без стандартной хитрости не обошлось, конечно. Иначе и быть не могло, без вариантов.446
union & memory leak
Да провалиться мне на этом самом месте, если прошлый случай не из реальной практики! Если бы я намеревался выдумать хороший пример про утечку памяти, то я бы вспомнил про объединения. Казус с забытыми объектами в
union я подсмотрел, путешествуя в недрах стандартной библиотеки.
Допустим, есть у нас хорошо уже знакомый класс R:
struct R {};
using UniqueR = std::unique_ptr<R>;
Мы желаем в некотором классе example хранить либо уникальный указатель на объект R, либо магическое число. Для таких сомнительных оптимизаций можно воспользоваться услугами анонимных объединений:
class example {
union {
int smth;
UniqueR ptr = UniqueR{new R};
};
public:
example() {}
~example() {}
};
Что такое объединения? Это класс, но не простой, а сделанный с особым хитрованством. Плюсы предполагают, что в объединении мы работаем только с одним членом класса, то есть активен только один из нескольких заявленных. Выбрать активный по умолчанию элемент можно инициализацией. В нашем примере это ptr. Поэтому при создании объекта класса example будет вызван конструктор unique_ptr и конструктор R.
Можно изменить активный член по умолчанию:
union {
int smth = 1;
UniqueR ptr;
};
Здесь активным становится smth, конструктор UniqueR не вызывается. Нельзя только одновременно инициализировать и smth, и ptr, сразу выскочит ошибка multiple fields in union 'example::<unnamed union>' initialized.
Однако мы отвлеклись, проблема не в этом. Несмотря на то, что union есть класс, при уничтожении объекта example, ptr не будет удален автоматически! Да-да, не удивляйтесь, деструктор не будет вызван.
В разделе стандарта 11.5 Unions [class.union] мы можем найти подсказки, почему это так.
Объединение, как и всякий порядочный класс, может иметь функции-члены, даже конструкторы и деструкторы. Только не вирутальные, но кому они нужны?
Если какой-то член объединения имеет нетривиальный конструктор по умолчанию, деструктор или иные специальные методы, то они должны быть явно определены для всего объединения, либо будут неявно удалены.
Например, рассмотрим такое объединение:
union U {
int i;
float f;
std::string s;
};
у класса std::string вообще мало чего есть тривиального, поэтому стандарт велит тайно удалить у объединения U вообще все. Тут без обмана, попытка создать объект типа U обернется ошибкой. Нужно дополнить объединение хотя бы конструктором и деструктором, чтобы объект можно было бы сконструировать без проблем. С анонимным объединением все гораздо печальнее: здесь конструктор и деструктор в принципе нельзя определить, ввиду полного отсутствия имени.
Для корректной работы с объединением нужно удалить объекты вручную
~example() { ptr.~UniqueR(); }
В общем, плюсы намекают, мол, сами следите: чего вы там насоздавали в объединении, то и удаляйте.446
weird memory leak
Как сейчас помню, обсуждали, что бы такого интересного рассказать на конференции. Жара была страшная, шел грибной снег. Почему бы не рассказать про опасность утечек памяти? Ведь даже RAII не гарантирует успех, если применять его бездумно. Вот допустим, в одном модуле прошивки мы создаем на некотором участке памяти некий объект
UniqueR, что на самом деле std::unique_ptr<R>:
struct R {};
using UniqueR = std::unique_ptr<R>;
new (buffer) UniqueR {new R};
Этот buffer мы передаем в другой модуль прошивки и восстанавливаем оригинальный объект через насильственное приведение к указателю.
UniqueR *ptr = static_cast<UniqueR *>(buffer);
Теперь, когда объект UniqueR у нас в руках, модуль вызывает функцию обработки.
handle(std::move(*ptr));
Обработчик пустой, поскольку нам лень что-то делать.
void handle(UniqueR && data) {}
К вящему удивлению, конструктор R вызывается один единственный раз, деструктор не вызывается вовсе, так же как и не происходит уничтожение объекта UniqueR.
Происходит банальная утечка памяти, несмотря на якобы переданное владение объектом UniqueR обработчику.
Здесь собеседник прервал мой рассказ: "Вам не кажется, что пример несколько надуман? Такое в реальной разработке никто не станет делать! В здравом уме-то..."
Как я ни убеждал, что это относительно свежий случай из реальной практики, никто не поверил.
...подающий надежды талант из очень Латинской Америки, Хосе работал в штате у заказчика. Он никак не мог найти, где протекает память, поэтому попросил меня. Мне было скучно, и я с удовольствием ловил ошибку в течение нескольких часов. Как оказалось, в механизм передачи внутренних сообщений (что-то вроде xQueue во FreeRTOS) закрались непростительные грехи!
Хосе вероломно нарушил правило A8-4-12:
Объект std::unique_ptr может быть передан в функцию только: (1) по значению, предполагает передачу владения (2) по lvalue ссылке для выражения того, что функция только производит некоторые манипуляции над объектом без овладевания оным.В качестве исключения допускается переход владения путем передачи
std::unique_ptr по rvalue ссылке, если эта ссылка перемещается в объект std::unique_ptr внутри вызываемой функции.
Код Хосе плох со всех сторон. В обработчике не происходит передача владения, поскольку нет перемещения ссылки в объект.
Должно быть так:
void handle(UniqueR && data) {
UniqueR tmp{std::move(data)};
}
или так:
void handle(UniqueR) {}
В этих случаях владение действительно передается, и при выходе из обработчика вызываются все нужные деструкторы.
Ну и еще одно маленькое замечание. Объект создается через placement new, но юное дарование не посчитало нужным вызвать деструктор явно.
Должно быть хотя бы так:
UniqueR *ptr = static_cast<UniqueR *>(buffer);
handle(std::move(*ptr));
ptr->~UniqueR();
"Почему так?" - с болью в голосе вопрошал я Хосе.
"Разве unique_ptr не должен был все разрулить? Я же передавал владение объектом. Вот в rust все безопасно, не понимаю, почему в плюсах все так сложно", - пожимал он плечами.
"Да все просто, надо только их знать!" - укорял я.
"Ой, ну все! Я вот не знаю плюсов, всю эту ерунду мне пишет chatGPT"
Я в ужасе смотрел на него. Теперь многое стало на свои места.
"Тебе все равно никто не поверит", - залился он беззаботным смехом.446
transaction_safe & runtime_error
Хоть на улице еще тепло, но не нужно даже переворачивать календарь, чтоб понять какой сейчас месяц. Черно-белые стайки scholaris vulgaris (школотрон обыкновенный) уже кучкуются возле казенных учреждений, а это первый и бесспорный признак осени. Скоро снова будем любоваться красиво желтеющей листвой тенистых аллей, а в тишине морозного утреннего воздуха слышать крик отчаяния
std::runtime_error, с которым стандартная библиотека GCC вытворяет такое, что вышептать страшно. Поэтому надо смотреть.
Допустим, я поддерживаю свою нестандартную библиотеку, и с незапамятных времен имеется там некая структура error. Времена меняются, появляется модное поветрие: все вдруг захотели использовать транзакционную память. Чтоб идти в ногу со временем, нужно добавить поддержку этой экзотики, но обойтись малой кровью.
Для начала добавим макрос _GLIBCXX_TXN_SAFE!
// mystd.h
#if __cpp_transactional_memory >= 201500L
#define _GLIBCXX_TXN_SAFE transaction_safe
#else
#define _GLIBCXX_TXN_SAFE
#endif
Поскольку __cpp_transactional_memory определен только при сборке со специальным флагом -fgnu-tm, то в других случаях этот макрос на код никак не повлияет. Незаметно поместим его в определение error:
struct error {
int x;
error(int z) _GLIBCXX_TXN_SAFE;
~error() _GLIBCXX_TXN_SAFE;
};
Для нормальных людей ничего не поменяется, они по-прежнему могут включать заголовочный файл моей библиотеки, не опасаясь подвоха. Реализация конструктора и деструктора тоже останется на своих местах:
// mystd.cpp
error::error(int _x) : x{_x} {}
error::~error() { x = 0; }
И библиотеку я буду собирать по-старому, без всяких там -fgnu-tm. "Однако тогда транзакционники не смогут использовать библиотеку!" - изумитесь вы. Конечно смогут! Ведь я вручную наклепаю полный набор конструкторов и деструкторов для такого случая.
extern "C" {
void _ZGTtN5errorC1Ei(error * that, int i) { that->x = i * 2; }
void _ZGTtN5errorC2Ei(error * that, int i) { that->x = i * 3; }
void _ZGTtN5errorD1Ev(error * that) { x = -1; }
void _ZGTtN5errorD2Ev(error * that) { x = -2; }
void _ZGTtN5errorD0Ev(error * that) { x = -3; }
}
Выглядит ужасно. Конструктор для особых случаев больше не копирует действия основного конструктора. Это может быть оправдано, ведь конструкторы классов std::runtime_error могут быть довольно сложными, и не все действия могут быть выполнены в транзакции, вот и приходится исхитряться: например, вместо operator new в транзакционном конструкторе можно вызвать _ZGTtna (т.е. transaction clone for operator new[]).
Тогда при подключении нашей библиотеки к такому целевому коду (параметры компиляции -lmystd -fgnu-tm)
// main.cpp
atomic_noexcept {
error p{10};
}
скомпилированный код будет выглядеть примерно так:
bl _ITM_beginTransaction ... bl _ZGTtN5errorC2Ei add r3, r7, #8 mov r0, r3 bl _ZGTtN5errorD2Ev.localalias bl _ITM_commitTransactionЗдесь компилятор вызовет именно транзакционную версию конструктора и деструктора, те, что мы обманным путем определили вручную. Чуете, чем пахнет? Грязным хаком!
446
transactional memory
Снились мне братья Алиевы. Стоя на плоту, они молча укоряли меня за нераскрытую тему переопределенных конструкторов
std::runtime_error. Им все еще было невдомек, зачем это понадобилось. Можно предположить, что в GCC все сошли с ума, как и на Балабановской фабрике, но такое простое объяснение чаще всего ложно.
Приглядимся получше к образовавшемуся безобразию. В файле cow-stdexcept.cc реализован макрос CTORDTOR, и слабонервных попросим изучать его внутренности закрытыми глазами:
#define CTORDTOR(NAME, CLASS, BASE) \
void _ZGTtNSt##NAME##C1EPKc (CLASS* that, const char* s) { \
...
Далее следуют заклинания, призывающие Вельзевула.
Чуть ниже в этом же файле этот дьявольский макрос применяется к std::runtime_error и его наследникам:
CTORDTOR(13runtime_error, std::runtime_error, runtime_error)
Это значит, что макрос определит псевдоконструктор _ZGTtNSt13runtime_errorC1EPKc. Очень необычное и редкое имя, почти как Эраст. Меня нельзя назвать большим любителем подглядывать за символами в объектных файлах, но префикса GTt не помню в именах конструкторов. Раздекорируем имя утилитой c++filt:
echo _ZGTtNSt13runtime_errorC1EPKc | c++filt
transaction clone for std::runtime_error::runtime_error(char const*)
Что задумало GCC? Какие еще клоны?
Срочно открываем Itanium C++ ABI, где тема декорирования имен раскрыта полностью. Судя по истории изменений документа, еще в 2015 году (это важно!) там появилась поддержка неких transaction-safe функций. В разделе 5.1.4.6 Transaction-Safe Function Entry Points без труда находим описание:
Функция, объявленная transaction_safe или имеющая атрибут [[optimize_for_synchronized]], имеет две точки входа: нормально декорированное имя функции, которая используется для вызова вне транзакционного контекста, и другая точка входа для вызова в контексте транзакций. Декорированное имя функции в контексте транзакций такое же, как и обычное, с добавлением префикса GTt
<special-name> ::= GTt <encoding>
Ну, теперь все стало на свои места! Жуткий макрос CTORDTOR определяет конструкторы для контекста транзакционной памяти!
Это довольно старая концепция, описана много десятилетий назад. Тема обширная, но если в двух словах, то эти ваши мьютексы и прочие lock-механизмы - это путь пессимистов. Транзакции же - путь оптимистов, ведь мы считаем, что в большинстве своем процессы будут менять разные участки памяти независимо в рамках транзакций. Если коллизии все же происходят, и процессы работают с одним и тем же участком памяти, то одна транзакция откатывается и будет повторяться, пока не сработает чисто. В святые 90-е появились программные реализации (STM), а позже, в 2000-е, и реализации в железе (HTM). Неудивительно, что возрастал интерес со стороны С++. В 2008 году была сформирована группа для проработки внедрения TM в стандарт. Уже на следующий год появляется черновик Draft Specification of Transactional Language Constructs for C++, версии 1.0. В 2015 году даже было готово рацпредложение Technical Specification for C++ Extensions for Transactional Memory, описывающее, какие изменения надо внести в стандарт для поддержки транзакций.
Однако в настоящее время это все еще не часть стандарта. Хотя можно подключить библиотеку libitm, добавив при компиляции флаг -fgnu-tm.
В этом случае, если пометить конструктор как transaction_safe
class error {
public:
error() transaction_safe { ... }
};
компилятор любезно создаст для нас как обычный конструктор _ZN5errorC2Ev, так и его транзакционную версию _ZGTtN5errorC2Ev. Последний имеет смысл только в особом транзакционном контексте. Например, в той же библиотеке есть такая сущность, как синхронизированный блок. Это не транзакция, но уже кое-что: все инструкции в синхронизированных блоках будто бы защищены глобальной блокировкой. Это означает, что все синхронизированные выполняются по-порядку и не мешают друг другу, поэтому результат работы одного синхронизированного блока доступен в следующем.
Так вот, в таком блоке мы напишем
synchronized {
error e{};
}
Тут и пригодится наш конструктор-клон _ZGTtN5errorC2Ev, дальше действовать будет он.446
ctor trick
"Спрячь этот слайд, его никто не должен увидеть!" - мой собеседник неуловимо изменился в лице, когда понял, о чем я рассказываю. То ли он побледнел, то ли поседел, по видеосвязи сложно было понять, впрочем, восторгом он явно не лучился: "Не представляешь, что начнется, если разработчики узнают правду!"
Вот так из моего доклада пропал важный кусок, раскрывающий грязные секретики GCC. На самом деле я хотел показать еще одну причину никогда не использовать
std::runtime_error, уж больно хитро он сделан. Слишком вычурно для нашего прямолинейного кода. Однако начнем издалека.
Допустим, есть у нас простецкий класс:
class example {
public:
example(int const x) : _x{x} {}
int _x;
};
Ничего примечательного, кроме конструктора. Да и тот, как вы знаете, не совсем настоящая функция: адрес конструктора невозможно получить.
Точно невозможно? Ведь пользовательские конструкторы вполне себе настоящие, по крайней мере, в нашем классе example. И вот однажды в недрах GCC увидел я хитрый трюк: можно обратиться к конструктору через декорированное имя! В этом случае вся эта ваша плюсовая магия рассеивается, как тень в теплой августовской ночи, и остаются лишь банальные функции.
using ctor_t = void(*)(example *, int);
ctor_t ctor = &_ZN7exampleC2Ei;
Вот и поймали мы конструктор базового объекта за хвост и можем вполне его применить:
alignas(example) std::array<uint8_t, sizeof(example)> buffer;
example *pex = reinterpret_cast<example *>(buffer.data());
ctor(pex, 20);
assert(pex->_x == 20); // OK!
Тут мы насильственно создаем указатель на объект example, память для которого выделяем в буфере buffer, и вызываем конструктор как обычную функцию для нашего рукотворного объекта.
Кроме этого GCC позволяет дополнять недоопределенные конструкторы вручную.
Допишем около нашего класса еще одну неприметную функцию:
extern "C" void _ZN7exampleC1Ei(example *that, int x) {
that->_x = x + 100;
}
Теперь скажите, чему будет равно значение внутренней переменной здесь:
example ex {10};
Правильно, ex._x == 110!
Как такое вообще возможно? GCC, следуя ABI, создает несколько конструкторов:
- конструктор полного объекта с суффиксом C1,
- конструктор базового объекта с суффиксом C2.
В нашем примере, когда мы создаем объект, то вызываем конструктор полного объекта (C1), и если нет сложного наследования, то C1 используется всегда. А вот конструктор класса example реализуется для базового объекта (С2). А работает эта схема потому, что директивно один конструктор отождествляется со вторым:
.weak _ZN7exampleC1Ei
.thumb_set _ZN7exampleC1Ei,_ZN7exampleC2Ei
Вот этой слабостью можно воспользоваться, мы просто определим свой конструктор полного типа с блэкджеком и девицами.
Однако эта функция только изображает из себя конструктор, ибо оперировать приватными членами класса она не может. Зато вполне легально внутри конструктора полного объекта можно вызвать базовый настоящий:
extern "C" void _ZN7exampleC2Ei(example *that, int x);
extern "C" void _ZN7exampleC1Ei(example *that, int x) {
_ZN7exampleC2Ei(that, x + 100);
}
Никому не рекомендую использовать эти трюки, тем более у нормальных людей нужды в таком не возникает. А за остальными выехал отдел цензуры.446
abi::__cxa_demangle
Древний трактат Лунь Юй повествует о том, что однажды Цзы Лу спросил Конфуция: "Вэйский правитель привлечет вас к управлению государством. Что вы сделаете наперво?"
Конфуций ответил: "Необходимо начать с исправления имен".
Цзы Лу заметил: "Вы начинаете издалека. Зачем нужно исправлять имена?"
Тут учитель сказал: "Как ты необразован! Я тебе перегрузку функций без декорирования рожу, что ли?!"
Философ был прав, декорирование имен - это очень интересная особенность плюсов, четко обособляющая его от прародителя. Как вы помните, мы можем получить такие имена как результат функции
__cxa_current_exception_type, а уж если в припадке безумия мы захотим использовать RTTI... жаль, что названия эти без допинга не разобрать. Однако ABI в очередной раз протягивает нам функцию помощи. Такой прием использует обработчик terminate по умолчанию:
void default_terminate() {
std::type_info *exc = abi::__cxa_current_exception_type();
if (exc) {
char const *name = exc->name();
int status = -1;
char *dem = abi::__cxa_demangle(name, 0, 0, &status);
fprintf(stderr, "terminate called after '%s'\n", (status == 0? dem : name));
if (status == 0) free(dem);
} ...
Есть некая магическая функция, которая разбирает каракули компилятора и возвращает нормальное имя типа:
char* __cxa_demangle(const char* mangled_name, char* output_buffer, size_t* length, int* status);
где mangled_name - нуль-терминированная строка или C-строка, содержащая имя, которое должно быть раздекорировано;
output_buffer - область динамически выделенной памяти размером *length байт, куда будет записано раздекорированное имя. Если буфера output_buffer недостаточно, он будет увеличен через realloc. Допустимо передать NULL вместо output_buffer; в этом случае функция сама вызовет malloc и запишет раздекорированное имя в полученную память;
Если length не NULL, в *length записывается длина раздекорированного имени. Но если мы сами передаем буфер output_buffer, то через этот параметр должна быть передана длина буфера.
В *status записывается одно из следующих значений:
0: раздекорирование прошло успешно.
-1: Ошибка динамического выделения памяти.
-2: mangled_name - неправильное имя согласно C++ ABI правилам декорирования имен.
-3: Один из аргументов неправильный.
Если совесть и стандарты безопасности позволяют работать с динамической памятью, то можно использовать unique_ptr, чтоб не забыть освободить память после использования.
std::unique_ptr<char, void(*)(void*)> uq {abi::__cxa_demangle(name, 0, 0, &status), std::free};
fprintf(stderr, "exception %s\r\n", uq.get());
При сильном желании и безрассудстве, можно попытаться использовать статический буфер.
char buffer[128];
size_t len = 128;
char *dem = abi::__cxa_demangle(name, buffer, &len, &status);
Но будьте осторожны, функция __cxa_demangle всегда полагает, что buffer получен в результате динамической аллокации, и если длины буфера не хватит под полученное имя, то в попытке расширить буфер через realloc, будет вызвана free(buffer). Это приведет к краху.
Интересно, что обратного преобразования из обычного в декорированное имя ABI не предлагает. Не больно-то и хотелось.
Чтоб два раза не вставать, скажу, что в обработчике terminate из исключения можно выжать больше, чем просто имя:
try {
__throw_exception_again;
} catch(const std::exception& exc) {
fprintf(stderr, " what(): %s\r\n", exc.what());
} catch(...) {}
Поскольку мы в подтвержденном контексте исключения, попробуем во внутреннем блоке try-catch повторно сгенерировать его через __throw_exception_again, что на самом деле просто псевдоним throw. Если это исключение наследник std::exception, то перехватим его и вывалим в поток все, что у него в методе what().
Так-то, Цзы Лу, если имена неправильны, то слова не имеют под собой оснований.446
std::terminate
"Уходить надо красиво", - говаривал один мой товарищ, хотя сам едва ли это умел. Он до того боялся сказать менеджеру о скором увольнении "по собственному", что решил инсценировать собственную погибель. Пришел на работу пораньше и усадил на стул в кубикле очень похожего на себя кадавра. В свитерах с оленями все мы на одно лицо. "Вот найдут сие хладное тело", - рассуждал он, - "решат, что сгорел на работе, да уволят по причине игры в ящик". К сожалению, дерзкий план рухнул. Менеджер не только не заметил снижения продуктивности работника, но и назначил покойника техлидом. Поскольку тот сделался приятным человеком, и больше никогда не спорил с начальством. Уходить нужно уметь. Даже приложение нужно покидать грациозно. Как
std::terminate. Допустим, мы помним, как и когда он действует, но что же он должен делать по умолчанию, если никаких обработчиков не назначено? Конечно, существует обработчик по умолчанию, который возвращает std::get_terminate(), если мы не устанавливали собственных.
В GCC незамутненная функция std::get_terminate() вернет некий abi::__terminate_handler. Что характерно, это глобальный объект. Определяется он примерно так:
std::terminate_handler __cxxabiv1::__terminate_handler =
_GLIBCXX_DEFAULT_TERM_HANDLER;
Непонятный дефайн портит ясную картину, поэтому для окончательного просветления:
#if _GLIBCXX_HOSTED && _GLIBCXX_VERBOSE && __cpp_exceptions
# define _GLIBCXX_DEFAULT_TERM_HANDLER __gnu_cxx::__verbose_terminate_handler
#else
# include <cstdlib>
# define _GLIBCXX_DEFAULT_TERM_HANDLER std::abort
#endif
Главное здесь заключается в том, что раскрывается он по-разному, в зависимости от параметров сборки стандартной библиотеки.
Если, например, исключения запрещены, то глобальным обработчиком std::terminate будет банальная std::abort. А вот если исключения присутствуют, то все гораздо веселее.
Что же делает __gnu_cxx::__verbose_terminate_handler?
Во-первых, он отслеживает вызовы terminate, в случае обнаружения рекурсии вызывает std::abort().
Во-вторых, проверяет, был ли вызван terminate в контексте исключения, иначе вызывает std::abort().
В-третьих, вызывает std::abort() в любом случае.
Второй пункт кажется наиболее интересным. Можно ли и в нашем самописном обработчике проверить контекст исключения? Легко!
void my_terminate_handler() {
std::type_info *t = abi::__cxa_current_exception_type();
if (t) {
char const *name = t->name();
fprintf(stderr, "name: %s\r\n", name);
}
}
Функция abi::__cxa_current_exception_type() возвращает объект типа обрабатываемого в данный момент исключения, иначе возвращает нулевой указатель.
Такой же типовой объект можно получить и в блоке catch при вызове abi::__cxa_current_exception_type(). У класса std::type_info не так много полезных методов, но самый наглядный для нас - это name(). Этот метод покажет нам настоящее имя объекта исключения. Одна беда только: имя типа декорированное.
Если мы напишем в коде такую конструкцию:
std::function<void()>{}();
то name() вернет что-то вроде St17bad_function_call, а для
throw 1;
name() вернет i.
Да, соглашусь, имена получились не очень, но это поправимо.446
std::zero_cost_conf
Вот и прошла конференция. Я, совершенно обессиленный, был погружен в поезд и отправлен восвояси. Может показаться, что мероприятие длится всего несколько часов, но на самом деле оно начинается за месяц до, когда ты неосторожно соглашаешься написать заявку. Ну, большое ли дело? В управляющую компанию я тоже заявки писал, чтоб садик перед домом облагородили, и они уже лет пять на рассмотрении. Здесь же события развивались со скоростью не Ласточки, но Синкансэна. Раз, и сообщают, что ты в программе конференции. Два, ты уже в зуме на черновом прогоне доклада. Три, уже в поезде мчишь в столицу, а в голове проносится: "Куда ты лезешь, она тебя сожрет!".
Кстати, перед конференцией обязательно должен быть технический прогон. Пришлось прибыть пораньше и предстать пред ясны очи организаторов а-ля натюрель.
"С Горького я, странник, пришел спикеров говорящих посмотреть...", - жалобно простонал я на входе и просочился в зал. Там на меня с осуждением смотрели бесконечные ряды пустых стульев, грозно намекая на завтрашний позор. Надев микрофон, я гордо поднялся на сцену. К сожалению, только мысленно. Первые пятнадцать минут меня пытались достать шваброй из-под сцены. Когда наконец-то я оказался в правильной конфигурации, на подмостках, то вместо доклада тихо запел "Боже, Царя храни". В глазах оргов блестели слезы. Это потому, что голос очень красивый. Я был единственным выпускником музыкальной школы, которому на экзамене запретили петь, дабы не смущать одноклассников, а главное, комиссию.
На следующий день меня выселили из гостиницы, поскольку стук зубов мешал соседям спать. Вот и на конференцию пришел пораньше, спрятался в спикерской и скрытно наблюдал другие выступления. И тут в докладе Антона Полухина (спокойно, он меня не заметил) я слышу, что наконец-то в стандарте будет
std::inplace_vector!
Это динамически изменяемый вектор, но с фиксированной емкостью, что определяется во время компиляции. Элементы хранятся внутри непрерывного участка памяти. Очень напоминает std::vector, но его можно и нужно использовать там, где динамическая аллокация не приемлема. Это что-то вроде boost::static_vector, только в стандарте.
Наконец-то можно отказаться от приплясываний около std::array с индексом - статического вектора, собранного на коленке.
"Вот ведь!" - думаю, - "такими темпами мне и рассказать будет не о чем!"
А тут еще оказывается и operator delete запретят использовать с неполными типами! AUTOSAR придется переписать еще раз. А как вообще так получается, что использовать new мы не можем с неполными типами, а delete - пожалуйста? Очень хороший пример есть в документе P3144R0 (Deprecate Delete of a Pointer to an Incomplete Type):
struct X;
X *createX();
int main() {
X *p = createX();
delete p;
}
struct X {?
int x;
}
X *createX() {
return new X {};
}
Вот тут, поскольку new мы используем внутри функции, компилятор не коробит, что в main у нас тип X неполностью определен, и поэтому delete вызывается безразмерный void operator delete (void *).
Это запретят в с++26! с++26? Тут я расслабился и чуть не проспал выступление. Остальное вы знаете. Спасибо, что смотрели.446
С++ Zero Cost Conf
Прошу прощения, но публикация продолжения пока откладывается. Все мое время было безжалостно съедено подготовкой к публичному мероприятию. Где-то недели две назад меня спросили, можем ли мы что-то рассказать интересного про наши проекты. Я лишь многозначительно кивнул головой. И вот, несколько дней назад узнаю, что уже 27 июля 2024 года мне предстоит выступать на крупной IT-конференции: С++ Zero Cost Conf https://cppzerocostconf.yandex.ru/cppzerocostconf_2024
Там хоть и написано, что доклад про избавление проекта от динамического распределения памяти, но это не точно. Скорее, поделюсь самым интересным, что встречалось на прошлом проекте с жесткими требования безопасности. Как выживать с MISRA на шее, что делать, если в лесу на вас напал AUTOSAR. Что не войдет по этическим соображениям в основной доклад, обязательно опубликую здесь.
Заходите, если будет время. Хотя я сам в субботу в городе не сидел бы. Лениво лежал бы где-нибудь на пляже и пальцем на песке рисовал полиморфные аллокаторы.
Ладно, побежал делать презентацию, а то пока только три слайда есть: титульный, приветствие и "спасибо за внимание".
До скорой встречи!
446
std::terminate_handler
Еще одна функция, которая помогает нам изящно завершать работу прошивки или правильно перезагружаться, -
std::terminate. Некоторые пишут, что это функция, вызываемая при сбое обработки исключения. Это почти правда, чаще всего std::terminate вызывается именно для них, родимых. Например, если мы не смогли перехватить сгенерированное исключение, если конструктор статического объекта или функция в std::atexit почему-то бросает исключение, то std::terminate тут как тут. Даже при нарушении спецификации динамического исключения, например, когда оно генерируется из функции, помеченной noexcept:
void func() noexcept {
std::function<void()> f;
f();
}
Это приводит к std::terminate, даже если мы попытаемся поймать исключение в блоке try.
Этим же закончится и попытка бросить исключение, если вдруг __cxa_allocate_exception не найдет достаточно памяти под него.
В конце концов, если в прошивке что-то пошло не так, то можно бросить белое полотенце на ринг, то есть вызвать std::terminate из пользовательского кода. При этом пользователь имеет возможность назначить собственный обработчик для таких исключительных ситуаций через функцию std::set_terminate, что может быть нам очень полезно. Тип обработчика должен совпадать с std::terminate_handler
void handle() {
// reset to safe state
}
Затем просто назначаем обработчик:
auto old = std::set_terminate(&handle);
Функция вернет старый обработчик, ради интереса его можно вызвать, получив отповедь в strerr и std::abort в конце: "terminate called without an active exception".
Чем же на самом деле занимается std::terminate? Заглянем в глубины стандартной библиотеки.
Объявлена она как [[noreturn]], поэтому не обязана и не будет ничего возвращать. Исключений сама не должна генерировать, что логично, ибо с этими исключениями ей же и возиться.
В GCC реализация выглядит так:
void std::terminate () throw() {
__cxxabiv1::__terminate (get_terminate ());
}
Здесь просто вызывается __cxxabiv1::__terminate для установленного обработчика или обработчика по умолчанию.
void __cxxabiv1::__terminate (std::terminate_handler handler) throw () {
__try {
handler ();
std::abort ();
} __catch(...)
{ std::abort (); }
}
По сути же, это просто обертка для отлова исключений, которых не должно быть при исполнении std::terminate. Если оно все же случается, то мы тут же попадаем на std::abort, иначе handler выполняется до конца.
Такой обработчик может обнаружить ситуацию, когда прошивка собирается с флагом -fno-exceptions, то есть исключения отключены, но линкуется с обычной libstdc++.a, собранной с -fexceptions. Как бы ужасно это ни было, когда что-то пойдет не так, какой-нибудь объект из стандартной библиотеки шаблонов может попытаться бросить исключение, а вот перехватить его будет нечем. Все закономерно закончится вызовом std::terminate.446
std::new_handler
Есть такие люди, которым всегда нужно контролировать ситуацию. Я думал, этих людей называют "нормальными разработчиками", но врачи говорят, что это обсессивно-компульсивное расстройство. В общем, шифер потек, кукуха поехала, говорят те, кто ничего не хочет контролировать. Поэтому они никогда и не узнают, что для четких пацанов якобы с ОКР С++ приготовил всякие обработчики интересных ситуаций. Одна из них - динамическое выделение памяти через оператор
new. Что обычно бывает, если выделение памяти проваливается? Мы ловим исключение std::bad_alloc. Если же исключения отключены в нашем небольшом embedded проекте из-за капризов спецификации безопасности, то довольствуемся вызовом std::abort. И это тоже может стать проблемой, если нам нужно перезагружаться "правильно", то есть хотя бы оставить тревожное сообщение в логе. Выход есть!
В разделе стандарта 16.4.5.7 Handler functions [handler.functions] можно найти описание:
Программа на с++ позволяет устанавливать различные обработчики прямо во время исполнения, просто передавая указатель на обработчик, определенный в программе или библиотеке, в функцию std::set_new_handler. Также в программе можно получить указатель на текущий обработчик, вызвав std::get_new_handler.
Сигнатура обработчика и служебный функцией определена в стандарте как:
using new_handler = void (*)();
new_handler get_new_handler() noexcept;
new_handler set_new_handler(new_handler new_p) noexcept;
Надо заметить, set_new_handler возвращает указатель на прежний установленный обработчик, либо nullptr.
Предполагается, что оператор new возвращает указатель на выделенную память, если все прошло гладко. Если же malloc возвращает NULL, как и в случае с крокодилом, есть два пути. Во-первых, если текущий обработчик не установлен (get_new_handler возвращает nullptr), new выбрасывает исключение bad_alloc.
Во-вторых, если обработчик имеется, то new его вызывает. Из обработчика вовсе не обязательно возвращаться, можно залогировать все, что нужно, и смело перезагружаться.
Если же обработчик милостиво вернул управление, тогда цикл повторяется. И будет крутиться до тех пор, пока попытка выделить память не увенчается успехом, либо обработчик все же прекратит страдания безнадежного приложения.
В стандартной библиотеке реализовано это примерно так:
void * operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc) {
void *p;
...
while ((p = malloc (sz)) == 0) {
new_handler handler = std::get_new_handler();
if (!handler) _GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler();
}
return p;
}
Какой прок будет от внедрения обработчика неприятных ситуаций в операторе new? Говорят, что это должно было позволить сделать красиво одну из трех вещей:
- можно попытаться освободить память;
- можно изящно завершить программу;
- можно выбросить исключение std::bad_alloc или его наследников.
Первый пункт предполагает, наверное, что мы найдем самый жирный объект и освободим память под ним, тогда запрошенный в new кусочек станет доступным. Эх, если бы MISRA не запрещала бы пользоваться динамическим распределением памяти, обязательно бы попробовал.446
memcpy_s implementation
Вот сижу, смотрю, как солнце красиво, будто сияющий клифф-дайвер, падает в воду. Стремительно, как и шансы
Annex K попасть в настоящий стандарт. Вы уже поняли, что никто не собирается реализовывать микрософтовские супер-пупер функции, но есть штрейкбрехеры, которые таки поспешили с реализацией странного. Давайте посмотрим на их поделия.
Наши китайские друзья вот так представляют себе тело функции:
errno_t __cdecl memcpy_s(void * dst,
size_t sizeInBytes,
const void * src,
size_t count) {
if (count == 0) {
/* nothing to do */
return 0;
}
/* validation section */
_VALIDATE_RETURN_ERRCODE(dst != NULL, EINVAL);
if (src == NULL || sizeInBytes < count) {
/* zeroes the destination buffer */
memset(dst, 0, sizeInBytes);
_VALIDATE_RETURN_ERRCODE(src != NULL, EINVAL);
_VALIDATE_RETURN_ERRCODE(sizeInBytes >= count, ERANGE);
/* useless, but prefast is confused */
return EINVAL;
}
memcpy(dst, src, count);
return 0;
}
Впрочем, они божатся, что все основано на исходном коде crt микрософта, который он когда-то предоставлял. Выглядит правдоподобно, я верю. Внутри функции memcpy_s мы сразу находим вызов memcpy. Похоже, новые функции никогда не смогут побить рекорд скорости старичков.
Перед одлскульной функцией копирования стоят всего-навсего многословные проверки на отсутствие тривиальных ошибок, или, как еще их называют, sanity check.
Макрос _VALIDATE_RETURN_ERRCODE примерно описывается как:
#define _VALIDATE_RETURN_ERRCODE( expr, errorcode ) { \
int _Expr_val=!!(expr); \
if ( !( _Expr_val ) ) { \
errno = errorcode; \
return ( errorcode ); \
}} \
Смысл действия очевиден, если выражение expr не true, то записываем код ошибки в errno и экстренно выходим и возвращаем тот же номер ошибки.
Проверяется, например, указатель на источник и приемник, которые не должны быть нулевыми. В такой проверке, конечно, ничего дурного нет, но иногда это вовсе излишне. Например, если это локальные переменные:
int src;
uint8_t dst[4];
...
memcpy_s(dst, 4, &src, sizeof(src)); // перебор
Никогда в здравом уме вы бы не стали их проверять, тратить на это драгоценные такты процессора!
Вообще, sanity check - дело тонкое, должен исходить из ситуации и здравого смысла, а если у вас есть какой-то универсальный тестер для любых типов данных, то, скорее всего, работает он не очень эффективно. Хорошо, что есть c++, где некоторые проверки можно и вовсе опустить за счет возможностей языка. Например, использовать ссылку вместо указателя. Банально? Однако шанс получить разыменование нулевого указателя внутри функции стремится к нулю.446
is there std::memcpy_s?
Если вы всегда хотели намекнуть коллегам, что их пулл-реквест не очень, но не могли подобрать слов, то просто почитайте обзор экспертов на bounds-checking interface (пресловутое Annex K). Эти изменения никого не оставляли равнодушными. Кто-то к ним отнесся хорошо, как авторы документа N1967 Carlos O'Donell и Martin Sebor. Этот объемный документ написан в 2015 году. Примечательно, что в разделе "Доступные реализации" написано:
Хоть спецификация безопасного интерфейса существует уже более 10 лет, ее реализаций разной степени полноты не так уж и много. Есть две реализации в проектах с открытым исходным кодом, но ни в одном из популярных дистрибутивов, таких как BSD или linux, они не стали доступными для пользователей. А GNU C Library вообще неоднократно отклоняла предложения о включении по причинам, указанным Austin Group в их первоначальном обзоре TR 24731-1 N1106. Маловероятно, что API-интерфейсы будут включены в будущие версии этих дистрибутивов.Забавно, но за четыре года ничего не поменялось, Robert C. Seacord в 2019 году напишет почти слово в слово такой же вывод. Да уж, прочитав отчет этой остинской группы, сомневаешься, что это вообще когда-либо подпустят к обязательной части стандарта. Какие же правильные слова нашли парни из Austin Group, чтоб очернить предложенные Микрософтом изменения? Это настоящая методичка по языку ненависти! Во-первых, группа приветствует столь пристальное внимание к проблемам переполнения буфера. Во-вторых, группа предполагает, что такой подход, передача длины в качестве аргумента в некоторых строковых функциях, может привнести столько же проблем, сколько и решить. Ведь нет никаких гарантий, что программист поставит в качестве аргумента какое-то разумное значение. В итоге это все приведет к замутнению использования изначально четкой и понятной функции. Даже функции, выделяющие память с помощью
malloc(), обеспечивают более безопасные, понятные и надежные интерфейсы.
В-третьих, загрязняется пространство имен.
В общем, многие находят предложенный TR спорным, и остинская группа не выказывает ему решительной поддержки. Приведу в пример наиболее выдающихся мыслителей Остина:
Curtis Smith не думает, что этот TR будет особенно полезным и сомневается во всеобщем одобрении предложения из-за неудобных имен с окончанием _s. Тем более, все, кто хотел проверять границы массивов, уже давно написал свои функции. Еще он рекомендует желающим жонглировать строками выучить и использовать с++, конечно, если их не сильно беспокоит производительность. Хотя, если кого-то беспокоит производительность, тот не станет вызывать функции с избыточными проверками.
Paul Eggert сразу предлагает голосовать против. Предложение спорное и может привести к забаговыванию ПО, да еще и не отражает консенсуса в сообществе.
Еще он вспоминает, как маэстро Торвальдс писал о похожем предложении:
Этот код медленный, уродливый, непонятный, непереносимый и не более безопасный по сравнению с оригинальным. Вкратце, это просто тупой код. Но если вы хотите на виду у всех впрягаться за тупой код, воля ваша. Только, пожалуйста, не надо этим кичиться.Собственно, мейнтейнеры GNU C Library
glibc отвергли похожее предложение по тем же причинам.
Ulrich Drepper, например, писал:
Это ужасно неэффективная чуханина от BSD. Учитывая историю противоречий, я удивлен, обнаружив это предложение опять на рассмотрении.
Неудивительно, что после такой критики "безопасные" функции и близко не подпустили к c++.
Как вы знаете, в плюсах очень рекомендуют включать плюсофицированные заголовочные файлы: не string.h, а cstring. Тогда можно использовать натурализованный memcpy из пространства имен std. Так вот, никаких страшных функций с окончанием _s в том пространстве нет и не будет никогда.446
std::memcpy_s
Как-то в одной экзотической для меня IDE решил скопировать пару массивов, недолго думая, реализовал через
memcpy. Вдруг среда разработки заволновалась и как выдаст предупреждение:
Call to function 'memcpy' is insecure as it does not provide security checks introduced in the C11 standard. Replace with analogous functions that support length arguments or provides boundary checks such as 'memcpy_s' in case of C11Забавно, все беды от небезопасного копирования в
memcpy. Мы и не знали.
Оказывается, еще в 2003 году некто Martyn Lovell изложил некоторые крамольные мысли в докладе WG14/N997 Proposal for Technical Report on C Standard Library Security.
Автор стонал, что стандартные функции появились в незапамятные времена, когда для безопасного программирования достаточно было каски, чтоб выпавший пудовый трансформатор не сбил с мысли. Автор жаловался, что использовать сейчас такой грубый интерфейс основных функций некуртуазно и опасно. Изнеженный домашний программист может легко пораниться. Автор канючит, что надо постепенно и осторожно принять альтернативные версии каждой из опасных функций и объявить оные устаревшими. Разработчик же подобен лягушке: если варить медленно, то далеко не упрыгает.
Впрочем, даже месье Мартин понимал, что простое переключение на эти новые функции само по себе не обеспечит безопасность. Только массовые расстрелы, анализ кода и тщательное тестирование спасет разрабатываемое приложение. Он просто надеется, что новые функции снизят количество тривиальных ошибок кодирования в области безопасности.
В современных реализациях стандартной библиотеки C есть три типа проблем. Две из которых нам совершенно неинтересны. Сосредоточимся на первой, где функции страдают оттого, что им не передают параметры для безопасной реализации. Сюда якобы и попадает наша любимая функция memcpy.
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
Думаю, все знают, что функция делает, однако стандарт не говорит ничего о том, что такое n. Это не размер s1 или s2, это просто количество байт, которые будут скопированы из s2 в s1.
Наверное, это душераздирающе опасно. Так и хочется решить эту проблему, добавив новую функцию с соответствующими параметрами в стандарт C. А чтоб хорошо запоминалось, к стандартному имени добавим окончание _s. Хотя лучше бы добавить _ms. Чтоб сразу было понятно, что ноги у этого документа растут из компании Микрософт. Что породило волну шуток про квалификацию мелкомягких работников, которые не в состоянии проверить размер буфера.
Тем не менее, все эти функции со странными окончаниями таки протащили в стандарт. Теперь в разделе Annex K гордо перечислены все новые функции из bounds-checking interfaces.
Правда, оговорка имеется. В стандарте сказано, что только реализации, которые определяют дефайн __STDC_LIB_EXT1__ должны соответствовать этому дополнению. Если не определяет, то и не обязаны все эти сомнительные функции предоставлять.
Также, чтоб не загрязнять пространство имен, пользователь должен сам объявить __STDC_WANT_LIB_EXT1__ до подключения заголовочных файлов. Правда, некоторые компиляторы плевать хотели на это правило, да, MinGW?
Тем не менее, в некоторых средах можно добраться до стильной, модной функции
errno_t memcpy_s(void * restrict s1, rsize_t s1max, const void * restrict s2, rsize_t n);
Какие проверки берет на себя функция:
- s1 и s2 не должны быть нулевыми указателями, иначе обидится и вернет EINVAL.
- s1max и n не должны превышать RSIZE_MAX.
- n не должно быть больше s1max, иначе вернет ERANGE.
Если проверка провалилась, как асфальт на улице Варварской, то функция запишет s1max нулей в s1, но только если s1 не нулевой указатель и s1max не больше RSIZE_MAX.
Если нарушения нет, то функция вернет ноль.
Даже если не учитывать тот факт, что это все - порождение корпорации зла, все равно полезность новых функций в стандарте вызовет споры с элементами поножовщины и в небольшом дружном сообществе. Собственно, реализацию нового интерфейса можно обнаружить только в компиляторах для Windows. Почему? A suivre.446
std::auriga
От неожиданных звонков никогда не жди ничего хорошего. Хуже только звонки с незнакомых номеров, которые я уже давно игнорирую. Цукербрин так и не позвонил, а посвящать разного рода мошенников в тонкости работы афедронного коллайдера или обсуждать явные признаки каллигатической остеополлюции всех их родственников до седьмого колена терпенья уже не хватало. Однако сбросить этот звонок я не смог: весело тренькал тимс, приглашая присоединиться к внезапному рабочему собранию.
Вызов принят.
- Парни, мне очень жаль... - начал наш менеджер со стороны заказчика.
Тут сердце мое упало. Когда менеджер заводит разговор с командой таким образом, это тревожный звоночек. Тем не менее, скорбные лица коллег ухитрялись выражать облегчение. Между собой мы уже пару лет мрачно шутили на тему неизбежного увольнения. Геополитика.
- ... я ничего не мог поделать, все было решено там, - менеджер ткнул пальцем куда-то вверх, а по его щеке неожиданно покатилась горькая скупая слеза. Нам было его жаль, мировой мужик, теперь обречен вытягивать неподъемный проект в окружении сомнительных разработчиков из восточной Европы. На его месте мы бы тоже зарыдали.
Видел его таким же несчастным лишь однажды, когда, будучи в командировке, мы пошли в столовую, и я налил себе пару ложек супа в огромную миску. Кто же знал, что у них там порция оценивается по объему тары? Ему, на правах хозяина, пришлось изрядно раскошелиться.
Впрочем, никакие сентиментальные сцены не смогли смягчить удар об стену оборудования заказчика после окончательной блокировки моего рабочего аккаунта. Я отдал проекту пять лет, за которые успел поседеть от созерцания их кода, а потом какой-то Ганс решает, что все и без нас заработает... Удачи, попутного ветра в горбатую спину.
Я же отправился навстречу черной меланхолии.
Знаю, многих интересовал вопрос, где Аурига, куда пропала? Слухи ходили разной степени упорности.
Только сплетни о смерти были сильно преувеличены. Компания и сама бы могла развеять туман неопределенности, однако, судя по изменениям на официальном сайте, она была всецело охвачена модным ныне явлением ребрендинга. Сами можете оценить результаты, лично я бы для колорита добавил больше матрешек с балалайками верхом на медведях. Четко обозначить намеченный вектор развития.
Те сотрудники, кто посещает офис ради неповторимой рабочей атмосферы, сочных фруктов или массажа языка возле кулера, свидетельствуют, что компания дышит, народ в столовую ходит, эйчары арканами перспективных кандидатов ловят. Вот для меня Аурига чуть было не закончилась, так сильны были негодование и досада. Перспектива снова работать на проклятых буржуинов меня нисколько не прельщала. Подумывал даже заняться написанием детских стихов, однако мои вирши про истекающую соком Луну не приняли ни в одном издательстве, заинтересовались только в местном лечебном заведении закрытого типа, куда рукопись попала по ошибке. К счастью, не пришлось даже кодить в переходе за донаты, ведь внезапно очень многим отечественным компаниям просто как воздух стали необходимы грамотные специалисты. Те, кто спер у компаний с мировым уровнем не только разбитый ноутбук, но и бесценный опыт разработки качественных продуктов.
446
std::ssize
С тех пор как проект закрылся, я живу под мостом и единственный вопрос, который меня ныне интересует: как определить размер контейнера? Можно использовать рулетку либо длинную палку. Если дело совсем плохо, то сделать замеры можно и ногой. Однако лучше вспомнить про функцию
std::size, которая сама определит, что за контейнер пытаются ей скормить: продвинутый плюсовой со встроенным методом size(), либо рабоче-крестьянский сишный массив. Демиурги стандартной библиотеки добавили эту функцию в стандарте с++17, дабы умилостивить бога обобщенного программирования и облегчить работу ленивым разработчикам. Реализация для контейнеров предельно простая:
template <typename Container>
constexpr auto size(Container const &cont) -> decltype(cont.size())
{ return cont.size(); }
Для массивов есть своя специализация функции:
template <typename Tp, size_t Nm>
constexpr size_t size(const Tp (&)[Nm])
{ return Nm; }
Четко, понятно, доходчиво. Казалось бы, что тут можно улучшить? Однако в с++20 появляется новая функция std::ssize. Зачем понадобился еще один метод, делающий все то же самое?
Олдфагам может показаться имя ssize ностальгически знакомым. Не показалось, действительно, есть такой тип ssize_t, назначением которого было возвращать размер чего-нибудь либо отрицательное значение в качестве кода ошибки. Этакий optional на минималках. Собственно, и для функции std::ssize вся разница заключается в типе возвращаемого значения. Разница незначительная на первый взгляд, но тут кроется фундаментальный сдвиг в восприятии размеров и индексов.
Стандартная библиотека смело заявила, что отрицательные индексы и размеры применительно к контейнерам не имеют смысла. Например, есть у нас некоторый std::array<X, 10U>, размер его всегда строго положительная величина: constexpr size_type size() const, как и индексы в операторе reference operator[](size_type __n).
Изначально же отрицательный индекс массива не был лишен смысла, например, если есть указатель на середину массива ptr, то ptr[-1] значит лишь смещение относительно текущего положения *(ptr - 1).
Тут назревает конфликт стандартной библиотеки и адресной арифметики. Чаще всего размерные типы беззнаковые, а типам индексов хотелось бы быть знаковыми. Почти всегда размер используется для каких-нибудь махинаций с индексами. Тогда такое смешение знаковых и беззнаковых типов - бескрайнее поле для посева ошибок.
В предложении p1227R1 описывается такой пример:
template <typename T>
bool has_repeated_values(const T& container) {
for (int i = 0; i < container.size() - 1; ++i) {
if (container[i] == container[i + 1]) return true;
}
return false;
}
Отстреливший не одну ногу разработчик сразу найдет выражение container.size()-1 опасным. Поскольку результат size() беззнаковый, то при нулевом размере size()-1 легким движением руки превращается в огромное положительное число. Последствия могут быть воистину катастрофическими. Это как если бы Ромео был из семьи uint32_t , а Джульетта - из int32_t. В результате все умерли, чума на оба ваших дома! Зато ssize вернет результат size(), приведенный к знаковому типу или чему-то вроде std::ptrdiff_t, поэтому ничего страшного не случится.446
std::inout_ptr_t
Наконец-то жуткая холодная весна заканчивается, и лето уже бесстыдно заглядывает нам в окна. Нарциссы совсем распустились. И цветы тоже не отстают.
Вот иду я по измокнувшей в солнце улице, и гложет меня нехорошее чувство. Забыл совсем рассказать, что в заголовочном файле
<memory> новых классов появилось несколько. Про std::out_ptr_t мы уже все знаем, но в лучших традициях индийского кино, у этого адаптера есть еще и младший брат: std::inout_ptr_t.
Ясное дело, отличие не только в названии, хотя они и похожи, как ухо и второе ухо. Если посмотреть внимательней, станет понятно, что этот бедный родственник живет за счет out_ptr_t.
template<typename _Smart, typename _Pointer, typename... _Args>
class inout_ptr_t {
using _Out_ptr_t = out_ptr_t<_Smart, _Pointer, _Args...>;
using _Impl_t = typename _Out_ptr_t::_Impl_t;
_Impl_t _M_impl;
Зачем же понадобился нам этот сородич?
Мы знаем, что out_ptr_t используется исключительно для добычи указателя из функции. Не об этом ли говорит имя типа? В doxygen-комментариях есть что-то похожее, в описании аргумента функции можно было добавить "направление параметра": @param[in], @param[out] и @param[in,out].
С направлением out все понятно, указатель передается в функцию, чтоб туда значение было записано.
/**
* @param[out] p_param
*/
void makeSense(int * p_param) {
*p_param = 42;
}
Направление in говорит о том, что значение читается и как-то используется в функции. Комбинация in,out намекает, что значение по указателю не только используется, но и на его место записывается некий результат.
Так же и inout_ptr_t дает нам возможность использовать первоначальное значение указателя, он не сбрасывается как в случае out_ptr_t.
Возьмем пример прямо из стандарта, благо имена там красивые. Допустим, есть некоторая функция, которая возвращает указатель на выделенную память под объект типа star_fish.
struct star_fish* star_fish_alloc();
И есть другая функция, которая должна освободить память по переданному указателю и записать туда новое значение на свежевыделенный участок.
int star_fish_populate(struct star_fish** ps, const char* description);
Небольшая структура обеспечивает окончательное освобождение многострадальной памяти.
struct star_fish_deleter {
void operator() (struct star_fish* c) const noexcept;
};
std::unique_ptr<star_fish, star_fish_deleter> peach(star_fish_alloc());
// здесь мы используем объект, а потом пересоздаем его
star_fish_populate(std::inout_ptr(peach), "caring clown-fish liker");
Если развернуть необычные конструкции, то мы выполняем следующие шаги:
int* peach_raw = peach.release();
star_fish_populate(&peach_raw, "caring clown-fish liker");
peach.reset(peach_raw);
При инициализации inout_ptr_t вызывается функция release(). При этом текущее значение указателя сохраняется. Инициализация в исходниках выглядит как _M_ptr = _M_smart.release();
Затем это же значение используется в функциях преобразования operator Pointer*() const и operator void**() const. Таким образом, внутри функции star_fish_populate можно получить старое значение указателя, чтоб освободить его.
И уже традиционно, не рекомендуется использовать inout_ptr_t в чистом виде, только через функцию std::inout_ptr. "Оно вкусно и на цвет красиво!"446
inside of std::out_ptr_t
Теперь, когда широкой общественности стало известно о существовании
out_ptr_t, некоторые разработчики не могут спокойно пить смузи и есть фалафель, измазанный в хумусе. Спать им и так некогда, надо работать! Но прежде всего, надо хотя бы одним глазком заглянуть в нутро out_ptr_t.
Стандарт в разделе 20.3.4.1 Class template out_ptr_t [out.ptr.t] дает приблизительное описание этого класса:
template<class Smart, class Pointer, class... Args>
class out_ptr_t {
public:
explicit out_ptr_t(Smart&, Args...);
out_ptr_t(const out_ptr_t&) = delete;
~out_ptr_t();
operator Pointer*() const noexcept;
operator void**() const noexcept;
private:
Smart& s; // exposition only
tuple<Args...> a; // exposition only
Pointer p; // exposition only
};
В конструктор объекта такого типа передается ссылка на указатель, возможно, даже не самый глупый, который будет храниться в s.
Далее угадывается настолько классическая схема работы с хранением параметров, что хочется воскликнуть: "Граждане! Храните отложенные аргументы в tuple. Если, конечно, они у вас есть". Последний член класса p получает почетный nullptr в качестве начального значения. Допустим, Smart - это все же тип умного указателя (хотя out_ptr_t работает и с другими типами), тогда у него точно есть метод reset(), и он будет вызван немедленно.
У класса есть два метода приведения типов, которые могут быть использованы для доставки указателя внутрь out_ptr_t.
std::out_ptr_t<Smart,Pointer,Args...>::operator Pointer*
std::out_ptr_t<Smart,Pointer,Args...>::operator void**
Делают они примерно одно и то же, выдают адрес указателя p: addressof(const_cast<Pointer&>(p)).
В деструкторе мы наконец-то помещаем значение указателя в некую умную субстанцию s тем или иным способом, например:
if (p) {
apply([&](auto&&... args) { s.reset(p, std::forward<Args>(args)...); }, std::move(a));
}
apply нам нужен только для того, чтоб распотрошить наш tuple c сохраненными аргументами и передать их методу reset как следует.
Теперь давайте проверим, действительно ли создавать std::out_ptr_t - это плохая идея?
Создадим специально для наших стыдных опытов не очень умный указатель:
template <class T>
struct stupid_ptr {
void reset(T *t = nullptr) { ptr = t; }
T* ptr = nullptr;
};
std::out_ptr_t очень терпим и не откажется работать даже с таким убогим классом.
stupid_ptr<hackrf_device> p {};
std::out_ptr_t<std::stupid_ptr<hackrf_device>, hackrf_device*> g {p};
hackrf_open(g); // p.ptr == nullptr
g.~out_ptr_t<std::stupid_ptr<hackrf_device>, hackrf_device*>(); // p.ptr != nullptr
Работает ожидаемо, как и описано на cppreference. Указатель, что выдает нам hackrf_open, хранится в некой временной переменной, и только в деструкторе телепортируется в масло.
Однако, если мы обратимся к стандартным типам, то и поведение, и даже размер объекта изменится.
std::unique_ptr<hackrf_device, decltype(close_hackrf)> p {};
std::out_ptr_t<std::unique_ptr<hackrf_device, decltype(close_hackrf)>, hackrf_device*> g {p};
hackrf_open(g); // p.get() != nullptr
g.~out_ptr_t<std::unique_ptr<hackrf_device, decltype(close_hackrf)>, hackrf_device*>();
Как оказалось, здесь std::out_ptr_t не ждет вызова деструктора, а сразу помещает указатель в unique_ptr. Если сравнить размер out_ptr_t<unique_ptr> с out_ptr_t<stupid_ptr>, то первый будет в два раза меньше. Из этого можно сделать вывод, что вспомогательный член класса Pointer p отсутствует. Если же мы заглянем в исходники, то найдем подтверждение нашей догадке и ужаснемся: этот тип вовсе не так прост, как кажется. У него богатый внутренний мир, полный частичных специализаций для разных типов указателей.
Вывод уже напрашивается сам собой: тарелку после гречки лучше мыть сразу, ночью надо спать, а функцию std::out_ptr - обязательно использовать.446
std::out_ptr_t
Однажды на каком-нибудь интервью Главный Разработчик в конторе Horns-n-Hooves Ltd. спросит вас: "А какие умные указатели вы знаете?". Не упустите момент и смело вывалите ему на очки не только скучный
unique_ptr и слабосильный shared_ptr, но и сопутствующие шаблонные классы-адаптеры. Если и есть что-то хорошее в нововведениях с++23, то это тип std::out_ptr_t.
Он был предложен жарким летом 2018 года в документе P1132R0 некими JeanHeyd Meneide, Todor Buyukliev и Isabella Muerte. Авторы сего опуса неплохо начали, написав, что out_ptr_t - это вроде как Моисей, который приведет сишный API и умные указатели в землю обетованную. Не слишком научно, но мне как-то спокойнее, если предложения для плюсов пишут религиозные люди. Ибо они, убоявшись геенны огненной, не станут предлагать использовать венгерскую нотацию или какой-нибудь garbage collector.
В общем, этот вспомогательный класс пригодится тем, кому просто необходимо использовать сишные библиотеки в работе. Очень часто там приходится начинать с вызова функции, которая создает некую точку входа, условный дескриптор для управляемого объекта. Могу привести в пример библиотеку hackrf для SDR.
Заурядное начало работы выглядит так:
hackrf_init();
hackrf_device *device = NULL;
hackrf_open(&device);
Последняя функция выдает нам правильный указатель на управляемый объект.
Вот затрапезная концовка работы:
hackrf_close(device); hackrf_exit();Чистый и незамутненный си. Какая гадость! Нужно срочно переписать это на божественном с++, да так, чтоб было сплошное RAII. Никто не запрещает использовать красивый и умный указатель для управления ресурсами уже сейчас.
auto delete_hackrf = [](hackrf_device* device){ hackrf_close(device); };
std::unique_ptr<hackrf_device, decltype(delete_hackrf)> dev {};
hackrf_device *device {};
hackrf_open(&device);
dev.reset(device);
Выглядит это, откровенно говоря, не очень привлекательно. Слишком много танцев вокруг функции инициализации приходится исполнять, так можно и ногу потерять в процессе.
Как же заставить сишный API работать с умным указателем более изящно? Здесь на помощь к нам бежит специальный класс-адаптер out_ptr_t.
std::unique_ptr<hackrf_device, decltype(delete_hackrf)> dev {};
hackrf_open(std::out_ptr(dev));
или то же самое для shared_ptr:
std::shared_ptr<hackrf_device> sh_dev {};
hackrf_open(std::out_ptr(sh_dev, delete_hackrf));
Сколько грации в этой паре строчек! Класс берет на себя правильную инициализацию умного указателя внутри сишной функции, а функция std::out_ptr берет на себя создание правильного объекта std::out_ptr_t, т.е. выведение аргументов шаблона. Ведь полное описание шаблона выглядит как:
template <class Smart, class Pointer, class... Args> class out_ptr_t;
Вручную такого монстра уж точно не захочется создавать. Впрочем, как раз это и не рекомендуют делать, и я догадываюсь почему...