C++ Embedded
Відкрити в Telegram
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Показати більше446
Підписники
Немає даних24 години
+27 днів
+430 день
Архів дописів
446
std::put_money
Бывает, что на интервью спрашивают: какая ошибка вам больше всего запомнилась? Не знаю, на что рассчитывают. Никто не любит грустные истории, которые становятся еще печальнее из-за обилия технических подробностей.
Если уж рассказать историю epic fail, то лучше праздничную. Как-то я согласился на собеседование седьмого ноября. Этот памятный день эта уже давно перестал быть выходным, поэтому на дату я особо не обратил внимания. Тем более, что для меня все праздники затмил седьмой день рождения дочери, к которому пришлось основательно готовиться и перенадувать целую кучу шаров. С тех пор по квартире дрейфовала большая полная гелия цифра 7. Вот наступает час Х, собеседование начинается, я включаю камеру. Тут позади меня в кадр вальяжно вплывает огромная розовая семерка.
Собеседники во все глаза смотрят на этот перформанс и подавленно молчат.
Потом самый сообразительный нашелся:
- С праздником вас!
- Ага, спасибо, - пытаясь вытолкать незваную гостью из кадра, я посмотрел на него с подозрением, не понимая какой праздник имеется в виду: день рождения? День народного единства?
Короче, в ту контору меня не взяли. Сказали, что только старый прожженный коммунист будет в годовщину Великой Октябрьской социалистической революции надувать гигантскую семерку и тыкать ейной мордой прямо в камеру. Проклятые капиталисты увидели в этом рэд флаг. Хотя, на самом деле, я ничего такого не имел в виду.
Есть, конечно, и скучная история при плюсы. Как-то раз я приложение отлаживал несколько часов кряду.
У меня не сходилась величина таймаута, которая считывалась из конфигурации:
std::chrono::seconds timeout {load_timeout_value()};
Для проверки, недолго думая, я написал:
std::cout << "timeout " << timeout << std::endl;
Значение должно быть 100 секунд, а вывод был такой:
timeout 64sКоррупция памяти? Я бросился сразу прогонять приложение в valgrind, но анализатор молчал. Отладчик не показывал ничего криминального. Тут у меня зародилось смутное сомнение. Поднимаю я взгляд на несколько строчек выше и вижу:
std::cout << "ID " << std::hex << id << std::endl;
Какой-то добрый человек, чтоб проще было смотреть ID, добавил манипулятор вывода hex. Только вот применяется он на поток, и действие его длится, пока систему счисления не вернет обратно std::dec. Самое плохое, что даже значения типа std::chrono::seconds будут молча выведены в другой системе счисления.
Тут бы не помешал еще манипулятор std::showbase, который явно показывает, в какой системе счисления выводится число:
timeout 0x64sУжасно выглядит, зато сразу понятно, что-то не то пошло не так. Отменить действие можно через
std::noshowbase.
Коварны также манипуляции с локалью.
Есть, например, забавная функция std::put_money. Обычно разработчики избегают ее использовать, думают, что сейчас придется раскошелиться.
На самом деле этот манипулятор преобразует денежное значение в его символьное представление.
template<class MoneyT>
/*unspecified*/ put_money(const MoneyT& mon, bool intl = false);
где mon - это и есть монетарное значение, в самых мелких монетках, судя по всему. (Использовать ли специальный валютный символ определяет intl).
long double price = 100.5;
std::cout.imbue(std::locale("ru_RU.UTF-8"));
std::cout << std::put_money(price) << '\n'; // 1,00 ₽
Если mon типа double и значением 100.5, это всего лишь один рубль.
А в другой локали это внезапно один богомерзкий доллар.
std::cout.imbue(std::locale("en_US.UTF-8"));
std::cout << << std::put_money(price) << '\n'; // $1.00
Функция std::cout.imbue устанавливает нужную локаль, если не находит в системе, то бросает исключение.
Есть не только денежные манипуляторы, еще имеются временные, просто числовые, но за этим добром нужен глаз. Лучше два.446
no-abbreviated-auto-in-template-arg for Frank
Старина Frank Heckenbach звезд с неба не хватал, но был педант. Любил порядок и собирал свои приложения с
-pedantic, а в приступах дерзости и с -pedantic-errors. Всегда пользовался версией компилятора, предоставленной его дистрибутивом линукса по умолчанию. Всяких расширений компилятора на дух не переносил. Все должно быть по стандарту для переносимости, хотя, как и всякий хороший разработчик, стандарт не читал.
В одно прекрасное июльское утро Фрэнк обновил дистрибутив, запустил сборку своего любимого приложения и пошел за кофе.
"Проблем даже на свежем gcc14 быть не должно, - рассуждал он, - ведь мой код написан очень аккуратно".
К вящему ужасу Фрэнка, по возвращении его ожидала бесконечная простыня одинаковых предупреждений:
warning: use of 'auto' in template argument only available with '-fconcepts-ts'Утро тут же перестало быть прекрасным, а кофе вкусным. Фрэнк забегал по волосам и рвал на себе комнату: "Немыслимо! Я годами использовал трюк c
auto в качестве шаблонного аргумента в параметрах сокращенной функции, а теперь они вот как со мной!"
Фрэнк быстро взял себя в руки, а руками завел баг c++/120917.
Там бедолага жалуется, что, имея страстное желание писать чистый и переносимый код, он всячески избегал диалектов, расширений компиляторов и иных перверсий. Только единожды соблазнился конструкцией void f(S<auto>); и понатыкал подобного в коде в великом множестве.
"Поскольку никаких предупреждений не было, - рыдал он, - я думал, что это часть стандартных концептов. А теперь вы мне говорите, что эта возможность исчезнет? По сути, GCC коварно заманил меня в ловушку, а теперь она захлопнулась, да? Немедленно верните фичу обратно!"
Вернули ему от мертвого осла уши. Заодно сказали, что сам дурак и надо было читать документацию.
Использование параметра сборки -std=c++NN никогда не отключало все расширения. И вообще, надо было собирать с -pedantic, а еще лучше с -pedantic-errors.
Тут GCC со всего разбега сел в лужу, т.к. на самом деле сборка с -std=c++17 и флагом -pedantic дает предупреждение, а вот с -std=c++20 уже нет. Нехотя пришлось признать, что это могло ввести рядового пользователя в заблуждение. Ну не каждый разработчик осилит стандарт хотя бы до середины, там пишут, пишут... конгресс, немцы какие-то... голова пухнет!
Ясность внес Jason Merrill, который признался, что они хотели бы диагностировать все фичи из Concepts TS, что не вошли в стандарт, но не осилили. Сейчас он исправил это досадное недоразумение, а в качестве извинения сделал Френку флажок -Wno-abbreviated-auto-in-template-arg, который включает возможность использовать auto в шаблонных аргументах сокращенных функций по-тихому, как встарь. Только эта возможность станет доступна в GCC 16.
Пока самым правильным способом исправить ошибку будет замена void f(S<auto>) на
template <class T>
void f(S<T>)
Бедняга Фрэнк, завидев близкие перспективы муторного рефакторинга, сразу скис. Он бы хотел пройтись sed-ом по коду и просто заменить S<auto> на что-то более приемлемое. Может быть, вот так:
template <typename T> concept True = true;
template <typename T> struct S;
auto f (S <True auto>);
Спасибо, говорят, Фрэнк! Ты нашел еще один неправильный вариант использования auto, который почему-то еще работает, хотя не должен.
Однако в libstdc++ кое-то использует интересный концепт для проверки принадлежности аргументов к шаблонным типам:
template<typename _Tp>
concept __is_derived_from_optional = requires (const _Tp& __t) {
[]<typename _Up>(const optional<_Up>&){ }(__t);
};
Фрэнк был не дурак и намек понял, на скорую руку сварганил универсальный концепт для своего кода:
template <typename T, template <typename ...> typename P>
concept DerivedFromTemplate = requires (T v) {
[] <typename ... U> (const P <U ...> &){ } (v);
};
Тогда можно успешно и по стандарту заменить void f(S<auto>); на void f(DerivedFromTemplate<S> auto);
Еще лучше изначально не писать дичь, не придется страдать, как Фрэнк.446
abbreviated auto in template argument
- Ага, а вот и ошибка! - с чувством глубокого удовлетворения я откинулся в кресле. Находить недочеты не в своем коде всегда приятно.
- Что там? Тест упал? - за плечом мгновенно материализовался коллега и вперился в мой монитор.
- Нет. Все гораздо хуже... - тяжелый вздох.
Взгляд сослуживца забегал по строчкам кода.
- Да где? Вроде все правильно написано... сложно ошибиться в слове
auto.
На экране был выделен фрагмент примерно такого содержания:
void process(std::optional<auto> const &x) {
if (x.has_value()) { handle(x.value()) }
}
Хоть на животном уровне и понятно, что имел в виду автор, но это же кошмар!
В C++20 появились сокращенные шаблонные функции (Abbreviated function template), чтоб бедный разработчик не спотыкался о template<typename> перед определением функции. Стало можно использовать заполнитель auto в качестве аргумента: void f1(auto) или void f2(Concept auto). Однако в стандарте не предусмотрено использование заполнителя как типозаменителя в аргументе шаблона.
Для настоящего, но утонченного индейца этот auto должен так же резать глаз, как выхлоп станции аэрации на Гребном канале. Впрочем, у нас тут не мемопровод, а серьезный канал. Позволю себе пару-тройку строк посвятить маленькой исторической справке. Вы же не против?
Все началось много лет назад, году эдак в 2015, тогда разработчики активно грезили концептами, ждали их, словно пришествия Мессии. Когда же появился документ ISO/IEC technical specification "C++ Extensions for Concepts", некоторые компиляторы расценили его как руководство к действию, и GCC рвануло впереди паровоза бешено имплементировать новые вкусные возможности. Где-то с восьмой версии GCC можно было собрать код с std::optional<auto> в качестве аргумента, но компилятор хотя бы предупреждал:
warning: use of 'auto' in parameter declaration only available with -fconceptsИменно Concepts TS провозглашал опрощение синтаксиса до такой степени, что
auto можно использовать в качестве типозаменителя в шаблонных аргументах.
А вот с GCC 10 предупреждение исчезает бесследно для c++20! Сборка на GCC 13.2 которая используется у нас, к примеру, молчит как рыба.
Только к GCC 14 разработчики признались, что эта фишка не из нормальных концептов. Вновь появляется предупреждение:
warning: use of 'auto' in template argument only available with '-fconcepts-ts'При том, что использование
concepts-ts объявлено устаревшей практикой:
note: '-fconcepts-ts' is deprecated and will be removed in GCC 15; please convert your code to C++20 conceptsВ самом деле, этот пример в GCC 15 уже не собирается. Именно это обстоятельство меня крайне беспокоит. - У кого-то слишком много свободного времени, что странно. Релиз на носу, - коллега посмотрел на меня в упор. - Тогда послушай, что стало с бедолагой Фрэнком... A suivre.
446
std::start_lifetime_as
Каждый, кто прошел суровую школу Си перед тем, как обрести подлинное счастье в С++, испытывал искушение начать штамповать объекты по старинке:
struct X { int a, b; };
X *make_x() {
X *p = (X*)malloc(sizeof(struct X));
p->a = 1;
p->b = 2;
return p;
}
Или воскрешать объекты в духе старой школы прямо на байтовом потоке:
void process(Stream *stream) {
unique_ptr<char[]> buffer = stream->read();
if (buffer[0] == FOO)
process_foo(reinterpret_cast<Foo*>(buffer.get()));
else
process_bar(reinterpret_cast<Bar*>(buffer.get()));
}
Хоть методы вполне рабочие, но применять их нельзя, это явное UB. Стандарт четко разграничивает акт сотворения объектов от яичницы в 6.6.2 Object model [intro.object]:
Объект может быть создан через определение (ну как обычно), через оператор new, путем неявного выбора активного элемента объединения, при создании временного объекта.Никакие
malloc или преобразования буфера не дадут такого эффекта. Поэтому чисто формально, в обоих примерах объект не создан, и нельзя использовать получившийся объектоподобный кадавр.
Наконец, Ville Voutilainen в 2017 году публикует крик души "What to do with buffers that are not arrays, and undefined behavior thereof?", а Richard Smith в 2020 году доводит этот вопль до финального предложения P0593R6, где предлагается ввести некую стандартную и магическую функцию std::bless, разрешившую неопределенность в подобных случаях. Как и std::launder никаких реальных действий она не исполняет, это своего рода барьер, возможность сказать компилятору, что сейчас в этом месте будет свершен акт творения нового объекта.
Предложение было принято с некоторыми оговорками. В результате уже в c++23 первый параграф 6.6.2 Object model [intro.object] изменили, добавив, что объект может быть создан так же неявно (implicitly creates objects).
Например, вызов std::malloc теперь неявно создает объект: X *p = (X*)std::malloc(sizeof(struct X));
Благословлены на неявное сотворение оказались bit_cast с аллокаторами и массивами и даже хранилище new std::byte buffer[N];
Еще одним камнем преткновения стало имя магической функции. Хоть в C++ и виднеются кресты, но не все разработчики - благочестивые христиане. Кое-кого возмутило имя bless. Они нашли эту концепцию оскорбительной, выразив надежду, что в комитете не засели какие-нибудь набожные... э-э-э... кретины (fuckturds!). Им посоветовали успокоиться, ведь предложение было бы более конструктивным, если бы они избегали обсценной лексики и предложили бы более разумную альтернативу, вроде std::endow, std::favor или хотя бы std::preserve.
Однако комитет всех переиграл и назвал праведную функцию start_lifetime_as.
Нетипизированную версию функцию void std::bless(void *start, size_t length) для благословения массивов вообще забраковали, ведь тот же эффект дает использование new (start) std::byte[length].
Теперь первый пример с malloc полностью невиновен и не является UB.
Во втором нужно использовать std::start_lifetime_as
void process(Stream* stream) {
std::unique_ptr<char[]> buffer = stream->read();
if (buffer[0] == FOO)
processFoo(std::start_lifetime_as<Foo>(buffer.get())); // OK
else
processBar(std::start_lifetime_as<Bar>(buffer.get())); // OK
}
Что же, теперь все игры с сериализацией объектов, что раньше лишь умозрительно казались верными, теперь еще и правильные по стандарту.446
inplace_vector. select_size_type
Третьего дня в деревне был праздник, поставили новые счетчики. Дабы на корню пресечь возможные лиходейства, смонтировали их прямо на столбах. Ни один киловатт-час не пройдет неучтенным! Я немного беспокоился, что придется карабкаться на самый верх, чтоб показания снять, но всем выдали приспособу с сегментным индикатором. Штуковина эта по радиоканалу данные считывала и мне показывала. Долго показывала, полтора дня. Потом показания выросли резким скачком и сравнялись с награбленным состоянием Чубайса.
Сегодня смотрю, никак сам разработчик на столб лезет. Важный, с ноутбуком и программатором. В наказание, видать, за несоблюдение MISRA. Докарабкался, весь аж затрясся от радости, багу нашел, небось. Пусть перешивает, как жить без счетчика?
А знаете, кому еще нужен счетчик? Конечно же
inplace_vector не может обойтись только хранилищем, т.е. массивом, нужен еще хранитель настоящего размера вектора.
template<typename _Tp, size_t _Nm>
class inplace_vector {
...
private:
union {
_Tp _M_elems[_Nm];
};
decltype(__select_size_type()) _M_size = 0;
...
};
где _M_size - это и есть счетчик элементов. Тип его, странный, конечно, но всегда нужно помнить, что разработчики стандартной библиотеки - люди прижимистые, они спать не смогут, если байт не сэкономят. Ну правда, если в векторе хранить три элемента, то непростительно отдавать под счетчик 4 байта. Одного за глаза хватит. Поэтому тип лучше всего вычислять при конструировании inplace_vector, в зависимости от величины _Nm.
Функция времени компиляции __select_size_type() устроена довольна просто
static consteval auto __select_size_type() {
if constexpr (__fits<unsigned char>)
return (unsigned char)0;
else if constexpr (__fits<unsigned short>)
return (unsigned short)0;
else if constexpr (__fits<unsigned int>)
return 0u;
else if constexpr (__fits<unsigned long>)
return 0ul;
else
return 0uz;
}
Мы просто перебираем беззнаковые типы, начиная с самого узкого, и возвращаем ноль подходящего типа.
Пригодность типа определяет еще одна метафункция __fits,
template<typename _UInt, bool = (alignof(_Tp) <= sizeof(_UInt))>
static constexpr bool __fits = _Nm <= __gnu_cxx::__int_traits<_UInt>::__max;
По форме это просто константа, показывающая достаточно ли широк тип _UInt, чтоб вместить _Nm, что легко определить через __gnu_cxx::__int_traits<_UInt>::__max.
Условие в шаблоне bool = (alignof(_Tp) <= sizeof(_UInt)) говорит нам о том, что эта оптимизация имеет смысл, если только выравнивание целевого типа меньше или равно выравниванию предполагаемого типа счетчика. Например, если тип элементов char, то можно смело брать в качестве счетчика переменную типа uin16_t. Если же тип элементов вектора uint32_t, то нет особого смысла делать счетчик из uint8_t. Весь выигрыш сожрет заполнение для выравнивания. Поэтому:
template<typename _UInt>
static constexpr bool __fits<_UInt, false> = false;
мы просто говорим, мол, тип не подходит. Рано или поздно мы найдем достаточно толстый тип.
В общем, экономия на спичках удалась, можно спать спокойно.446
if consteval
Осень в городе. От холода уже отнимаются ноги, но я стою, не могу оторвать взгляд от маленького дуба, желтые листочки которого озорно осыпаются, словно тикеты в конце спринта. Еще чуть, и ветви его станут похожи на бренчи нашего проекта - хилые и убогие. От этой мысли я вздрогнул и заставил себя смотреть в реализацию
inplace_vector. Впрочем, и там обнаружилось завораживающее ветвление:
constexpr void _M_init() {
if !consteval {
#if __glibcxx_start_lifetime_as
std::start_lifetime_as_array<_Tp>(data(), _Nm);
#endif
} else { ... }
}
Ловко! Уже из названия понятно и ежу, что теперь мы сможем точно узнать, выполняется ли этот код во время компиляции.
Позвольте, но мы и раньше могли, просто не хотели. У нас же есть std::is_constant_evaluated!
Действительно, магический метод доступен в с++20, в ранних своих реализациях в GCC опирающийся на встроенную функцию:
constexpr bool is_constant_evaluated() noexcept {
return __builtin_is_constant_evaluated();
}
__builtin_is_constant_evaluated - волшебная функция GCC, придуманная исключительно для плюсов. Ведь тут есть двуликие constexpr-функции, которые могут вызываться как в compile-time, так и в runtime. Сама функция возвращает true только в контексте времени компиляции.
Какая же нужда выгнала нас на дорогу к if consteval?
Началось все в марте 2021 года с невинного на первый взгляд предложения P1938R3 от Barry Revzin, Richard Smith, Andrew Sutton, Daveed Vandevoorde.
Жизнь стала лучше, пишут они, в с++20 появилось много новых штучек для любителей повычислять что-то во время компиляции, в частности consteval и is_constant_evaluated
Однако во взаимодействиях constexpr и consteval есть определенные проблемы.
consteval int f(int i) { return i; }
Нормально, f - функция, созданная для compile-time.
constexpr int g(int i) {
if (std::is_constant_evaluated()) {
return f(i) + 1; // Error
} else {
return 42;
}
}
А вот тут уже хуже, такой код не соберется, даже если вы не планировали вызывать g в runtime:
error: 'i' is not a constant expressionСущая правда, в этом контексте i действительно не обладает константностью, но мы все равно смутно ощущаем неправильность происходящего. Еще ни в коем случае не стоит писать
if constexpr (std::is_constant_evaluated()), результат будет немного предсказуем - true, независимо от контекста.
Это настолько очевидный источник ошибок, что бедный Барри немедля отправил сообщения об этом и в gcc, и в clang, чтобы побудить компиляторы предупреждать о таком неправильном использовании.
В общем, is_constant_evaluated работает, но недостаточно хорошо.
Поэтому ребятки предлагают новую форму проверки:
if consteval {}
Ведет себя так же, как и if (std::is_constant_evaluated()) {}
но заголовок type_traits не требуется, отличный синтаксис полностью устраняет путаницу, не дает проверять контекст выполнения неправильно. Внутри блока разрешено использовать immediate-функции. Ведь conseval-функция на самом деле это immediate function, и могут они вызываться только в соответствующем immediate function контексте. Вот if (std::is_constant_evaluated()) такой контекст не создает, поэтому и выходит ошибка. Расширив правильный контекст на if consteval, мы наконец-то осилим злосчастный пример:
constexpr int g(int i) {
if consteval {
return f(i) + 1; // ok: immediate function context
} else {
return 42;
}
}
Еще и is_constant_evaluated можно переписать:
constexpr bool is_constant_evaluated() noexcept {
if consteval { return true; } else { return false; }
}
Ловкость рук, никакого мошенничества.446
std::construct_at
Сабж стремительно залетает в разработку вместе с с++20. Эта функция должна была служить лишь одной цели - создавать объекты на выделенном участке памяти в разных построениях времени компиляции.
Идея витала в воздухе уже давно и в 2017 году материализовалась в виде предложения P0784R0 (
Standard containers and constexpr), где Louis Dionne, Richard Smith, Daveed Vandevoorde высказали смелую мысль о возможности использования разных контейнеров вроде std::vector или std::unordered_map в constexpr-функциях. На пути к этому было несколько препятствий: отсутствие constexpr-деструктора, невозможность выделять или освобождать память на этапе компиляции, и, самое главное, недоступность placement new. Многие, если не все, стандартные контейнеры лишь исполняют приседания и пляски вокруг размещения нового объекта в заранее выделенной области памяти.
Эволюционировав до P0784R7, предложение было принято, и в 2019 году в GCC появляется construct_at примерно в таком виде:
template<typename _Tp, typename... _Args>
constexpr auto construct_at(_Tp* location, _Args&&... args)
noexcept(noexcept(::new((void*)0) _Tp(std::declval<_Args>()...)))
-> decltype(::new((void*)0) _Tp(std::declval<_Args>()...))
{ return ::new((void*)location) _Tp(std::forward<_Args>(args)...); }
Очевидно, никакой особой магии тут нет, лишь банальный вызов placement new. Однако нам за такие вольности в constexpr-функциях компилятор оторвал бы руки:
error: call to non-'constexpr' function 'void* operator new(std::size_t, void*)'
Ну не нравились компилятору функции new в compile-time, да еще ко всяким преобразованиям void* относился с подозрением. Зато на std::construct_at он глаза приподзакроет.
С ней можно конструировать объекты аж в consteval:
consteval int get() {
union {
NonTriv value;
} buf {};
std::construct_at<NonTriv>(&buf.value, 1);
return buf.value.i_;
}
Но, как известно, дай разработчику палец - он возьмет второй, чтоб сломать один, а второй героически потерять. В 2022 году тихое роптание разработчиков собрал в документе P2747R0 (Limited support for constexpr void*) некий Barry Revzin. Он предложил таки сделать исполняемой в compile-time именно placement new, а не суррогат, ибо магическая функция construct_at слишком ограничена по функциональности. Ей можно заменить только инициализацию по значению: new (p) T(args...), а иное заменить невозможно, например, такие случаи как инициализация по умолчанию new (p) T, инициализация списком new (p) T{a, b} и др.
Очень кстати в 2023 году было одобрено предложение P2738R1 (constexpr cast from void*: towards constexpr type-erasure) от Corentin Jabot и David Ledger. Там убедительно доказывалось, что стоит разрешить некоторые преобразования указателей в void* для constexpr выражений.
Раньше мы могли делать такое преобразование:
void *p = static_cast<void*>(&buf.value);
А такого мы делать не смели:
NonTriv *o = static_cast<NonTriv*>(p); error: cast from 'void*' is not allowed in a constant expression before C++26Раз в c++26 мы уже поддерживаем преобразования вроде
static_cast<T*>(static_cast<void*>(p)), то можно подпилить правила и для placement new.
Тогда можно делать так:
consteval int get() {
union {
NonTriv value;
} buf {};
new (&buf.value) NonTriv{2};
return buf.value.i_;
}
В общем, при с++26 все будет хорошо. Он наступит скоро, нужно только подождать. Все идет по плану.446
std::inplace_vector. internals
Как-то раз в Акихабаре, заблудившись в магазине всяческой электроники, я нечаянно вышел к стеллажу с удивительной клавиатурой. Изящные черты ее манили, хромированные части призывно блестели. Сам Довлатов бы не погнушался овладеть такой. Клавиши были приятны глазу, округлы как у антикварного Ундервуда, и взывали к тактильному контакту. Подкравшись к устройству ввода данных, я дал волю рукам. Под пальцами приятно клацало. Не знаю, сколько я там стоял, но японские продавцы стали ползать передо мной в позе догэдза. Я быстро смекнул, что они умоляют меня уйти, чтоб мои крики восторга не пугали постоянных гиковатых клиентов. "Дайдзёбу даё, бака!" - вежливо извинился я и ретировался.
Все эти годы образ прекрасной клавиатуры преследовал меня.
На выходных цифровой детокс дал трещину. Я обнаружил, что хоть мобильный интернет находится в подавленном состоянии, однако сайты известных маркетплейсов доступны. Поскольку делать все равно было нечего, я вспомнил о давней мечте и, немного подумав, вбил в поиске "ретроклавиатура". С выданных карточек на меня смотрела точь-в-точь клавиатура из моего школьного компьютерного класса. Проклятые продаваны, не настолько же я старый, чтоб быть "ретро". Свинство какое. Я решительно закрыл интернет-магазин и пошел смотреть, что внутри у
inplace_vector:
template<typename _Tp, size_t _Nm>
class inplace_vector {
...
private:
union {
_Tp _M_elems[_Nm];
};
...
};
Внутри, конечно же, массив _M_elems из элементов _Tp и размером _Nm. Это ожидаемо, но если бы массив _M_elems не был заключен в объединение, то при конструировании объекта inplace_vector создание элементов массива было бы обязательным. Собственно, тогда бы это была бы почти копия std::array. Здесь же изящно исполнили старый добрый трюк с union, который делает вызов конструктора члена класса необязательным. Мы уже видели, как этот прием использовался в других контейнерах стандартной библиотеки. Совершенно логично притащить это и сюда, а не страдать с какими-нибудь самописными контейнерами стиле std::aligned_storage. Добиться правильной работы с памятью не то что бы сложно, но чревато внезапными проблемами. Будто заходишь в магазин 1000 мелочей за пипидастром, а находишь среди полок дьявола.
Благодаря тому, что работаем мы почти с обычным массивом, можно не беспокоиться о выравнивании или смещении, расположении в памяти и т.п. Заполнение элементами сводится к вызову метода unchecked_emplace_back.
template<typename... _Args>
constexpr _Tp&
unchecked_emplace_back(_Args&&... args) {
__glibcxx_assert(_M_size < _Nm);
auto __p = std::construct_at(data() + _M_size,
std::forward<_Args>(__args)...);
++_M_size;
return *__p;
}
В качестве аргументов передаются через прием universal reference значения для конструктора _Tp. По коду тут все ясно, data() дает адрес начала массива _M_elems, _M_size текущий размер вектора, и, по совместительству, смещение для нового элемента. Мы создаем новый элемент, двигаем _M_size и возвращаем ссылку на новый элемент по адресу __p.
Непосредственно акт творения доверен новой функции std::construct_at. На самом деле конструкция эквивалентна вызову placement new.
auto p = new (data() + _M_size) _Tp{std::forward<_Args>(__args)...};
У construct_at есть свои преимущества, но это уже отдельный вопрос.446
std::inplace_vector
Наконец-то дошли руки до новых полезных и абсолютно замечательных контейнеров. Всем известно, что на ночь я читаю стандарт. Это способствует быстрому засыпанию и здоровому сну. Так вот, не успел я осилить черновик стандарта 2023 года, как на дворе уже год 2025, и коварнейшие из людей успели выпустить новый талмуд. С грустью просматривая 2.5 тысячи страниц, я увидел знакомое название:
inplace_vector.
Стандарт сухо описывает новый библиотечный тип как непрерывный контейнер. Ёмкость его фиксирована, а элементы хранятся прямо внутри самого объекта inplace_vector.
Нам давно уже грозились выдать подобный контейнер. Самое проработанное предложение в этом направлении, которое я видел, это P0843R14, называемое inplace_vector за авторством Gonzalo Brito Gadeschi, Timur Doumler, Nevin Liber и David Sankel.
В документе предлагалось включить в библиотеку вектор, меняющий размер динамически, с фиксированной вместимостью и встроенным хранилищем.
Думаю, embedded-разработчикам не нужно объяснять, зачем это нужно. Хотя все равно поясню:
Такой тип просто необходим, если выделение памяти невозможно. Ага, ведь стандарт разработки прямо это запрещает. Ну или мы брезгуем выделять память из-за снижения производительности. Либо мы очень хотим, чтоб элементы вектора хранились внутри самого объекта, что было бы полезно для сериализации, например. А std::array не подходит по каким-то причинам.
Интересно, что причины появления этого контейнера не поменялись с февраля 2018 года, когда свет увидело первое предложение P0843r1, еще с названием fixed_capacity_vector.
Правда, там сразу оговаривалось, что имя fixed_capacity_vector так себе, пусть просто место покараулит, пока подберут подходящее.
В документе даже приводился список подходящих имен:
array_vector, намекающее на близкородственное скрещивание std::array и std::vector;
bounded_vector, подсказывающее, что вектор ограничен;
static_capacity_vector и fixed_capacity_vector, просто кричащие, что вектор не резиновый;
embedded_vector, самое лучшее название, но к сожалению, сетуют автор, слово embedded уже занято. Да, это мы его заняли;
inline_vector, та же проблема, inline уже застолбили плюсовики;
stack_vector, это вообще неудачное название, что это будет за стек-вектор, если его можно разместить в static памяти?
static_vector - конечно же, появление имени неслучайно, здесь обнаружился идейный вдохновитель - контейнер из Boost.Container boost::container::static_vector<T,Capacity>.
Современное название контейнер получил в 2023 году на слете адептов безличной жизни в Варне. Все предложенные названия были настолько хороши, что выбирая из названий static_vector,
fixed_capacity_vector и невесть откуда взявшегося inplace_vector, подавляющим большинством голосов (14 из 23) был выбран последний вариант.
Что же, благодарствуем, теперь в c++26 у нас появилось это чудо. Жаль, что попробовать его удалось лишь сейчас в trunk версии GCC.
С таким контейнером задача, которая доставила мне немало хлопот, решилась бы за минуту.
Тогда у нас был очень нетривиальный объект вроде
struct NonTrivial {
NonTrivial(int) {}
};
вернее, объектов могло быть много, но никто не мог заранее сказать сколько. Хорошо, что максимальное количество таких объектов было известно.
Понятное дело, std::array<NonTrivial, 10> тут никак не подходит. Массив при создании не сможет вызвать конструктор по умолчанию и откажется собираться.
Тогда много времени ушло на создание специфического контейнера, массива с элементами вектора, что-то вроде std::array<std::optional<NonTrivial>, 10>. Нет, написать его дело нехитрое, а вот протащить через AUTOSAR и цензоров было не очень просто.
Вот бы пригодился тогда std::inplace_vector:
std::inplace_vector<NonTrivial, 10> table {};
Вектор при создании не пытается вызвать конструктор NonTrivial, что весьма приятственно.
Объект конструируется внутри хранилища только при вызове
table.push_back(NonTrivial{1});
и только один раз.
Этот тип настолько прекрасен, что заслуживает отдельной секции вивисекции. A suivre.446
weird reflection. part3
Подобно герою лавкрафтовских Хребтов Безумия, я чувствую себя обязанным предупредить мир об опасности странной рефлексии, целиком и полностью основанной на
__PRETTY_FUNCTION__. Ведь если вас не разубедить, вы не успокоитесь, пока не извлечете на свет божий нечто такое, что погубит плюсы, какими мы их знаем. Например, можно довольно просто определять свои концепты.
Вот представьте, что есть у нас два класса Result в разных пространствах имен:
namespace lol::kek::check {
struct Result {
static inline constinit size_t value {42};
};
} // namespace lol::kek::check
namespace lol::mem::check {
struct Result{};
} // namespace lol::mem::check
Между собой мы заранее договорились, что если класс находится в пространстве имен kek (но это не базовый namespace), то у него есть статическая константа value, которая невероятно важна в каком-то алгоритме.
У классов из других подпространств имен этого свойства нет, поэтому логично было бы заиметь концепт, выделяющий классы именно из этого подпространства имен.
Для этого немного модифицируем нашу функцию handle:
template <class T>
consteval bool handle() {
std::string_view func {__PRETTY_FUNCTION__};
size_t position = func.find("T = ");
func.remove_prefix(position + 4);
position = func.find("]");
func.remove_suffix(func.size() - position);
position = func.find("::kek::");
return position != std::string_view::npos;
}
Если в качестве шаблонного аргумента мы передадим lol::kek::check::Result, то сигнатура функции будет такой
bool handle() [with T = lol::kek::check::Result]
Поэтому мы выделяем подстроку от T = до ], где содержится имя типа. Поскольку оно всегда будет полного вида, т.е. с именами всех пространств имен, определить подпространство kek не составит труда. Достаточно поискать в имени типа строчку ::kek::, и все, возвращаем результат поиска.
Функция помечена как consteval, можно использовать ее во время компиляции. Собственно, до собственного концепта остался единственный шаг.
template <class T>
concept is_kek_subnamespace = handle<T>();
Концепт is_kek_subnamespace проверяет есть ли в полном имени типа T упоминание подпространства kek.
Проверим работоспособность метода на некой функции func:
template <is_kek_subnamespace T>
void func(T) {
std::cout << T::value << std::endl;
}
Вызов функции func(lol::kek::check::Result{}) компилируется без проблем, программа выплюнет в стандартный поток вывода 42.
А вот вызов func(lol::mem::check::Result{}) сделать уже не удастся
template argument deduction/substitution failed: note: constraints not satisfied In substitution of 'template<class T> requires is_kek_subnamespace<T> void func(T) [with T = lol::mem::check::Result]'Не удовлетворяет, значит, концепту, что весьма удовлетворяет нас. Понятия не имею, что с этим делать, но это нужно срочно сунуть куда-нибудь в продакшн, пусть коллеги изумляются. Ну и проверим, не упадет ли чего.
446
weird reflection. part2
Макрос
__PRETTY_FUNCTION__ плох тем, что он не принадлежит стандарту. Это расширение GCC, хотя в других компиляторах могут быть аналогичные решения, не стоит надеяться на идентичную работу макросов. Как они передают значения enum class? Дьявол в деталях.
Есть "правильное" представление, т.е. так, как это сделано в typeid. Имя типа можно получить через метод name, но, как вы помните, это будет декорированное имя. Нас это едва ли остановит:
std::size_t sz {256};
int status {};
char* buffer {static_cast<char*>(std::malloc(sz))};
char *realname {abi::__cxa_demangle(typeid(Y<Result::Ok>{}).name(), buffer, &sz, &status)};
Мы опять беспардонно вытащили abi::__cxa_demangle из обработки исключений ради получения читаемого имени.
Здесь Y это пошлая структура, введенная только ради шаблонного аргумента:
template <Enumable auto T>
struct Y {};
В результате realname представляет имя типа в нормальном виде.
Y<(Result)0>Такое представление нам не очень нравится, ведь
(Result)0 - это на самом деле Result::Ok, негоже тратить лишние когнитивные усилия для осознания этого факта. Если бы __PRETTY_FUNCTION__ тоже выдавал бы имя в таком виде, это было бы печально. Однако __PRETTY_FUNCTION__ представляет значения enum в пределах перечисления в красивом виде: 0 -> Result::Ok, а за пределами перечисления в обычном виде: 3 -> (Result)3. Это сразу наводит на мысль, что таким нехитрым способом можно определить размер enum class!
template <Enumable T, size_t I>
consteval size_t GetSizeImpl() {
constexpr auto const x = handle<T{I}>();
if constexpr ( x.find('(') == std::string_view::npos) {
return GetSizeImpl<T, I+1>();
}
return I;
}
template <Enumable T>
consteval size_t GetEnumSize() {
return GetSizeImpl<T, 0>();
}
Теперь можно использовать это для проверки:
static_assert(GetEnumSize<Result>() == 3); // OK!
Вызов во время компиляции GetEnumSize с указанием enum class запускает рекурсивный поиск границы перечисления. Начинаем мы с проверки нулевого элемента GetSizeImpl<T, 0>, ну, предположим, что все наши enum начинаются с нуля. В функции GetSizeImpl мы получаем результат преобразования значения перечисления в текстовое имя через вызов функции handle<T{I}>(), которую мы набросали в прошлый раз. Если имя не содержит скобочек, то оно точно есть в enum, нам следует продолжить поиск, увеличив значение индекса GetSizeImpl<T, I+1>(). Если скобочки появились, то все, мы дошли до края, текущий индекс элемента I - искомый размер.
Да, работает только для непрерывных перечислений, где все значения идут подряд от нуля. Для разбора иных ситуаций надо думать еще, но лень и неохота. Да и другие странные рефлексии ждут не дождутся своей реализации.446
weird reflection
Тяжело бороться с цифровым детоксом, когда проводного интернета тебе не видать как рыбьих ушей. Ситуация кажется безвыходной, и остается только лишь глядеть на небо. Не с надеждой, нет, а с параболической антенной. Спутников с интернетом на борту для средней полосы России мало, их по пальцам может пересчитать даже фрезеровщик средней руки.
Делать нечего, идем к трехцветному провайдеру на поклон. Интернет-челобитную быстро оформили, но это единственное, что произошло без проволочек. Товарищи божились, что в течение трех рабочих дней комплект приедет, но в означенный срок пришло только разочарование. В ответ на мое справедливое возмущение служба поддержки поведала мне, что в выбранную мной точку выдачи проник конь-людоед, съел сотрудника и похитил мой заказ. Я согласился подождать, пока они не наймут нового сотрудника или не привезут свежее оборудование в другую точку.
Через несколько дней беспокоящих дозвонов они признали, что просто профукали антенну в комплекте, но Степаныч уже придает тазу алюминиевому глубокому из хозмага правильную форму. Окрыленный новой надеждой я не тревожил трехцветных еще неделю, но потом вера в золотые колдырские руки начала угасать, пришлось снова тыкать саппортистов палкой. Они с прискорбием сообщили, что Степаныч не справился и пропал вместе с тазом, заняв пятихатку у главбуха на корм коту. Мой заказ, к сожалению, пришлось передать дилеру. Дозвонившись до оного, я узнал, что он драг и антенну зачем-то уже набил. Дальше было что-то про какого-то хмурого, но слушать я не стал. Стало ясно, что заказ мой помножен на ноль.
Зайдя еще раз в их интернет-магазин чудес, убедился в этом окончательно: пока они мастерски разминали мне мозг, цена оборудования выросла еще на 10 тысяч.
Безумие! Лето заканчивается, а интернета все нет. Этот гротеск мне напомнил один абсурдный метод рефлексии, подсмотренный в одном серьезном проекте. Хотя сейчас он уже не кажется мне таким ужасным. Вот, к примеру, возьмем небольшое перечисление:
enum class Result : int32_t {
Ok = 0,
Nok,
NotImplemented
};
Как нам получить строковые наименования элементов, пригодные для вывода в консоль? Нормальный разработчик может вручную задать map или BiMap, массив на худой конец со строковыми наименованиями, ну а я напишу такую функцию
template <Enumable auto T>
constexpr std::string_view handle() {
std::string_view func {__PRETTY_FUNCTION__};
size_t position = func.find("T = ");
func.remove_prefix(position + 4);
position = func.find(";");
func.remove_suffix(func.size() - position);
return func;
}
Элементарный концепт Enumable ограничивает шаблонный параметр перечислениями:
template <typename T>
concept Enumable = std::is_enum_v<T>;
К тому же, это дает понять, что мы желаем видеть в качестве шаблонного аргумента значение из enum, например, Result::Ok. Здесь важно включить в тип функции не просто enum, а конкретное его значение. Тогда макрос __PRETTY_FUNCTION__ выдаст нам такую строчку
constexpr std::string_view handle() [with auto [requires ::Enumable<<placeholder>, >] T = Result::Ok; std::string_view = std::basic_string_view<char>]
Это то, что нужно! В последнее время у нас появилось множество возможностей для работы со строками на этапе компиляции. Поэтому мы легко можем выделить название значения, ориентируясь на символы "T = ". Работает ли это действительно во время компиляции? Конечно, constexpr не внушает доверия, но
static_assert(handle<Result::Ok>() == "Result::Ok"); // OK!
Собирается без нареканий со стороны компилятора.
Даже если мы вызовем функцию как
handle<Result{2}>()
она вернет Result::NotImplemented.
Конечно, использовать для рефлексии __PRETTY_FUNCTION__ это почти наркомания, но это же работает для GCC. Значит, это рано или поздно попадет в продакшн. Да, я там и видел подобный бесподобный код.446
overload resolution
Когда начинаю бугуртить, глядя на код некоторых проектов, я еду гулять в нижнюю часть города. Там нет интернета, и вероятность написать лишнее крайне мала.
"Ах, — думаю я, проходя мимо памятника паровозу завода 'Красное Сормово', — ведь в конце концов кто знает? Может быть, так и надо. Не может же быть, чтоб разработчики использовали возможности нового стандарта просто так".
Вот, к примеру, делают люди какой-то обработчик команд, где у каждой свой тип:
struct Cmd1 {};
struct Cmd2 {};
struct Cmd3 {};
Времени было мало, реализован только отклик на одну команду. Раньше-то обработчик был бы скучнее, чем обои в виндовсе:
struct Handler {
Result handle(Cmd1) { return Ok; }
Result handle(Cmd2) { return NotImplemented; }
Result handle(Cmd3) { return NotImplemented; }
};
Сейчас же есть много способов реализовать это, глаза разбегаются. Есть же auto, почему бы не применить его здесь?
struct Handler {
Result handle(auto cmd) { return NotImplemented; }
};
Если раскидать сахарок, то внутри мы найдем старый добрый советский...
struct Handler {
template<class type_parameter_0_0>
inline Result handle(type_parameter_0_0 cmd) { return NotImplemented; }
};
Да, да, да, шаблонный метод класса.
Думаю, всем понятно, что при вызове handle(Cmd1{}) будет инстанцирован метод
struct Handler {
...
template<>
inline Result handle<Cmd1>(Cmd1 cmd) { return NotImplemented; }
};
Впрочем, не будем ждать, пока это произойдет, возьмем дело в свои руки. Переопределим свой обработчик для реализованной команды вне класса:
template <>
Result Handler::handle<Cmd1>(Cmd1 x) { return Ok; }
Согласен, это не слишком куртуазно. Да и вообще, метод handle любой тип принимает без разбора. Нехорошо.
Нужно ограничить диапазон передаваемых типов. Хорошо, что плюсы стали модные, сделаем так:
Result handle(auto x) requires(std::is_same_v<decltype(x), Cmd1> ||
std::is_same_v<decltype(x), Cmd2> ||
std::is_same_v<decltype(x), Cmd3>) {
return NotImplemented;
}
Result handle(Cmd1) { return Ok; }
А что? Одну команду мы все-таки реализовали. С одной стороны, появилась неоднозначность, непонятно, что мы хотим на самом деле от обработки Cmd1, однако нет. Код вполне легален, хотя многие гуру не рекомендуют смешивать перегрузку методов и шаблонные методы. Слабаки! Они просто не читали тот небольшой фрагмент стандарта, описывающий мучительный выбор перегруженной функции. Страниц 25 вроде, я бы сказал точнее, если бы дочитал. Начать стоит с 12.2.2 Candidate functions and argument lists [over.match.funcs], где описан процесс формирования списка всех функций, которые хоть как-то подходят для запрашиваемого вызова. Потом быстро пройтись по 12.2.3 Viable functions [over.match.viable], где из всего набора кандидатов выбираются наиболее подходящие. Полирнуть стоит 12.2.4 Best viable function [over.match.best] - назначение крайней функции, которая и будет отдуваться за всех.
Там английским языком написано, что можно сравнить функции-кандидаты F1 и F2, и F1 будет лучшей функцией, если она нешаблонная, а F2 - шаблонная, причем аргументы не нужно будет каким-нибудь особо вычурным образом. Понятно, компилятор просто обязан выбрать Result handle(Cmd1).
Вот если аргументы нужно приводить к variant
Result handle(std::variant<Cmd2, Cmd3>) { return NotImplemented; }
тогда при вызове handle(Cmd2{}) будет выбрана шаблонная функция. Вот если бы шаблонной не было... но это не наш метод!
Остальные типы стильно отсекаем:
Result handle(auto x) requires(!std::is_same_v<decltype(x), Cmd1> &&
!std::is_same_v<decltype(x), Cmd2> &&
!std::is_same_v<decltype(x), Cmd3>) {
return WrongCmd;
}
Хотя в стандарте написано, что у методов с эллипсом приоритет вообще ниже плинтуса, вызовется он, если вообще нет других вариантов, т.е. можно было бы этим воспользоваться
Result handle(...) { return WrongCmd; }
однако это тоже не наш вариант, тут нет духа новой школы!446
inherited struct initialization
Причин моего пригорания было две. Во-первых, компания Пёстелеком. Раньше я относился к ней нейтрально, пока не решил стать ее клиентом. Началось все еще два года назад, когда мимо моего дома проложили новенький черный и блестящий кабель, на котором каждые сто метров красовались синие бирки с гордой надписью "Пёстелеком".
Когда же губернатор стал устраивать нам цифровой детокс, то стал я поглядывать на заветный кабель со все нарастающим вожделением.
В итоге, я махнул рукой на влажную репутацию конторы и подал заявку на подключение.
Заявка быстро ушла в работу, мне уже виделось как пёстелекомовские инженеры склоняются над картой, словно отцы народов, потирают широкие лбы, соображая, как лучше доставить мне интернет.
"Только ADSL", - таков был окончательный вердикт технического отдела.
Ладно, мы люди негордые, ведите сюда вашу медяху. Синие бирки на кабеле издевательски подмигивали.
На следующий день заявку аннулировали. Нет технической возможности, сказали в техподдержке.
Так и слышу, как пёстелекомовский инженер отбрыкивается: "Очумели?! Как я без мобильного интернета буду кабели обжимать?"
Не хотят работать и ладно, но сколько времени упущено, Ди Жэньцзе сам себя на отсмотрит...
Вторая вымораживающая от копчика до макушки вещь - злоупотребление структурами. Людей привлекает их открытость, они начинают использовать их странным для меня образом. Тонкая грань между структурой и классом находится в идеологическом поле. Структура должна использоваться только в качестве бездушного агрегатора типов. У класса же есть душа, он сложнее организован, при порождении объекта класса он начинает жить, вызывается конструктор и т.п.
Нет ничего плохого в структуре:
struct P {int x, y;};
Это агрегатор. Только потом кто-то пишет
return P{.y=1};
Это не ошибка даже, но четкие пацаны собирают с -pedantic -Wall -Wextra, поэтому сборка будет замусорена предупреждениями
warning: missing initializer for member 'P::x' [-Wmissing-field-initializers]Да, безопасней всего инициализировать все. Можно пойти на хитрость и добавить в структуру значения по умолчанию:
struct P {int x = 1, y;};
Не нравится? Используй designator-инициализацию на полную: return P{.x = 1, .y = 1};
Либо вообще не используй: return P{0, 1}
(и потом вспоминай сквозь слезы, какое число что значит).
Однако если нужно менять структуру, добавить новый член, например, то оба варианта потребуют правок всех инициализаций подобного типа объектов.
Еще один изъян обнаруживается, если вспомнить, как в С++ обошлись с наследием Си.
Была разрешена пустая инициализация:
struct {} s = {}; // C++ одобряет
Зато нельзя стало:
- менять порядок инициалиазции членов:
struct P a = {.y = 1, .x = 2}; // C++ не одобряет (не тот порядок)
- миксовать обычную и designator-инициализации.
struct P c = {.x = 1, 2}; // C++ не одобряет (миксование)
- использовать вложенную инициализацию напрямую
struct Np { P p; int x; };
Np np {.p.x = 0, .p.y = 1, .x = 1}; // C++ не одобряет (это не designator)
Правильнее и безопаснее вложенную инициализацию нужно делать так
Np np {.p {.x = 0, .y = 1}, .x = 1};
Вот, как вы помните, открытое наследование не снимает статус агрегата у структуры:
struct A { int x; };
struct B : A { int y; };
Формально, B - это агрегат, можно использовать designator-инициализацию.
На деле же B b {.y=0}; принесет с собой кучу предупреждений компилятора про missing initializer for member 'B::A'.
Оно и понятно, базовый объект A ничем не проинициализирован. Нельзя просто написать B b {{}, .y=0}; - как мы уже выяснили, это запрещенное миксование. Но и выцепить для designator-а базовую структуру нельзя, у нее нет явного имени. Значения по умолчанию тоже не спасают struct A { int x=0; }; компилятору все равно не нравится.
Остается только противная базовая инициализация.
B b {{}, 1};
Поддерживать такое, если в структуру включены довольно сложные классы или множество элементов, удовольствие ниже среднего.
Лучше уж сделать честные классы с конструкторами, чем поддерживать получившуюся агрегатную лапшу.446
std::get_temporary_buffer
В очередной раз роясь в мусо... то есть разглядывая предметы в антикварной лавке, я заметил пару небольших вещиц:
std::get_temporary_buffer и std::return_temporary_buffer.
От самих названий уже веет архаикой. Если произнести их вслух, то эхом вернется отголосок прекрасной эпохи мечтателей. В это сложно поверить, но существовали они еще до с++11 стандарта.
Даже революция одиннадцатых плюсов на них не сильно повлияла, вид get_temporary_buffer остался почти неизменен:
template<class T>
std::pair<T*, std::ptrdiff_t> get_temporary_buffer(std::ptrdiff_t count) noexcept;
Как вы уже догадались по названию, функция запрашивает временный буфер. Если count нулевой или отрицательный, то ничего хорошего нам не видать. В противном случае могут выдать неинициализированное непрерывное хранилище для count объектов типа T.
Впрочем, могут и не выдать, удовлетворить прошение частично, поэтому возвращать приходится пару значений.
Первый элемент пары - указатель на начало выделенной области памяти. Второй элемент показывает для скольких объектов выделена память.
Да, похоже на оператор new, только память остается неинициализированной.
Хоть буфер вроде и временный, но RAII тут неприменим, не молились люди тогда на ООП. Предполагалось освобождать его явно функцией return_temporary_buffer:
template< class T >
void return_temporary_buffer(T *p);
Функция освобождает хранилище, выделенное ранее через get_temporary_buffer.
Особой популярностью эти штуки не пользовались, разработчики недоумевали, зачем оно нужно, если есть malloc и new, и в с++17 функции были объявлены устаревшими. Потом, в с++20, вообще удалены из стандарта. Можно открыть один из последних черновиков и обнаружить бедолаг в списке живых мертвецов: 16.4.5.3.2 Zombie names [zombie.names].
Однако к нам в руки попало письмо некого разработчика gcc, не будем называть имен, хотя это Jonathan Wakely. Он пишет, что хоть функции помечены устаревшими, но ничего сделать нельзя, они уже используется, хоть и не напрямую, в stl_algo.h. Давайте просто спрячем эти функции в глубине стандартной библиотеки и сделаем вид, будто все удалили.
Действительно, если подсунуть GCC код auto x = std::get_temporary_buffer<uint64_t>(4); с опцией -std=c++23, то получим только предупреждение
warning: 'std::pair<_Tp*, long int> std::get_temporary_buffer(ptrdiff_t) [with _Tp = long unsigned int; ptrdiff_t = long int]' is deprecated [-Wdeprecated-declarations]
Можно немного копнуть глубже в GCC и использовать иное пространство имен:
auto x = std::__detail::__get_temporary_buffer<uint64_t>(4);
Только теперь возвращается не пара, а просто указатель, что вполне обосновано, ведь реализация у него такая:
template<typename _Tp>
inline _Tp* __get_temporary_buffer(ptrdiff_t __len) _GLIBCXX_NOTHROW {
...
#if __cpp_aligned_new && __cplusplus >= 201103L
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
return (_Tp*) _GLIBCXX_OPERATOR_NEW(__len * sizeof(_Tp), align_val_t(alignof(_Tp)), nothrow_t());
#endif
return (_Tp*) _GLIBCXX_OPERATOR_NEW(__len * sizeof(_Tp), nothrow_t());
}
Внутри-то там new, кто бы мог подумать! Они должны были побороть new, а не примкнуть к нему...
Собственно, поэтому функции условно мертвые. Временный буфер был предназначен для эффективной оптимизации небольших запросов памяти, но нет свидетельств того, что это было достигнуто на практике. Хотя они старались.446
where is std::like_t
Что же помешало, кроме нелепого названия,
std::like_t войти в стандарт?
Стенограммы обсуждения мне не выдали, но есть такое ощущение, что этой метафункции просто не нашлось достойного применения, кроме как готовить возвращаемое значение для forward_like. Оригинальное же предложение Deducing this приводит такое обоснование:
Набросаем базовый класс B с методом get, который использует явный this, чтоб вернуть ссылку на член класса i типа int.
struct B {
int i {1};
template <typename Self>
auto&& get(this Self&& self) { return self.i; }
};
И тут на сцену врывается некий наследник B - коварный D со своим собственным членом i типа double.
struct D : public B {
double i {2.};
using B::get;
};
Тогда при вызове get от объекта типа D, обнаруживается тонкий момент: Self в методе get будет D, а не B. Функция get вернет ссылку на D::i. Вообще, авторы большой проблемы в этом не видят, просто люди должны помнить, что метод с явным this - просто статическая функция-член с удобным синтаксисом вызова.
Ежели мы хотим странного, придется явно уточнить, что именно мы возвращаем: return (self)::B.i или forward<Self>(self).B::i для большей точности.
Однако все это перестает работать, если наследование приватное!
struct D : private B {
double i {2.};
using B::get;
};
Все, доступа к переменным B из класса D больше нет. Тогда, говорят авторы, упихивать коленом нужно аккуратно, но сильно, чтоб не растерять квалификаторы.
return ((like_t<Self, B>&&)self).i;
Без сантиментов в си-стиле привести self к B и взять нужное.
Именно тут, думаю, комитет с полными от ужаса штанами сполз под столы, откуда уже предложил развидеть последний пример в обмен на исключение like_t.
Вот в полезности std::forward_like никто не усомнился.
Допустим, есть член класса сложного типа, вроде std::unique_ptr<int>, реализуем функцию get старыми дедовскими методами.
struct FarStates {
std::unique_ptr<int> ptr {new int};
auto&& get(this auto&& self) {
return *std::forward<decltype(self)>(self).ptr;
}
};
Так делать совсем не круто, потому что std::unique_ptr всегда разыменовывается в неконстантную ссылку.
В результате такой бессмысленный код компилируется:
FarStates const fs {};
fs.get() = 1; // Этого не должно быть
Теперь воспользуемся forward_like:
auto&& get(this auto&& self) {
return std::forward_like<decltype(self)>(*self.ptr);
}
А вот с этим выражением проблем быть не должно, вернее, компилятор все понял и настучал нам по рукам.
error: assignment of read-only location 'FarStates::get<const FarStates&>(fs)'
Теперь можно не беспокоиться за свои велосипеды.446
std::like_t
"Лето наконец-то перестало притворяться невинной весной и таки показало свой хищный оскал. Многие разработчики от этого попадали в отпуска. Еще бы, ведь использование плюсов повышает температуру тела, что в горячее время года может вызвать необратимую денатурацию белка...", - тут я в ужасе закрыл интернет.
Хватит рыться в с++, надо переходить в легкий жанр, детективные истории в квантовом мире... или в тревел-блогеры податься.
Я открыл путеводитель по Алтаю. На полях семнадцатой страницы карандашом было выведено: "Где
std::like_t?". Я пытался игнорировать надпись и читать дальше про красоты Чулышманской долины, но занозой в мозгу засел вопрос: "Какой еще std::like_t?".
Я закрыл книгу и открыл интернет.
Нечто похожее появилось в стандарте c++23 - std::forward_like.
Описание к этому объекту дается такое:
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;
Возвращает ссылку на x, тип которой имеет те же свойства, что и T&&. То есть возвращаемый тип определяется так:
Если тип std::remove_reference_t<T> константный, тогда добавляем к возвращаемому типу const (итого получаем const std::remove_reference_t<U>).
Если T&& это lvalue ссылка, тогда возвращаемый тип тоже будет lvalue ссылкой (иначе это rvalue ссылка).
Ну и тип T должен быть ссылочным, без волюнтаризма.
Собственно, все эти нововведения вышли из предложения p0847R0 Deducing this в феврале 2018 года за авторством Gašper Ažman, Simon Brand, Ben Deane и Barry Revzin.
Это то самое предложение, которое подарило нам явную передачу this в методы класса. Очень полезная штука, когда нужно реализовать адаптированные под контекст вызова методы, например, оператор доступа к элементу контейнера.
struct accessor {
vector<string>* container;
decltype(auto) operator[](this auto&& self, size_t i);
};
Глянув предложение по диагонали, я вспомнил откуда взялся std::like_t. Предполагалось, что в стандарт войдут две метафункции:
like_t, переносящая все квалификаторы с первого типа на результирующий. (т.е. like_t<int&, double> выдает double&)
и forward_like, модификация std::forward, что передает свой аргумент с адаптацией типа. В общем-то, forward_like<T>(u) это сокращенная запись для std::forward<like_t<T,decltype(u)>>(u).
Эти функции должны были помогать нам правильно реализовать работу с явным this
decltype(auto) operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>((*container)[i]);
}
Думаю, многие уже сталкивались с проблемой, когда тип возвращаемого значения необходимо подстроить под this, которому ничего не стоит подхватить где-нибудь дополнительные квалификаторы. Сейчас приходится городить всякий boilerplate код, а forward_like сильно облегчит нам работу, понимание кода и его сопровождение.
Хотя до стандартизации доползла только передаточная функция, like_t рано сбрасывать со счетов.
Заглянем в недра GCC:
template<typename _Tp, typename _Up>
[[nodiscard,__gnu__::__always_inline__]]
constexpr __like_t<_Tp, _Up>
forward_like(_Up&& __x) noexcept
{ return static_cast<__like_t<_Tp, _Up>>(__x); }
Сопротивление живо! Функция like_t существует под именем std::__like_t. Пользоваться, конечно, нужно с осторожностью.446
std::string_view. endgame
Хоть мобильный интернет появился относительно недавно, я с содроганием вспоминаю времена, когда его не было. Вчера, например. Или позавчера. От безысходности даже залез в соседский сад, привлеченный слабым, но незапароленным сигналом wi-fi. Правда, на клацание клавиш выбежал старик Пропердыкин с берданкой. "Не ел я ваши яблочки, дедушка", - кричу я из-за куста крыжовника. А тот как завопит, что, мол, архаровцы-наркоманы-зумерки все его мегабайты сожрали, и давай солью палить. Еле ноги унес, но кое-что успел записать по поводу третьего и самого убедительного довода для передачи
std::string_view по значению.
Передавать std::string_view по значению хорошо, ибо устраняет неопределенность. Срывает покровы. Ведь если передаешь некий объект по ссылке в функцию, то она знает об объекте далеко не все. Возможно, этот объект принадлежит кому-то еще? Поэтому оптимизировать итоговый код нужно очень консервативно и осторожно.
Передача по значению предоставляет более широкие полномочия для оптимизации.
Возьмем для иллюстрации две функции, вычисляющие длину переданной строки std::string_view. Естественно, найти размер можно и проще, но допустим, на меня напал приступ идиотии.
void byvalue(std::string_view sv, size_t *p) {
*p = 0;
for (size_t i=0; i < sv.size(); ++i) *p += 1;
}
В функцию мы передаем по значению строку sv, затем проходим счетчик от нуля до sv.size() и увеличиваем счетчик *p на каждом шаге. Значение возвращается через указатель.
void byref(const std::string_view& sv, size_t *p) {
*p = 0;
for (size_t i=0; i < sv.size(); ++i) *p += 1;
}
Вторая функция делает ровно то же самое, только sv передается по ссылке.
Казалось бы, нет никакой разницы в алгоритме, отличие только в подаче аргумента. Однако компилятор не согласен, и вот какой результат выдаст GCC ARM32, даже если выкрутим оптимизацию на O3:
byvalue(std::basic_string_view<char, std::char_traits<char>>, unsigned int*): sub sp, sp, #8 str r0, [r2] strd r0, r1, [sp] add sp, sp, #8 bx lrВнезапно компилятор прозревает, что мы хотим сделать столь неуклюжим способом, и безжалостно оптимизирует функцию, оставляя только сохранение длины
sv по указателю p.
Судьба самого объекта sv его заботит мало: моя копия std::string_view, что хочу с ней, то и делаю. Иная картина вырисовывается для передачи по указателю.
byref(std::basic_string_view<char, std::char_traits<char>> const&, unsigned int*): movs r3, #0 str r3, [r1] ldr r2, [r0] cbz r2, .L4 .L5: adds r3, r3, #1 str r3, [r1] ldr r2, [r0] cmp r3, r2 bcc .L5 .L4: bx lrЕсли компилятор и понял идею, то не смеет оптимизировать код, объект
sv функции не принадлежит. GCC включает дурака и с чистой совестью исполняет именно тот бред, что мы ему тут написали. Для x86-64 будет примерно то же самое.
Теперь понимаете, насколько продуман был AUTOSAR, предписывающий передавать по значению любой объект размером не больше 2*sizeof(void*). Эх, как много еще людей не знают истинного стандарта...446
std::string_view. part2
Наконец-то лето стало похоже на настоящее, а это значит, что пришло время отпуска! Его я решил провести в аэропорту Шереметьево. Просто я люблю спать сидя, дорого есть и стоять в длиннющей очереди на посадку, но только не садиться в самолет после получасового переминания с ноги на ногу, а резко сорваться к другому выходу. Какая-никакая, а физическая активность. Почти "веселые старты".
В очередной раз, когда нестройная колонна отдыхающих рассыпалась - рейс задержали еще на неделю, у меня появилось немного времени, чтоб таки вернуться к наиболее веским причинам передавать
std::string_view по значению. Сейчас только пристроюсь с ноутом на освободившееся место у окна на стене. Лучше карабкаться повыше, иначе люди беспардонно головами задевают.
Так вот, хладнокровно рассмотрим ситуацию со стороны вызывающего.
Хотим мы передать некий объект по ссылке, и в большинстве случаев это будет значить, что положить в регистр придется адрес. Т.е. нужно записать это нечто в память, чтоб у него этот адрес вообще появился. Поэтому мы вынуждены порой задействовать стек. Даже если бы наш гипотетический объект уместился бы в регистре.
Допустим, есть две функции, одна принимает std::string_view по значению, другая - по ссылке.
int32_t byvalue(std::string_view sv);
int32_t byref(const std::string_view& sv);
Посмотрим, как будет выглядеть вызов этих функций в ассемблере. Для этого приготовим две функции: callbyvalue и callbyref. Они будет вызывать byvalue и byref соответственно.
void callbyvalue() { byvalue("hello"); }
void callbyref() { byref("hello"); }
Для архитектуры x86-64 GCC сгенерирует такой ассемблер:
.LC0: .string "hello" callbyvalue(): mov edi, 5 mov esi, OFFSET FLAT:.LC0 jmp byvalue(std::basic_string_view<char, std::char_traits<char>>)Тут все очевидно, мы сразу кладем в регистры все что нужно и вызываем
byvalue.
callbyref(): sub rsp, 24 mov rdi, rsp mov QWORD PTR [rsp], 5 mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0 call byref(std::basic_string_view<char, std::char_traits<char>> const&) add rsp, 24 retТут немного сложнее. По сути, мы конструируем полноценный объект
string_view на стеке и передаем его адрес в функцию.
Интересно, так ли это для архитектуры arm32? В прошлый раз результат был не очень. Что же, прогоняем код через соответствующий GCC и видим:
callbyvalue(): sub sp, sp, #8 movs r2, #5 movw r3, #:lower16:.LC0 movt r3, #:upper16:.LC0 strd r2, r3, [sp] ldrd r0, r1, [sp] add sp, sp, #8 b byvalue(std::basic_string_view<char, std::char_traits<char>>)Очень похоже на результат для x86-64, но есть нюанс. Тут мы все равно задействуем стек, хоть оптимизация
-O3 включена. Здесь мы конструируем объект string_view на стеке, затем выгружаем его обратно в регистры r0 и r1, чтоб передать их в руки byvalue.
callbyref():
push {lr}
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
sub sp, sp, #12
ldm r3, {r0, r1}
stm sp, {r0, r1}
mov r0, sp
bl byref(std::basic_string_view<char, std::char_traits<char>> const&)
add sp, sp, #12
pop {pc}
Результат для callbyref никаких неожиданностей не содержит. Мы сохраняем string_view на стеке, копируем адрес в регистр и вызываем byref.
Разница вроде бы незначительная, стек в любом случае используется, но ассемблер второго примера всегда длиннее. Хорошо, скрипя мозгом, согласимся с этим доводом, хотя он такой, "на тоненького". Следующий должен быть просто убийственный.
Тут пальцы ног предательски разжались, и я свалился со стены. Самое время постоять с важным видом в какой-нибудь очереди на посадку.446
std::string_view. part2
Наконец-то лето стало похоже на настоящее, а это значит, что пришло время отпуска! Его я решил провести в аэропорту Шереметьево. Просто я люблю спать сидя, дорого есть и стоять в длиннющей очереди на посадку, но только не садиться в самолет после получасового переминания с ноги на ногу, а резко сорваться к другому выходу. Какая-никакая, а физическая активность. Почти "веселые старты".
В очередной раз, когда нестройная колонна отдыхающих рассыпалась - рейс задержали еще на неделю, у меня появилось немного времени, чтоб таки вернуться к наиболее веским причинам передавать
std::string_view по значению. Сейчас только пристроюсь с ноутом на освободившееся место у окна на стене. Лучше карабкаться повыше, иначе люди беспардонно головами задевают.
Так вот, хладнокровно рассмотрим ситуацию со стороны вызывающего.
Хотим мы передать некий объект по ссылке, и в большинстве случаев это будет значить, что положить в регистр придется адрес. Т.е. нужно записать это нечто в память, чтоб у него этот адрес вообще появился. Поэтому мы вынуждены порой задействовать стек. Даже если бы наш гипотетический объект уместился бы в регистре.
Допустим, есть две функции, одна принимает std::string_view по значению, другая - по ссылке.
int32_t byvalue(std::string_view sv);
int32_t byref(const std::string_view& sv);
Посмотрим, как будет выглядеть вызов этих функций в ассемблере. Для этого приготовим две функции: callbyvalue и callbyref. Они будет вызывать byvalue и byref соответственно.
void callbyvalue() { byvalue("hello"); }
void callbyref() { byref("hello"); }
Для архитектуры x86-64 GCC сгенерирует такой ассемблер:
.LC0: .string "hello" callbyvalue(): mov edi, 5 mov esi, OFFSET FLAT:.LC0 jmp byvalue(std::basic_string_view<char, std::char_traits<char>>)Тут все очевидно, мы сразу кладем в регистры все что нужно и вызываем byvalue.
callbyref(): sub rsp, 24 mov rdi, rsp mov QWORD PTR [rsp], 5 mov QWORD PTR [rsp+8], OFFSET FLAT:.LC0 call byref(std::basic_string_view<char, std::char_traits<char>> const&) add rsp, 24 retТут немного сложнее. По сути, мы конструируем полноценный объект string_view на стеке и передаем его адрес в функцию. Интересно, так ли это для архитектуры arm32? В прошлый раз результат был не очень. Что же, прогоняем код через соответствующий GCC и видим:
callbyvalue(): sub sp, sp, #8 movs r2, #5 movw r3, #:lower16:.LC0 movt r3, #:upper16:.LC0 strd r2, r3, [sp] ldrd r0, r1, [sp] add sp, sp, #8 b byvalue(std::basic_string_view<char, std::char_traits<char>>)Очень похоже на результат для x86-64, но есть нюанс. Тут мы все равно задействуем стек, хоть оптимизация -O3 включена. Здесь мы конструируем объект string_view на стеке, затем выгружаем его обратно в регистры r0 и r1, чтоб передать их в руки byvalue.
callbyref():
push {lr}
movw r3, #:lower16:.LANCHOR0
movt r3, #:upper16:.LANCHOR0
sub sp, sp, #12
ldm r3, {r0, r1}
stm sp, {r0, r1}
mov r0, sp
bl byref(std::basic_string_view<char, std::char_traits<char>> const&)
add sp, sp, #12
pop {pc}
Результат для callbyref никаких неожиданностей не содержит. Мы сохраняем string_view на стеке, копируем адрес в регистр и вызываем byref.
Разница вроде бы незначительная, стек в любом случае используется, но ассемблер второго примера всегда длиннее. Хорошо, скрипя мозгом, согласимся с этим доводом, хотя он такой, "на тоненького". Следующий должен быть просто убийственный.
Тут пальцы ног предательски разжались, и я свалился со стены. Самое время постоять с важным видом в какой-нибудь очереди на посадку.