std::new_handler
Есть такие люди, которым всегда нужно контролировать ситуацию. Я думал, этих людей называют "нормальными разработчиками", но врачи говорят, что это обсессивно-компульсивное расстройство. В общем, шифер потек, кукуха поехала, говорят те, кто ничего не хочет контролировать. Поэтому они никогда и не узнают, что для четких пацанов якобы с ОКР С++ приготовил всякие обработчики интересных ситуаций. Одна из них - динамическое выделение памяти через оператор
В разделе стандарта
Программа на с++ позволяет устанавливать различные обработчики прямо во время исполнения, просто передавая указатель на обработчик, определенный в программе или библиотеке, в функцию
Сигнатура обработчика и служебный функцией определена в стандарте как:
Надо заметить,
Предполагается, что оператор
Во-вторых, если обработчик имеется, то
Если же обработчик милостиво вернул управление, тогда цикл повторяется. И будет крутиться до тех пор, пока попытка выделить память не увенчается успехом, либо обработчик все же прекратит страдания безнадежного приложения.
В стандартной библиотеке реализовано это примерно так:
Какой прок будет от внедрения обработчика неприятных ситуаций в операторе
- можно попытаться освободить память;
- можно изящно завершить программу;
- можно выбросить исключение
Первый пункт предполагает, наверное, что мы найдем самый жирный объект и освободим память под ним, тогда запрошенный в new кусочек станет доступным. Эх, если бы MISRA не запрещала бы пользоваться динамическим распределением памяти, обязательно бы попробовал.
Есть такие люди, которым всегда нужно контролировать ситуацию. Я думал, этих людей называют "нормальными разработчиками", но врачи говорят, что это обсессивно-компульсивное расстройство. В общем, шифер потек, кукуха поехала, говорят те, кто ничего не хочет контролировать. Поэтому они никогда и не узнают, что для четких пацанов якобы с ОКР С++ приготовил всякие обработчики интересных ситуаций. Одна из них - динамическое выделение памяти через оператор
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 не запрещала бы пользоваться динамическим распределением памяти, обязательно бы попробовал.
👍5🤔1
std::terminate_handler
Еще одна функция, которая помогает нам изящно завершать работу прошивки или правильно перезагружаться, -
Это приводит к
Этим же закончится и попытка бросить исключение, если вдруг
В конце концов, если в прошивке что-то пошло не так, то можно бросить белое полотенце на ринг, то есть вызвать
Затем просто назначаем обработчик:
Функция вернет старый обработчик, ради интереса его можно вызвать, получив отповедь в
Чем же на самом деле занимается
Объявлена она как
В GCC реализация выглядит так:
Здесь просто вызывается
По сути же, это просто обертка для отлова исключений, которых не должно быть при исполнении
Такой обработчик может обнаружить ситуацию, когда прошивка собирается с флагом
Еще одна функция, которая помогает нам изящно завершать работу прошивки или правильно перезагружаться, -
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
.👍6
С++ Zero Cost Conf
Прошу прощения, но публикация продолжения пока откладывается. Все мое время было безжалостно съедено подготовкой к публичному мероприятию. Где-то недели две назад меня спросили, можем ли мы что-то рассказать интересного про наши проекты. Я лишь многозначительно кивнул головой. И вот, несколько дней назад узнаю, что уже 27 июля 2024 года мне предстоит выступать на крупной IT-конференции: С++ Zero Cost Conf https://cppzerocostconf.yandex.ru/cppzerocostconf_2024
Там хоть и написано, что доклад про избавление проекта от динамического распределения памяти, но это не точно. Скорее, поделюсь самым интересным, что встречалось на прошлом проекте с жесткими требования безопасности. Как выживать с MISRA на шее, что делать, если в лесу на вас напал AUTOSAR. Что не войдет по этическим соображениям в основной доклад, обязательно опубликую здесь.
Заходите, если будет время. Хотя я сам в субботу в городе не сидел бы. Лениво лежал бы где-нибудь на пляже и пальцем на песке рисовал полиморфные аллокаторы.
Ладно, побежал делать презентацию, а то пока только три слайда есть: титульный, приветствие и "спасибо за внимание".
До скорой встречи!
Прошу прощения, но публикация продолжения пока откладывается. Все мое время было безжалостно съедено подготовкой к публичному мероприятию. Где-то недели две назад меня спросили, можем ли мы что-то рассказать интересного про наши проекты. Я лишь многозначительно кивнул головой. И вот, несколько дней назад узнаю, что уже 27 июля 2024 года мне предстоит выступать на крупной IT-конференции: С++ Zero Cost Conf https://cppzerocostconf.yandex.ru/cppzerocostconf_2024
Там хоть и написано, что доклад про избавление проекта от динамического распределения памяти, но это не точно. Скорее, поделюсь самым интересным, что встречалось на прошлом проекте с жесткими требования безопасности. Как выживать с MISRA на шее, что делать, если в лесу на вас напал AUTOSAR. Что не войдет по этическим соображениям в основной доклад, обязательно опубликую здесь.
Заходите, если будет время. Хотя я сам в субботу в городе не сидел бы. Лениво лежал бы где-нибудь на пляже и пальцем на песке рисовал полиморфные аллокаторы.
Ладно, побежал делать презентацию, а то пока только три слайда есть: титульный, приветствие и "спасибо за внимание".
До скорой встречи!
C++ Zero Cost Conf 2024
C++ Zero Cost Conf 27/07
🔥7👍4🤔1
std::zero_cost_conf
Вот и прошла конференция. Я, совершенно обессиленный, был погружен в поезд и отправлен восвояси. Может показаться, что мероприятие длится всего несколько часов, но на самом деле оно начинается за месяц до, когда ты неосторожно соглашаешься написать заявку. Ну, большое ли дело? В управляющую компанию я тоже заявки писал, чтоб садик перед домом облагородили, и они уже лет пять на рассмотрении. Здесь же события развивались со скоростью не Ласточки, но Синкансэна. Раз, и сообщают, что ты в программе конференции. Два, ты уже в зуме на черновом прогоне доклада. Три, уже в поезде мчишь в столицу, а в голове проносится: "Куда ты лезешь, она тебя сожрет!".
Кстати, перед конференцией обязательно должен быть технический прогон. Пришлось прибыть пораньше и предстать пред ясны очи организаторов а-ля натюрель.
"С Горького я, странник, пришел спикеров говорящих посмотреть...", - жалобно простонал я на входе и просочился в зал. Там на меня с осуждением смотрели бесконечные ряды пустых стульев, грозно намекая на завтрашний позор. Надев микрофон, я гордо поднялся на сцену. К сожалению, только мысленно. Первые пятнадцать минут меня пытались достать шваброй из-под сцены. Когда наконец-то я оказался в правильной конфигурации, на подмостках, то вместо доклада тихо запел "Боже, Царя храни". В глазах оргов блестели слезы. Это потому, что голос очень красивый. Я был единственным выпускником музыкальной школы, которому на экзамене запретили петь, дабы не смущать одноклассников, а главное, комиссию.
На следующий день меня выселили из гостиницы, поскольку стук зубов мешал соседям спать. Вот и на конференцию пришел пораньше, спрятался в спикерской и скрытно наблюдал другие выступления. И тут в докладе Антона Полухина (спокойно, он меня не заметил) я слышу, что наконец-то в стандарте будет
Это динамически изменяемый вектор, но с фиксированной емкостью, что определяется во время компиляции. Элементы хранятся внутри непрерывного участка памяти. Очень напоминает
Наконец-то можно отказаться от приплясываний около
"Вот ведь!" - думаю, - "такими темпами мне и рассказать будет не о чем!"
А тут еще оказывается и
Вот тут, поскольку
Это запретят в с++26! с++26? Тут я расслабился и чуть не проспал выступление. Остальное вы знаете. Спасибо, что смотрели.
Вот и прошла конференция. Я, совершенно обессиленный, был погружен в поезд и отправлен восвояси. Может показаться, что мероприятие длится всего несколько часов, но на самом деле оно начинается за месяц до, когда ты неосторожно соглашаешься написать заявку. Ну, большое ли дело? В управляющую компанию я тоже заявки писал, чтоб садик перед домом облагородили, и они уже лет пять на рассмотрении. Здесь же события развивались со скоростью не Ласточки, но Синкансэна. Раз, и сообщают, что ты в программе конференции. Два, ты уже в зуме на черновом прогоне доклада. Три, уже в поезде мчишь в столицу, а в голове проносится: "Куда ты лезешь, она тебя сожрет!".
Кстати, перед конференцией обязательно должен быть технический прогон. Пришлось прибыть пораньше и предстать пред ясны очи организаторов а-ля натюрель.
"С Горького я, странник, пришел спикеров говорящих посмотреть...", - жалобно простонал я на входе и просочился в зал. Там на меня с осуждением смотрели бесконечные ряды пустых стульев, грозно намекая на завтрашний позор. Надев микрофон, я гордо поднялся на сцену. К сожалению, только мысленно. Первые пятнадцать минут меня пытались достать шваброй из-под сцены. Когда наконец-то я оказался в правильной конфигурации, на подмостках, то вместо доклада тихо запел "Боже, Царя храни". В глазах оргов блестели слезы. Это потому, что голос очень красивый. Я был единственным выпускником музыкальной школы, которому на экзамене запретили петь, дабы не смущать одноклассников, а главное, комиссию.
На следующий день меня выселили из гостиницы, поскольку стук зубов мешал соседям спать. Вот и на конференцию пришел пораньше, спрятался в спикерской и скрытно наблюдал другие выступления. И тут в докладе Антона Полухина (спокойно, он меня не заметил) я слышу, что наконец-то в стандарте будет
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? Тут я расслабился и чуть не проспал выступление. Остальное вы знаете. Спасибо, что смотрели.
👍9❤4🔥3
std::terminate
"Уходить надо красиво", - говаривал один мой товарищ, хотя сам едва ли это умел. Он до того боялся сказать менеджеру о скором увольнении "по собственному", что решил инсценировать собственную погибель. Пришел на работу пораньше и усадил на стул в кубикле очень похожего на себя кадавра. В свитерах с оленями все мы на одно лицо. "Вот найдут сие хладное тело", - рассуждал он, - "решат, что сгорел на работе, да уволят по причине игры в ящик". К сожалению, дерзкий план рухнул. Менеджер не только не заметил снижения продуктивности работника, но и назначил покойника техлидом. Поскольку тот сделался приятным человеком, и больше никогда не спорил с начальством. Уходить нужно уметь. Даже приложение нужно покидать грациозно. Как
В GCC незамутненная функция
Непонятный дефайн портит ясную картину, поэтому для окончательного просветления:
Главное здесь заключается в том, что раскрывается он по-разному, в зависимости от параметров сборки стандартной библиотеки.
Если, например, исключения запрещены, то глобальным обработчиком
Что же делает
Во-первых, он отслеживает вызовы
Во-вторых, проверяет, был ли вызван
В-третьих, вызывает
Второй пункт кажется наиболее интересным. Можно ли и в нашем самописном обработчике проверить контекст исключения? Легко!
Функция
Такой же типовой объект можно получить и в блоке
Если мы напишем в коде такую конструкцию:
то
Да, соглашусь, имена получились не очень, но это поправимо.
"Уходить надо красиво", - говаривал один мой товарищ, хотя сам едва ли это умел. Он до того боялся сказать менеджеру о скором увольнении "по собственному", что решил инсценировать собственную погибель. Пришел на работу пораньше и усадил на стул в кубикле очень похожего на себя кадавра. В свитерах с оленями все мы на одно лицо. "Вот найдут сие хладное тело", - рассуждал он, - "решат, что сгорел на работе, да уволят по причине игры в ящик". К сожалению, дерзкий план рухнул. Менеджер не только не заметил снижения продуктивности работника, но и назначил покойника техлидом. Поскольку тот сделался приятным человеком, и больше никогда не спорил с начальством. Уходить нужно уметь. Даже приложение нужно покидать грациозно. Как
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
.Да, соглашусь, имена получились не очень, но это поправимо.
👍3👏2
abi::__cxa_demangle
Древний трактат Лунь Юй повествует о том, что однажды Цзы Лу спросил Конфуция: "Вэйский правитель привлечет вас к управлению государством. Что вы сделаете наперво?"
Конфуций ответил: "Необходимо начать с исправления имен".
Цзы Лу заметил: "Вы начинаете издалека. Зачем нужно исправлять имена?"
Тут учитель сказал: "Как ты необразован! Я тебе перегрузку функций без декорирования рожу, что ли?!"
Философ был прав, декорирование имен - это очень интересная особенность плюсов, четко обособляющая его от прародителя. Как вы помните, мы можем получить такие имена как результат функции
Есть некая магическая функция, которая разбирает каракули компилятора и возвращает нормальное имя типа:
где
Если
В
Если совесть и стандарты безопасности позволяют работать с динамической памятью, то можно использовать
При сильном желании и безрассудстве, можно попытаться использовать статический буфер.
Но будьте осторожны, функция
Интересно, что обратного преобразования из обычного в декорированное имя ABI не предлагает. Не больно-то и хотелось.
Чтоб два раза не вставать, скажу, что в обработчике
Поскольку мы в подтвержденном контексте исключения, попробуем во внутреннем блоке
Так-то, Цзы Лу, если имена неправильны, то слова не имеют под собой оснований.
Древний трактат Лунь Юй повествует о том, что однажды Цзы Лу спросил Конфуция: "Вэйский правитель привлечет вас к управлению государством. Что вы сделаете наперво?"
Конфуций ответил: "Необходимо начать с исправления имен".
Цзы Лу заметил: "Вы начинаете издалека. Зачем нужно исправлять имена?"
Тут учитель сказал: "Как ты необразован! Я тебе перегрузку функций без декорирования рожу, что ли?!"
Философ был прав, декорирование имен - это очень интересная особенность плюсов, четко обособляющая его от прародителя. Как вы помните, мы можем получить такие имена как результат функции
__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()
.Так-то, Цзы Лу, если имена неправильны, то слова не имеют под собой оснований.
👍4🤔3
ctor trick
"Спрячь этот слайд, его никто не должен увидеть!" - мой собеседник неуловимо изменился в лице, когда понял, о чем я рассказываю. То ли он побледнел, то ли поседел, по видеосвязи сложно было понять, впрочем, восторгом он явно не лучился: "Не представляешь, что начнется, если разработчики узнают правду!"
Вот так из моего доклада пропал важный кусок, раскрывающий грязные секретики GCC. На самом деле я хотел показать еще одну причину никогда не использовать
Допустим, есть у нас простецкий класс:
Ничего примечательного, кроме конструктора. Да и тот, как вы знаете, не совсем настоящая функция: адрес конструктора невозможно получить.
Точно невозможно? Ведь пользовательские конструкторы вполне себе настоящие, по крайней мере, в нашем классе
Вот и поймали мы конструктор базового объекта за хвост и можем вполне его применить:
Тут мы насильственно создаем указатель на объект
Кроме этого GCC позволяет дополнять недоопределенные конструкторы вручную.
Допишем около нашего класса еще одну неприметную функцию:
Теперь скажите, чему будет равно значение внутренней переменной здесь:
Правильно,
Как такое вообще возможно? GCC, следуя ABI, создает несколько конструкторов:
- конструктор полного объекта с суффиксом C1,
- конструктор базового объекта с суффиксом C2.
В нашем примере, когда мы создаем объект, то вызываем конструктор полного объекта (C1), и если нет сложного наследования, то C1 используется всегда. А вот конструктор класса
Вот этой слабостью можно воспользоваться, мы просто определим свой конструктор полного типа с блэкджеком и девицами.
Однако эта функция только изображает из себя конструктор, ибо оперировать приватными членами класса она не может. Зато вполне легально внутри конструктора полного объекта можно вызвать базовый настоящий:
Никому не рекомендую использовать эти трюки, тем более у нормальных людей нужды в таком не возникает. А за остальными выехал отдел цензуры.
"Спрячь этот слайд, его никто не должен увидеть!" - мой собеседник неуловимо изменился в лице, когда понял, о чем я рассказываю. То ли он побледнел, то ли поседел, по видеосвязи сложно было понять, впрочем, восторгом он явно не лучился: "Не представляешь, что начнется, если разработчики узнают правду!"
Вот так из моего доклада пропал важный кусок, раскрывающий грязные секретики 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);
}
Никому не рекомендую использовать эти трюки, тем более у нормальных людей нужды в таком не возникает. А за остальными выехал отдел цензуры.
👍7👏2❤1
transactional memory
Снились мне братья Алиевы. Стоя на плоту, они молча укоряли меня за нераскрытую тему переопределенных конструкторов
Приглядимся получше к образовавшемуся безобразию. В файле
Далее следуют заклинания, призывающие Вельзевула.
Чуть ниже в этом же файле этот дьявольский макрос применяется к
Это значит, что макрос определит псевдоконструктор
Что задумало GCC? Какие еще клоны?
Срочно открываем
Функция, объявленная
Ну, теперь все стало на свои места! Жуткий макрос
Это довольно старая концепция, описана много десятилетий назад. Тема обширная, но если в двух словах, то эти ваши мьютексы и прочие 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, описывающее, какие изменения надо внести в стандарт для поддержки транзакций.
Однако в настоящее время это все еще не часть стандарта. Хотя можно подключить библиотеку
В этом случае, если пометить конструктор как
компилятор любезно создаст для нас как обычный конструктор
Так вот, в таком блоке мы напишем
Тут и пригодится наш конструктор-клон
Снились мне братья Алиевы. Стоя на плоту, они молча укоряли меня за нераскрытую тему переопределенных конструкторов
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
, дальше действовать будет он.👍2🤯2😈1
transaction_safe & runtime_error
Хоть на улице еще тепло, но не нужно даже переворачивать календарь, чтоб понять какой сейчас месяц. Черно-белые стайки scholaris vulgaris (школотрон обыкновенный) уже кучкуются возле казенных учреждений, а это первый и бесспорный признак осени. Скоро снова будем любоваться красиво желтеющей листвой тенистых аллей, а в тишине морозного утреннего воздуха слышать крик отчаяния
Допустим, я поддерживаю свою нестандартную библиотеку, и с незапамятных времен имеется там некая структура
Для начала добавим макрос
Поскольку
Для нормальных людей ничего не поменяется, они по-прежнему могут включать заголовочный файл моей библиотеки, не опасаясь подвоха. Реализация конструктора и деструктора тоже останется на своих местах:
И библиотеку я буду собирать по-старому, без всяких там
Выглядит ужасно. Конструктор для особых случаев больше не копирует действия основного конструктора. Это может быть оправдано, ведь конструкторы классов
Тогда при подключении нашей библиотеки к такому целевому коду (параметры компиляции
скомпилированный код будет выглядеть примерно так:
Здесь компилятор вызовет именно транзакционную версию конструктора и деструктора, те, что мы обманным путем определили вручную. Чуете, чем пахнет? Грязным хаком!
Хоть на улице еще тепло, но не нужно даже переворачивать календарь, чтоб понять какой сейчас месяц. Черно-белые стайки 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
Здесь компилятор вызовет именно транзакционную версию конструктора и деструктора, те, что мы обманным путем определили вручную. Чуете, чем пахнет? Грязным хаком!
👍4
weird memory leak
Как сейчас помню, обсуждали, что бы такого интересного рассказать на конференции. Жара была страшная, шел грибной снег. Почему бы не рассказать про опасность утечек памяти? Ведь даже RAII не гарантирует успех, если применять его бездумно. Вот допустим, в одном модуле прошивки мы создаем на некотором участке памяти некий объект
Этот
Теперь, когда объект
Обработчик пустой, поскольку нам лень что-то делать.
К вящему удивлению, конструктор
Происходит банальная утечка памяти, несмотря на якобы переданное владение объектом
Здесь собеседник прервал мой рассказ: "Вам не кажется, что пример несколько надуман? Такое в реальной разработке никто не станет делать! В здравом уме-то..."
Как я ни убеждал, что это относительно свежий случай из реальной практики, никто не поверил.
...подающий надежды талант из очень Латинской Америки, Хосе работал в штате у заказчика. Он никак не мог найти, где протекает память, поэтому попросил меня. Мне было скучно, и я с удовольствием ловил ошибку в течение нескольких часов. Как оказалось, в механизм передачи внутренних сообщений (что-то вроде
Хосе вероломно нарушил
В качестве исключения допускается переход владения путем передачи
Код Хосе плох со всех сторон. В обработчике не происходит передача владения, поскольку нет перемещения ссылки в объект.
Должно быть так:
или так:
В этих случаях владение действительно передается, и при выходе из обработчика вызываются все нужные деструкторы.
Ну и еще одно маленькое замечание. Объект создается через placement new, но юное дарование не посчитало нужным вызвать деструктор явно.
Должно быть хотя бы так:
"Почему так?" - с болью в голосе вопрошал я Хосе.
"Разве unique_ptr не должен был все разрулить? Я же передавал владение объектом. Вот в rust все безопасно, не понимаю, почему в плюсах все так сложно", - пожимал он плечами.
"Да все просто, надо только их знать!" - укорял я.
"Ой, ну все! Я вот не знаю плюсов, всю эту ерунду мне пишет chatGPT"
Я в ужасе смотрел на него. Теперь многое стало на свои места.
"Тебе все равно никто не поверит", - залился он беззаботным смехом.
Как сейчас помню, обсуждали, что бы такого интересного рассказать на конференции. Жара была страшная, шел грибной снег. Почему бы не рассказать про опасность утечек памяти? Ведь даже 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"
Я в ужасе смотрел на него. Теперь многое стало на свои места.
"Тебе все равно никто не поверит", - залился он беззаботным смехом.
👍5🤣5
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(); }
В общем, плюсы намекают, мол, сами следите: чего вы там насоздавали в объединении, то и удаляйте.
👍6
rule A9-5-1
Томаш мне определенно не нравился. Собрав вокруг себя клубок единомышленников, он в очередной раз устроил набег на мой пулл-реквест. Весь код был испещрен комментариями, казавшимися мне нестерпимо глупыми, пошлыми и издевательскими настолько, что заставляли глаз непроизвольно дергаться. Мне определенно нужна была своя банда, чтоб отбиваться вместе.
Позвонил нашему новичку, Марчину, уж его-то будет проще вытащить на мою орбиту. Однако небольшой тест на лояльность не помешает.
"Привет, Марчин!" — сказал я как можно беззаботней. — "Подскажи, что значит: chuj ci w oko?"
Мой коллега забавно округлил глаза и недоуменно произнес: "Привет, а зачем тебе?".
"Да вот, Томаш мне утром сказал", — продолжал я с невинным видом, — "говорит, это что-то среднее между 'будь здоров' и 'очень ценю ваше мнение'".
"Кгхммм..." — кажется, Марчин начал задыхаться, на очах его выступили слезы, — "нуууу... раз он так сказал, то так и есть".
"Спасибо! Я знал, что ты не подведешь! Но пасаран!" - отключив сконфуженно улыбающегося Марчина, я с горечью подумал: "Этакая ты бобр курва, не выйдет у нас с тобой объединения".
А что же AUTOSAR говорит по поводу объединений?
Правило
Лапидарно, доходчиво. Обоснование приводится подобающее: объединения ни разу не типобезопасны, и их использование может легко заморочить голову разработчику, который, скорее всего, поймет вашу хитрую конструкцию неправильно.
Однако какое же правило без исключений?!
Строгие законы разрешают использовать меченый
Под мечеными объединениями имеется в виду, что мы где-то будем хранить номер или тип активного члена объединения. Выглядеть это должно примерно так:
Рядом с
Авторы стандарта предлагают использовать это подобным образом:
Удобство использования просто поражает воображение, зато это совершенно легально! Но, я думаю, все уже догадались, что это и есть примитивный
Это, конечно, не полный эквивалент объединения. Например, теперь вы не можете задать пустое объединение:
Но зачем бы такое могло понадобиться, ума не приложу. Зато теперь при разрушении объекта
А что же внутри у
Томаш мне определенно не нравился. Собрав вокруг себя клубок единомышленников, он в очередной раз устроил набег на мой пулл-реквест. Весь код был испещрен комментариями, казавшимися мне нестерпимо глупыми, пошлыми и издевательскими настолько, что заставляли глаз непроизвольно дергаться. Мне определенно нужна была своя банда, чтоб отбиваться вместе.
Позвонил нашему новичку, Марчину, уж его-то будет проще вытащить на мою орбиту. Однако небольшой тест на лояльность не помешает.
"Привет, Марчин!" — сказал я как можно беззаботней. — "Подскажи, что значит: 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
и индекс активного элемента, но без стандартной хитрости не обошлось, конечно. Иначе и быть не могло, без вариантов.👍1😁1🤔1
std::variant
Видел я некоторые реализации стандартной библиотеки, но только исследование исходников GCC вызывает стойкую ассоциацию с рысканием по свалке. В хорошем смысле, конечно. В одном месте вещицу забавную обнаружишь, к себе в проект утащишь, в другом - велосипед найдешь, точь-в-точь такой ты изобрел 10 лет назад! Вот вчера порылся опять, нашел красивый
Только вот объединение у нас не простое, а шаблонное
Стандарт в разделе
А раз
Если хорошенько подумать, это дико удобно. Есть возможность по индексу достучаться до любого элемента объединения даже во время компиляции. Кстати, в качестве индекса удобней брать не просто число, а целый тип, например, стандартный
Сойдет и так! В лучших традициях функционального программирования, наше шаблонное объединение - это рекурсивно воссоздающаяся структура. Давайте максимально упростим конструкцию, чтоб лучше рассмотреть суть:
Это общее определение, настолько неконкретное, что сработает, только когда типов в
А вот его частичная специализация работает всегда, если есть хоть один тип в пакете.
Здесь берется первый тип
Тогда, если мы захотим сделать такое объединение (допустим, что у нас есть конструктор по умолчанию),
будет вызван только конструктор по умолчанию
Однако если мы очень хотим сразу обозначить активный элемент, то стоит явно это указать:
Здесь сначала будет вызван конструктор
Судя по ненулевому индексу, следует вызвать конструктор объекта
Тут счетчик в типе индекса наконец-то равен нулю, и мы благополучно инициализируем первый элемент.
Это распространенный прием приведения в соответствие шаблонного элемента к индексу,
То же работает и с инициализацией самого
Тем самым устранив возможную неоднозначность. Можно и без тэгов, но об этом потом. Watchdog гонит меня со свалки.
Видел я некоторые реализации стандартной библиотеки, но только исследование исходников GCC вызывает стойкую ассоциацию с рысканием по свалке. В хорошем смысле, конечно. В одном месте вещицу забавную обнаружишь, к себе в проект утащишь, в другом - велосипед найдешь, точь-в-точь такой ты изобрел 10 лет назад! Вот вчера порылся опять, нашел красивый
std::variant
, тонкая работа, добротная, сейчас таких уже не делают. Дай, думаю, и вам покажу, что в GCC он сделан на основе объединения и индекса активного элемента.template<typename... _Types>
class variant
...
_Variadic_union<_Types...> _M_u;
__index_type _M_index;
Только вот объединение у нас не простое, а шаблонное
_Variadic_union
, в качестве шаблонных аргументов передаются все типы из std::variant
, естественно.Стандарт в разделе
11.5 Unions [class.union]
стихе первом говорит нам: A union is a class defined with the class-key union
.А раз
union
это класс, то почему мы ему не быть шаблоном?Если хорошенько подумать, это дико удобно. Есть возможность по индексу достучаться до любого элемента объединения даже во время компиляции. Кстати, в качестве индекса удобней брать не просто число, а целый тип, например, стандартный
std::in_place_index_t
, так называемый тег устранения неоднозначности. На самом деле этот индекс-тэг очень просто устроен, он даже не приводится автоматически к целому значению, как std::integral_constant
.template<size_t _Idx> struct in_place_index_t {
explicit in_place_index_t() = default;
};
template<size_t _Idx>
inline constexpr in_place_index_t<_Idx> in_place_index{};
Сойдет и так! В лучших традициях функционального программирования, наше шаблонное объединение - это рекурсивно воссоздающаяся структура. Давайте максимально упростим конструкцию, чтоб лучше рассмотреть суть:
template <class ... Types>
union _Variadic_union {
};
Это общее определение, настолько неконкретное, что сработает, только когда типов в
Types...
не останется вовсе.А вот его частичная специализация работает всегда, если есть хоть один тип в пакете.
template <class Head, class ... Tail>
union _Variadic_union<Head, Tail ...> {
template <class T>
_Variadic_union(in_place_index_t<0>, T && t) : first {t} {}
template <int N, class T>
_Variadic_union(in_place_index_t<N>, T && t) : rest {in_place_index<N-1>{}, t} {}
Head first;
_Variadic_union<Tail...> rest;
};
Здесь берется первый тип
Head
из пакетика и декларируется первый элемент объединения этого типа. Вторым элементом, который воссоздается на этом же адресе, конечно, будет другое шаблонное объединение _Variadic_union<Tail...> rest
. То есть тот же _Variadic_union
, но в качестве шаблонного параметра мы передаем оставшийся пакет без первого типа Head
.Тогда, если мы захотим сделать такое объединение (допустим, что у нас есть конструктор по умолчанию),
_Variadic_union<int, float, X> var {};
будет вызван только конструктор по умолчанию
_Variadic_union<int, float, X>::_Variadic_union()
. Члены объединения останутся неинициализированными.Однако если мы очень хотим сразу обозначить активный элемент, то стоит явно это указать:
_Variadic_union<int, float, X> var {in_place_index<1>{}, 3.14F};
Здесь сначала будет вызван конструктор
_Variadic_union<int, float, X>::_Variadic_union(in_place_index_t<1>, 3.14F)
Судя по ненулевому индексу, следует вызвать конструктор объекта
rest
: _Variadic_union<float, X>::_Variadic_union(in_place_index_t<0>, 3.14F)
Тут счетчик в типе индекса наконец-то равен нулю, и мы благополучно инициализируем первый элемент.
Это распространенный прием приведения в соответствие шаблонного элемента к индексу,
std::tuple
использует похожий метод индексации.То же работает и с инициализацией самого
std::variant
, можно сразу и явно выбрать активный элемент через индекс-тэг.std::variant<float, int, X> f{std::in_place_index<1>, 42};
Тем самым устранив возможную неоднозначность. Можно и без тэгов, но об этом потом. Watchdog гонит меня со свалки.
✍2👍2
std::__detail::__variant::_Uninitialized
На первый взгляд
Сидящая внутри структура
Вспомним, что у таких объединений скрыто удален конструктор и деструктор по умолчанию, и создавать такой объект невероятно трудно. Выход есть! Стандартная библиотека на этот случай выдавила из себя вспомогательный класс-обертку
В комментарии к этому удивительному решению слышатся жалостливые стоны неизвестного разработчика о том, что некий пункт 10.5.3 в
Тут же в следующих строчках появляется предположение, что мы не захотим удалять
Чуть позже все пошло наперекосяк: в с++17
Разработчики попытались напрямую привязаться к
Эта конструкция гарантированно является тривиально разрушаемым типом, даже если
Если один из типов нетривиально разрушаемый, тогда добавляем деструктор через концепт
На первый взгляд
std::variant
может показаться скучнейшим стандартным контейнером, но стоит только вглядеться в исходный код, тут же вы приметите следы бушующих внутри страстей. Особенно интересно было наблюдать сражения за свойство is_trivially_destructible
. О, сколько велосипедов было сломано в этой борьбе! Как известно, если переданные типы Types...
тривиально разрушаемы, то и std::variant<Types...>
наследует это свойство. В этом случае нет нужды заботиться о деструкторе вообще. Иначе есть нюансы.Сидящая внутри структура
_Variadic_union
вынуждена работать с любым типом, то есть деструктора у нее вроде и не должно быть, но с нетривиальными типами изволь работать без проблем. Напрямую реализовать это таки проблематично.union R {
int t;
std::string str;
};
Вспомним, что у таких объединений скрыто удален конструктор и деструктор по умолчанию, и создавать такой объект невероятно трудно. Выход есть! Стандартная библиотека на этот случай выдавила из себя вспомогательный класс-обертку
_Uninitialized
:template<typename _First, typename... _Rest>
union _Variadic_union<_First, _Rest...> {
...
_Uninitialized<_First> _M_first;
_Variadic_union<_Rest...> _M_rest;
};
_Uninitialized<T>
гарантированно будет литеральным типом, даже если T
не является таковым. К слову, литеральные типы - это типы, которым позволено быть constexpr
, их можно создавать таковыми, производить операции над ними и даже возвращать из constexpr
функций.В комментарии к этому удивительному решению слышатся жалостливые стоны неизвестного разработчика о том, что некий пункт 10.5.3 в
[basic.types]
не реализован (тут имеется в виду документ N4606 2016 года) и объединение даже с литеральными типом на борту не считается литеральным типом. Вот когда это свойство реализуют, он тут же уберет этот костыль. Ха, он и поныне там!template<typename _Type, bool = std::is_literal_type_v<_Type>>
struct _Uninitialized;
template<typename _Type>
struct _Uninitialized<_Type, true> {
template<typename... _Args>
constexpr _Uninitialized(in_place_index_t<0>, _Args&&... __args)
: _M_storage(std::forward<_Args>(__args)...) { }
...
_Type _M_storage;
};
template<typename _Type>
struct _Uninitialized<_Type, false> {
template<typename... _Args>
constexpr _Uninitialized(in_place_index_t<0>, _Args&&... __args) {
::new (&_M_storage) _Type(std::forward<_Args>(__args)...);
}
...
__gnu_cxx::__aligned_membuf<_Type> _M_storage;
};
Тут же в следующих строчках появляется предположение, что мы не захотим удалять
_Uninitialzied<T>
, поскольку такая обертка делает тип тривиально разрушаемым в любом случае, независимо от типа T
. Для нетривиального и нелитерального T
в _Uninitialized<T>
используется некий специальный буфер __aligned_membuf
, что резервирует память для объекта и позволяет создать объект позже, по требованию через placement new
.Чуть позже все пошло наперекосяк: в с++17
std::is_literal_type
признали порочной метафункцией, а в с++20 вообще удалили с концами.Разработчики попытались напрямую привязаться к
is_trivially_destructible_v
:template<typename _Type, bool = std::is_trivially_destructible_v<_Type>>
struct _Uninitialized;
Эта конструкция гарантированно является тривиально разрушаемым типом, даже если
T
таковым не является. Это работает для с++17, а в следующей версии стандарта оказалось, что это свойство не важно для _Variadic_union
. Была даже попытка реализовать структуру _Uninitialized
через union
, просто прицепив пользовательский деструктор, но выглядело это как челюсти Чужого. В с++20 легче же дописать пользовательский деструктор прямо для _Variadic_union
, чем упражняться с шаблонами.constexpr ~_Variadic_union() requires (!__trivially_destructible) { }
Если один из типов нетривиально разрушаемый, тогда добавляем деструктор через концепт
__trivially_destructible
. На самом деле мы разрушим объект уровнем выше в функции _Variant_storage::_M_reset()
, единообразия для. Зато с _Uninitialized
можно не заморачиваться и оставить самый простой вариант для всех типов.👍3🤔1
interview.part1
Наконец-то! Пригласили посмотреть офис большой китайской компании Ойвэй. Насчет интервью я особо не беспокоился, беседовал с несколькими командами и все проходило отлично. Однако оказалось, что не все команды умеют собеседовать одинаково хорошо.
Началось все приемлемо, встретили меня почти сразу. Лифт поднял нас на 13 этаж. Как только мы вышли, сопровождающая моя, заговорщицки подмигивая, прошептала, что ждут нас на 14. Вздохнув об ушедшей кабине, я поплелся вверх по лестнице. После того, как я оставил автограф в книге почетных гостей (в паспорт не смотрели, поэтому написал: "Довлатов С.Д."), меня без лишний церемоний впихнули в просторный зал для демонстраций, где на стенах висели огромные интерактивные доски, а под потолком были прибиты несколько проекторов. На полу уже сидели двое. Не вставая с места они поздоровались безжизненным голосом, я тоже поприветствовал их и сказал, что очень приятно. Хотя было не очень. Приземлившись рядом с новыми знакомыми, мы начали молчать. Изредка в зал вползали новые участники встречи и, уже не здороваясь, занимали лучшие места у батарей.
Через некоторое время в зал вихрем ворвался Руководитель с ноутбуком на голове.
- Вы работали с FreeRTOS? - спросил он вместо приветствия.
Я испытал глухое раздражение.
- Работал.
Насколько я понял, их проект с ОСВР не был связан и не планировал связываться.
- Тогда сможете сказать, что это такое?
Я поймал на себе иронический взгляд. Очевидно, резюме тут заранее не читали, а написанному не верили. Вдруг, мол, самозванец...
- Давайте, - не выдержал я, - прекратим этот идиотский экзамен. Я прочитал всего Таненбаума (тут я немного преувеличил, ограничился только "Современными операционными системами"). В общем, разбираюсь...
- И все-таки? - Руководитель ждал ответа. Причем того ответа, который ему был заранее известен.
- Ладно, - говорю, - попробую... Что ж, хоть и лучшей стратегией для построения архитектуры и является SuperLoop на bare-metal, однако порой кооперативной многозадачности становится явно недостаточно для поставленных задач, например, для управления сложными производственными процессами. Тут нужны именно операционные системы, для которых время является ключевым параметром. Вот тут нам и пригодится фриртос...
- При чем тут супер луп? И причем тут баре-метал?
- Ни при чем! - окончательно взбесился я. - И баре-метал ни при чем, а Фриртосом звали одного из мушкетеров: Атос, Портос и Фриртос.
- Успокойтесь, - прошептал Руководитель направления, - вы какой-то нервный... Давайте задачку порешаем, а?
- Задачку?! - взревел я, - А какую? - спросил я уже спокойным голосом.
Однако выражение лица решил сохранять прежнее - человека, внезапно унюхавшего кучу дерьма. Бывает, что с первой фразы уже по интонация четко определяешь, сработаешься ли с командой. Если нет, то соглашаться даже на жирный оффер не стоит. Иначе через полгода вы будете гоняться за ними по узким коридорам с шокером. А может они за вами...
A suivre.
Наконец-то! Пригласили посмотреть офис большой китайской компании Ойвэй. Насчет интервью я особо не беспокоился, беседовал с несколькими командами и все проходило отлично. Однако оказалось, что не все команды умеют собеседовать одинаково хорошо.
Началось все приемлемо, встретили меня почти сразу. Лифт поднял нас на 13 этаж. Как только мы вышли, сопровождающая моя, заговорщицки подмигивая, прошептала, что ждут нас на 14. Вздохнув об ушедшей кабине, я поплелся вверх по лестнице. После того, как я оставил автограф в книге почетных гостей (в паспорт не смотрели, поэтому написал: "Довлатов С.Д."), меня без лишний церемоний впихнули в просторный зал для демонстраций, где на стенах висели огромные интерактивные доски, а под потолком были прибиты несколько проекторов. На полу уже сидели двое. Не вставая с места они поздоровались безжизненным голосом, я тоже поприветствовал их и сказал, что очень приятно. Хотя было не очень. Приземлившись рядом с новыми знакомыми, мы начали молчать. Изредка в зал вползали новые участники встречи и, уже не здороваясь, занимали лучшие места у батарей.
Через некоторое время в зал вихрем ворвался Руководитель с ноутбуком на голове.
- Вы работали с FreeRTOS? - спросил он вместо приветствия.
Я испытал глухое раздражение.
- Работал.
Насколько я понял, их проект с ОСВР не был связан и не планировал связываться.
- Тогда сможете сказать, что это такое?
Я поймал на себе иронический взгляд. Очевидно, резюме тут заранее не читали, а написанному не верили. Вдруг, мол, самозванец...
- Давайте, - не выдержал я, - прекратим этот идиотский экзамен. Я прочитал всего Таненбаума (тут я немного преувеличил, ограничился только "Современными операционными системами"). В общем, разбираюсь...
- И все-таки? - Руководитель ждал ответа. Причем того ответа, который ему был заранее известен.
- Ладно, - говорю, - попробую... Что ж, хоть и лучшей стратегией для построения архитектуры и является SuperLoop на bare-metal, однако порой кооперативной многозадачности становится явно недостаточно для поставленных задач, например, для управления сложными производственными процессами. Тут нужны именно операционные системы, для которых время является ключевым параметром. Вот тут нам и пригодится фриртос...
- При чем тут супер луп? И причем тут баре-метал?
- Ни при чем! - окончательно взбесился я. - И баре-метал ни при чем, а Фриртосом звали одного из мушкетеров: Атос, Портос и Фриртос.
- Успокойтесь, - прошептал Руководитель направления, - вы какой-то нервный... Давайте задачку порешаем, а?
- Задачку?! - взревел я, - А какую? - спросил я уже спокойным голосом.
Однако выражение лица решил сохранять прежнее - человека, внезапно унюхавшего кучу дерьма. Бывает, что с первой фразы уже по интонация четко определяешь, сработаешься ли с командой. Если нет, то соглашаться даже на жирный оффер не стоит. Иначе через полгода вы будете гоняться за ними по узким коридорам с шокером. А может они за вами...
A suivre.
👍10💯2
interview.part2
Человек слаб. Стоит ему увидеть кликбейтный заголовок "Переложи одну спичку так, чтоб..." или "Продолжи последовательность", как тут же все важные дела забыты, зато туда-сюда перекидывается несчастная спичка, и многострадальная последовательность препарируется на предмет закономерности. Так и на собесе совершенно невозможно отказаться от задачи, ведь она словно игрушка в шоколадном яйце. Каждый раз надеешься найти что-то интересное, но сейчас не повезло. Оказалось, надо было всего лишь обнаружить цикл в односвязном списке.
"Наверняка взял из сборника 1000 занимательных алгоритмов", - скривившись подумал я, - "А когда-то волны Зоммерфельда рассчитывал, как низко я пал..."
Я уныло посмотрел на ноут, стоявший перед Руководителем. Он, перехватив мой взгляд, быстро пододвинул устройство к себе. Бумаги тоже не нашлось, зато интерактивных досок было в избытке. Запишем элемент списка:
Тут, говорю с видом знатока, классическая проблема обедающих в маршрутке философов. Им передают за проезд купюру, они передают ее друг другу по кругу. Потом к ним приходит куча мелочи, которую пересыпать тяжелее, поэтому ее они передают друг другу медленнее. Рано или поздно купюра и мелочь окажется в руках у одного из философов, он, раззява, все уронит. По звону монет мы поймем, что цикл есть:
Еще не успев дописать решение, я заметил, что некоторые стали откровенно скучать и уткнулись в смартфоны.
"Вообще-то, можно обойтись без второго указателя," - громко сказал я, чтоб стало еще веселее. Народ перестал тупить в телефон, а кто-то, кажется, описался от неожиданности.
Поскольку адреса будут выровнены по размеру указателя, то как минимум два последних бита будут нулевые всегда. Поэтому их можно использовать, чтоб пометить пройденный узел. Встретив такой узел, поймем, что цикл есть.
Сварганим небольшой класс:
Функция
Эта функция проверяет не достигнут ли конец списка, если нет, то проверяет, помечен ли узел. Если не помечен, то зацикленности не обнаружено, и можно вызвать
Руководитель направления сполз под стол и сомлел. Двое его подручных корчились на полу. Еще один в слезах убежал звать охрану.
"Пора удирать", - подумал я и был таков.
Человек слаб. Стоит ему увидеть кликбейтный заголовок "Переложи одну спичку так, чтоб..." или "Продолжи последовательность", как тут же все важные дела забыты, зато туда-сюда перекидывается несчастная спичка, и многострадальная последовательность препарируется на предмет закономерности. Так и на собесе совершенно невозможно отказаться от задачи, ведь она словно игрушка в шоколадном яйце. Каждый раз надеешься найти что-то интересное, но сейчас не повезло. Оказалось, надо было всего лишь обнаружить цикл в односвязном списке.
"Наверняка взял из сборника 1000 занимательных алгоритмов", - скривившись подумал я, - "А когда-то волны Зоммерфельда рассчитывал, как низко я пал..."
Я уныло посмотрел на ноут, стоявший перед Руководителем. Он, перехватив мой взгляд, быстро пододвинул устройство к себе. Бумаги тоже не нашлось, зато интерактивных досок было в избытке. Запишем элемент списка:
struct Node {
Node *next;
};
Тут, говорю с видом знатока, классическая проблема обедающих в маршрутке философов. Им передают за проезд купюру, они передают ее друг другу по кругу. Потом к ним приходит куча мелочи, которую пересыпать тяжелее, поэтому ее они передают друг другу медленнее. Рано или поздно купюра и мелочь окажется в руках у одного из философов, он, раззява, все уронит. По звону монет мы поймем, что цикл есть:
bool check_boring(Node *node) {
bool is_cycled = false;
std::pair<Node *, Node *> ptr {node, node};
bool step = false;
while (ptr.first && !is_cycled) {
ptr.first = ptr.first->next;
ptr.second = step ? ptr.second->next : ptr.second;
step = !step;
is_cycled = ptr.first == ptr.second;
}
return is_cycled;
}
Еще не успев дописать решение, я заметил, что некоторые стали откровенно скучать и уткнулись в смартфоны.
"Вообще-то, можно обойтись без второго указателя," - громко сказал я, чтоб стало еще веселее. Народ перестал тупить в телефон, а кто-то, кажется, описался от неожиданности.
Поскольку адреса будут выровнены по размеру указателя, то как минимум два последних бита будут нулевые всегда. Поэтому их можно использовать, чтоб пометить пройденный узел. Встретив такой узел, поймем, что цикл есть.
Сварганим небольшой класс:
template <class T> struct MarkPointer {
using Pointer = T *;
MarkPointer(Pointer *ptr) : ptr_{ptr} {}
~MarkPointer() { *ptr_ = reinterpret_cast<Pointer>((AsNumber() >> 1) << 1); }
bool IsEnd() const { return *ptr_ == nullptr; }
bool IsMarked() const { return (AsNumber() & 1) != 0; }
uintptr_t AsNumber() const { return reinterpret_cast<uintptr_t>(*ptr_); }
MarkPointer MarkAndNext() const {
Pointer *next = &((*ptr_)->next);
*ptr_ = reinterpret_cast<Pointer>(AsNumber() | 1U);
return {next};
}
Pointer *ptr_;
};
MarkPointer
принимает указатель на объект типа Pointer
, потому как мы вознамерились этот объект менять. Если Pointer
указывает на nullptr
, то мы нашли конец списка, метод IsMarked
определяет, помечен узел или нет. Самый интересный метод MarkAndNext
- порождает новый объект на основе указателя на следующий узел списка, одновременно помечая текущий узел. Теперь, когда у нас есть такой мощный объект, осталось только правильно его применить.template <class T> bool check(T *ptr) {
MarkPointer p {&ptr};
return check_impl(p);
}
Функция
check
создаст обертку для первого элемента в списке и вернет результат функции check_impl
.template <class T> bool check_impl(MarkPointer<T> const &pointer) {
return !pointer.IsEnd() && (pointer.IsMarked() || check_impl(pointer.MarkAndNext()));
}
Эта функция проверяет не достигнут ли конец списка, если нет, то проверяет, помечен ли узел. Если не помечен, то зацикленности не обнаружено, и можно вызвать
check_impl
для следующего элемента списка. Да, рекурсивно. Нет, адреса не будут испорчены после этого. Деструктор MarkPointer
уберет нашу метку, вернув ваш драгоценный и бесполезный список в исходное состояние.Руководитель направления сполз под стол и сомлел. Двое его подручных корчились на полу. Еще один в слезах убежал звать охрану.
"Пора удирать", - подумал я и был таков.
😁6👍5✍1
std::is_literal_type
Литералы появилось очень давно, наверное, еще во времена альбигойских войн. Вот литеральные типы - изобретение относительно свежее, появившееся только в стандарте с++11. В одном из последних черновиков стандарта в главе
Оказывается, литеральными считается и тип
Фух, в примечании к этому многословному определению человеческим языком поясняется, что литеральный тип — это такой тип, объект которого можно создать в константном выражении. Впрочем, гарантий такое определение не дает.
Другими словами, мы ожидаем, что литеральный тип можно использовать в
Праздник метапрограммирования нам испортил некий мистер Alisdair Meredith, написав в 2016 году работу Deprecating Vestigial Library Parts in C++17. В разделе Deprecate the is_literal Trait он потребовал убрать этот трейт из стандартной библиотеки.
"Уберите же класс свойств
Однако даже автор признает, что текущая реализация этой метафункции вреда не приносит, и можно ее оставить, пока комитет не примет решение об окончательном удалении. Работал же когда-то
Через два года, в 2018, наш знакомец в составе группы лиц (Alisdair Meredith, Stephan T. Lavavej, Tomasz Kamiński) по предварительному сговору опубликовали еще одну работу Reviewing Deprecated Facilities of C++17 for C++20, где таки вознамерились удалить совсем все малополезные классы свойств в том числе и
В качестве обоснования они написали, что классы имели ненадежные или неудобные интерфейсы. Метафункция
И могло получиться так, что программа, которая полагается на свойства класса
Допустим, есть у нас класс литерального типа:
Да, все требования литеральности класс соблюдает: деструктор тривиальный, тип члена
Можем ли мы тогда определить такую
Да, но есть нюанс. Если мы попытаемся использовать ее во время компиляции, то получим ошибку, о чем нас и предупреждали.
Ежели здесь мы не можем положиться на
В общем, определение литерального типа есть в стандарте, поэтому в языке еще существует
Литералы появилось очень давно, наверное, еще во времена альбигойских войн. Вот литеральные типы - изобретение относительно свежее, появившееся только в стандарте с++11. В одном из последних черновиков стандарта в главе
6.8 Types
, разделе 6.8.1 General [basic.types.general]
дается пространное и исчерпывающее определение литерального типа.Оказывается, литеральными считается и тип
void
, и скалярные, ссылочные типы, и даже массивы литеральных типов. Также литеральными могут стать классы, если им сделать тривиальный или constexpr
деструктор (есть и такой в с++20), и если все нестатические члены класса, а также базовые классы имеют литеральный тип. Сюда в с++17 попадают лямбды. На иные классы накладываются ограничения: объединения должны иметь хотя бы один член литерального типа. Если обычный класс имеет вариантные члены, то они должны удовлетворять вышеизложенным требованиям. Самое главное, что класс должен иметь хотя бы один constexpr
конструктор или шаблонный конструктор. Копирующий или перемещающий конструктор не в счет.Фух, в примечании к этому многословному определению человеческим языком поясняется, что литеральный тип — это такой тип, объект которого можно создать в константном выражении. Впрочем, гарантий такое определение не дает.
Другими словами, мы ожидаем, что литеральный тип можно использовать в
constexpr
выражениях. Если признаки литеральности известны, то почему бы не сделать соответствующую метафункцию std::is_literal_type
, которую можно было бы использовать в обобщенном программировании.Праздник метапрограммирования нам испортил некий мистер Alisdair Meredith, написав в 2016 году работу Deprecating Vestigial Library Parts in C++17. В разделе Deprecate the is_literal Trait он потребовал убрать этот трейт из стандартной библиотеки.
"Уберите же класс свойств
is_literal
!" - грозно кричал он со страниц статьи. - "Класс свойств is_literal
имеет исчезающе малое значение для метапрограммирования, поскольку на самом деле мы хотим быть уверены, что конкретная конструкция инициализируется константно. Определение литерального типа, где требуется наличие хотя бы одного constexpr
конструктора, слишком слабо, чтобы его можно было использовать осмысленно."Однако даже автор признает, что текущая реализация этой метафункции вреда не приносит, и можно ее оставить, пока комитет не примет решение об окончательном удалении. Работал же когда-то
std::variant
с этом внутри и ничего, не треснул.Через два года, в 2018, наш знакомец в составе группы лиц (Alisdair Meredith, Stephan T. Lavavej, Tomasz Kamiński) по предварительному сговору опубликовали еще одну работу Reviewing Deprecated Facilities of C++17 for C++20, где таки вознамерились удалить совсем все малополезные классы свойств в том числе и
is_literal_type
.В качестве обоснования они написали, что классы имели ненадежные или неудобные интерфейсы. Метафункция
is_literal_type
вовсе не предоставляла возможности определить, какое подмножество конструкторов и функций-членов типа было объявлено constexpr
.И могло получиться так, что программа, которая полагается на свойства класса
is_literal_type
или на шаблон is_literal_type_v
, может не скомпилироваться.Допустим, есть у нас класс литерального типа:
struct A {
constexpr A() : m{} {}
A(int i) : m{i} {}
int m;
};
Да, все требования литеральности класс соблюдает: деструктор тривиальный, тип члена
m
литеральный, constexpr
конструктор в наличии. static_assert(std::is_literal_type_v<A> == true);
Можем ли мы тогда определить такую
constexpr
функцию?constexpr int FUNC(int i) {
A a{i};
return i + a.m;
}
Да, но есть нюанс. Если мы попытаемся использовать ее во время компиляции, то получим ошибку, о чем нас и предупреждали.
static_assert(FUNC(1) == 2);
Ежели здесь мы не можем положиться на
is_literal_type
, то зачем оно надо?В общем, определение литерального типа есть в стандарте, поэтому в языке еще существует
std::is_literal_type
, однако в с++20 он официально признан удаленным, поэтому искать его следует в разделе 16.4.5.3.2 Zombie names [zombie.names].
Такой вот, понимаешь, кадавр.👍3🤔1
std::__parse_int
Если вам кажется, что вы занимаетесь полной ерундой на никому не нужном, богом забытом проекте в арьергарде технологического декаданса, не грустите. Вспомните, что в GCC есть файл
Соответствующий класс объявлен так:
Здесь
Например, если в пакете у нас
Все эти архиважные вычисления происходят во время компиляции, что невероятно удобно, но странно.
"Однако же, какая на редкость бесполезная метафункция!" - подумал я, когда впервые увидел это чудо. Потом задумался. В плюсах ни одна дичь не творится просто так. Разработчики GCC кринжатины не навалят перед дверью за здорово живешь, по недомыслию, только холодный расчет!
Стандарт же на вопрос: "Оно тут зачем?", отвечает уклончиво.
В разделе
Шаблонный числовой литеральный оператор — это такой шаблон, список параметров которого содержит единственный параметр, но не типовой, а пакет элементов типа
То есть если мы хотим сделать пользовательский литерал для чисел, то сделать его в шаблонном виде можно только в такой форме:
Для строк в с++20 появится своя форма шаблонного литерального оператора, но это уже другая история.
Тогда при попытке создать литерал
Чувствуете, куда ветер ветку клонит, чего дубравушка шумит? Тут может пригодиться
Для временных литералов может быть полезно проверять значение на осмысленность. Допустим, мы хотим сделать литерал для обозначения часов в сутках.
К сожалению, больше 24 часов в сутках быть не может, поэтому литеральный оператор запишем следующим образом:
Теперь если мы попытаемся создать литерал
А еще
В общем, не такая уж и бесполезная функция. Особенно для любителей создавать свои типы физических единиц, что, конечно, правильно и одобряется AUTOSAR.
Очень полезная функция, полезнее меня на этом багом забитом проекте...
Если вам кажется, что вы занимаетесь полной ерундой на никому не нужном, богом забытом проекте в арьергарде технологического декаданса, не грустите. Вспомните, что в GCC есть файл
bits/parse_numbers.h
интереснейшего содержания. Там размазана на пару сотен строк матафункция _Parse_int
, которая существует исключительно для того, чтоб получать числа из набора символов.Соответствующий класс объявлен так:
template<char... _Digs>
struct _Parse_int;
Здесь
_Digs
- это аргумент шаблона, пакет значений char
из которых и состоит искомое число.Например, если в пакете у нас
'1'
и '0'
, то эта хитрая бестия слепит число 10
.using _Val = std::__parse_int::_Parse_int<'1', '0'>;
static_assert(_Val::value == 10U); // OK
Все эти архиважные вычисления происходят во время компиляции, что невероятно удобно, но странно.
"Однако же, какая на редкость бесполезная метафункция!" - подумал я, когда впервые увидел это чудо. Потом задумался. В плюсах ни одна дичь не творится просто так. Разработчики GCC кринжатины не навалят перед дверью за здорово живешь, по недомыслию, только холодный расчет!
Стандарт же на вопрос: "Оно тут зачем?", отвечает уклончиво.
В разделе
12.6 User-defined literals [over.literal]
, стих 5 можно найти такие чарующие строки:Шаблонный числовой литеральный оператор — это такой шаблон, список параметров которого содержит единственный параметр, но не типовой, а пакет элементов типа
char
.То есть если мы хотим сделать пользовательский литерал для чисел, то сделать его в шаблонном виде можно только в такой форме:
template<char ... _Digits>
double operator ""_x();
Для строк в с++20 появится своя форма шаблонного литерального оператора, но это уже другая история.
Тогда при попытке создать литерал
_x
мы добьемся вызова оператора operator""_x()
:10_x -> double operator""_x() [with char ..._Digits = {'1', '0'}]
Чувствуете, куда ветер ветку клонит, чего дубравушка шумит? Тут может пригодиться
_Parse_int
! И уже пригодилось. В GCC мы можем найти использование этой метафункции в bits/chrono.h
.Для временных литералов может быть полезно проверять значение на осмысленность. Допустим, мы хотим сделать литерал для обозначения часов в сутках.
struct Hour {
uint32_t value;
};
К сожалению, больше 24 часов в сутках быть не может, поэтому литеральный оператор запишем следующим образом:
template <char... _Digits>
constexpr Hour operator""_Н() {
using _Val = std::__parse_int::_Parse_int<_Digits...>;
static_assert(_Val::value < 24U, "literal value is wrong!");
return Hour {_Val::value};
}
Теперь если мы попытаемся создать литерал
10_H
, то все пройдет гладко, но стоит написать 36_H
, как все рухнет:error: static assertion failed: literal value is wrong!
static_assert(_Val::value < 24U, ...
А еще
_Parse_int
понимает разные формы записи. 10_H
и 0xA_H
даст одно и то же значение.В общем, не такая уж и бесполезная функция. Особенно для любителей создавать свои типы физических единиц, что, конечно, правильно и одобряется AUTOSAR.
Очень полезная функция, полезнее меня на этом багом забитом проекте...
👍3🤔2
std::__select_int::_Select_int_base
"Загадывай свою загадку, я готов!" - плюхнувшись на стул, заносчиво произнес разработчик Ефпидрифий.
Ему не терпелось завершить аттестацию и впрыгнуть в новый грейд ведущего разработчика категории 5 подкатегории 3. Это был бы головокружительный скачок на целых две подкатегории!
Напротив него сидела товарищ Сфинкс. Абсолютно неподвижная, словно статуя, сквозь каменный покерфейс которой не решалась показаться ни одна эмоция.
"Хорошо", - отчеканила Сфинкс, - "скажите мне, какой тип следует использовать для переменной индекса?"
"Ну, это же очевидно!.." - рассмеялся разработчик и начал свой рассказ.
С его слов, ответ на этот вопрос таится в
Очевидно, для
Однако, если напихать в список еще
Здравый смысл подсказывает, что тип индекса стал
Как он это делает? Что скрывает в своей ненасытной утробе?
Опять же, ноги растут из якобы бесполезного
Его самая интересная частичная специализация:
Выражение
В самом
Значение имеет только количество типов в шаблонных аргументах. Здесь
На самом же деле, главная метафункция здесь
То есть можно во время компиляции определить подходящий тип для переменной:
Может быть, кому-то это пригодится.
Сфинкс молча, ликуя и скорбя, смотрела на Ефпидрифия. Она только вчера прочитала об этом трюке в малоизвестном телеграм-канале и не ожидала правильного ответа. Теперь придется исполнять со скалы бейсджампинг. Таков был уговор. Разработчик же хитро щурился, ему очень повезло, что буквально вчера он подписался на один малоизвестный телеграм-канал.
"Загадывай свою загадку, я готов!" - плюхнувшись на стул, заносчиво произнес разработчик Ефпидрифий.
Ему не терпелось завершить аттестацию и впрыгнуть в новый грейд ведущего разработчика категории 5 подкатегории 3. Это был бы головокружительный скачок на целых две подкатегории!
Напротив него сидела товарищ Сфинкс. Абсолютно неподвижная, словно статуя, сквозь каменный покерфейс которой не решалась показаться ни одна эмоция.
"Хорошо", - отчеканила Сфинкс, - "скажите мне, какой тип следует использовать для переменной индекса?"
"Ну, это же очевидно!.." - рассмеялся разработчик и начал свой рассказ.
С его слов, ответ на этот вопрос таится в
std::variant
, словно игла в яйце. Этот тип содержит список типов, который формируется во время компиляции, и ему остро необходим индекс, чтоб знать, объект какого тип сейчас на коне. Этот индекс, он же тэг, имеет весьма хитрое объявление.Очевидно, для
std::variant<char, char>
, все значения тэга лежат в диапазоне от нуля до единицы. Тут в качестве типа подойдет uint8_t
.using VariantCharType = std::variant<char, char>;
static_assert(sizeof(VariantCharType) == 2);
Однако, если напихать в список еще
char
, чтоб их стало больше 256, то std::variant
просто увеличится в размерах.using VariantChar256Type = std::variant<Types256>; // Types256 это 256 char-ов
static_assert(sizeof(VariantChar256Type) == 4);
Здравый смысл подсказывает, что тип индекса стал
uint16_t
, а из-за выравнивания размер скакнул в два раза.Как он это делает? Что скрывает в своей ненасытной утробе?
Опять же, ноги растут из якобы бесполезного
bits/parse_numbers.h
. Есть там один небезынтересный шаблон _Select_int_base
для определения подходящего численного типа.Его самая интересная частичная специализация:
template<unsigned long long _Val, typename _IntType, typename... _Ints>
struct _Select_int_base<_Val, _IntType, _Ints...>
: conditional_t<(_Val <= std::numeric_limits<_IntType>::max()),
integral_constant<_IntType, _Val>,
_Select_int_base<_Val, _Ints...>> {};
Выражение
conditional_t<(_Val <= std::numeric_limits<_IntType>::max())...
четко указывает, что если число _Val
убирается в диапазон типа _IntType
, то выбираем его, если нет, то выбрасываем этот тип и повторяем итерацию. Поэтому важно, чтоб возможные типы шли от самого узкого до самого широкого.В самом
std::variant
все сделано правильно:template <typename... _Types>
using __select_index =
typename __select_int::_Select_int_base<sizeof...(_Types),
unsigned char,
unsigned short>::type::value_type;
Значение имеет только количество типов в шаблонных аргументах. Здесь
__select_int
выбирает между uint8_t
и uint16_t
, если типов меньше 256, то первый тип, иначе второй. Больше вариантов нет. Разработчики справедливо считают, что если вы попытаетесь запихать в параметры больше 65535 типов, то что-то не в порядке, как минимум с головой.На самом же деле, главная метафункция здесь
_Select_int
, ее предполагалось использовать в литералах.template<char... _Digs>
using _Select_int;
То есть можно во время компиляции определить подходящий тип для переменной:
using T100 = std::__select_int::_Select_int<'1','0','0'>::type::value_type;
static_assert(std::is_same_v<T100, uint8_t>);
using T1000 = std::__select_int::_Select_int<'1','0','0','0'>::type::value_type;
static_assert(std::is_same_v<T1000, uint16_t>);
Может быть, кому-то это пригодится.
Сфинкс молча, ликуя и скорбя, смотрела на Ефпидрифия. Она только вчера прочитала об этом трюке в малоизвестном телеграм-канале и не ожидала правильного ответа. Теперь придется исполнять со скалы бейсджампинг. Таков был уговор. Разработчик же хитро щурился, ему очень повезло, что буквально вчера он подписался на один малоизвестный телеграм-канал.
🔥5👍2🤔1
No сountry for negative integer literals
Представьте, связывается с вами солидная компания ЛузерДейт, зовут поговорить. Потом пропадают. Вы про них забываете и спокойно ковыряетесь в коде. Потом всплывают неожиданно, как тигровая какула в бассейне, и тянут уже на собеседование. И вот, на следующий день ровно в 6 утра начинается экзекуция.
Конечно, будет задача. Давай, говорят, напиши нам как реализовать функцию
"Входные данные - это затрапезная си строка?" - уточняю я.
"Нул-терминэйтед си стринг", - щеголяет безукоризненным английским собеседник.
Одним движением мозолистой руки набираю я.
"Нет!" - доносится визг из наушников, - "на входе не
"А вы не заболели?" - рассуждаю я вслух, - "как потом этим пользоваться?"
Ладно, у них какие-то свои соображения, меняем аргумент
Тут глаза интервьюеров подозрительно прищурились, они почуяли кровь. Как, говорят, вы проверите входной диапазон?
Допустим, вот так
"Ага!" - взвились, как коршуны, мои собеседники, и, судя по звукам, стали скакать по столу, - "а тип-то разыменованной переменной беззнаковый! А если ее значение меньше? Видите проблему?"
Вот теперь я отчетливо видел проблему. Забыли они о явлении
Когда все успокоились, мы перешли к следующей задаче. Что покажет следующая проверка?
Покажет только одно - ваше присутствие на интервью нежелательно.
На самом деле выражение пройдет проверку. За разъяснениями опять обратимся к стандарту.
Судя по ней, целочисленные литералы в десятичном виде без суффикса сначала пробует стать
Целочисленные литералы без суффикса не в десятичном представлении после неудачной попытки стать
Поэтому
хотя это одно и то же число.
И еще деталь. Негативных целочисленных литералов не бывает! Это минус впереди указывает, что нужно применить унарный минус к значению литерала. Тут уже
Конечно, компания имеет полное право задавать любые вопросы и считать верными какие угодно ответы. Я не осуждаю, ведь главная цель собеседования - узнать насколько адекватна контора и люди, там работающие.
Однако это мне уже давно не стыдно за проваленные собеседования. Могу морозить чушь, как холодильник Снайге. Возможно, мне скучно отвечать, и я думаю о чем-нибудь другом, утопая в дальнем дорогом. Имею право что-то забыть и невразумительно мычать. В конце концов я даже не готовлюсь, и это привилегия соискателя. Чего интервьюеры никак не могут себе позволить.
Представьте, связывается с вами солидная компания ЛузерДейт, зовут поговорить. Потом пропадают. Вы про них забываете и спокойно ковыряетесь в коде. Потом всплывают неожиданно, как тигровая какула в бассейне, и тянут уже на собеседование. И вот, на следующий день ровно в 6 утра начинается экзекуция.
Конечно, будет задача. Давай, говорят, напиши нам как реализовать функцию
atoi
, которая перевела бы строку в число."Входные данные - это затрапезная си строка?" - уточняю я.
"Нул-терминэйтед си стринг", - щеголяет безукоризненным английским собеседник.
int lovely_atoi(const char *ptr) {
int result = 0;
while (*ptr) {
int dig = *ptr - '0';
result *= 10;
result += dig;
ptr++;
}
return result;
}
Одним движением мозолистой руки набираю я.
"Нет!" - доносится визг из наушников, - "на входе не
char
, а uint8_t
!""А вы не заболели?" - рассуждаю я вслух, - "как потом этим пользоваться?"
Ладно, у них какие-то свои соображения, меняем аргумент
int lovely_atoi(const uint8_t *ptr)
Тут глаза интервьюеров подозрительно прищурились, они почуяли кровь. Как, говорят, вы проверите входной диапазон?
Допустим, вот так
int dig = *ptr - '0';
if (dig < 0 || dig > 9) {
panic_at_the_disco();
}
"Ага!" - взвились, как коршуны, мои собеседники, и, судя по звукам, стали скакать по столу, - "а тип-то разыменованной переменной беззнаковый! А если ее значение меньше? Видите проблему?"
Вот теперь я отчетливо видел проблему. Забыли они о явлении
integral promotion
! Все значения в этом выражении будут неявно преобразованы в int
, и результат будет такой же. Никакого криминала тут нет, о чем забыли достопочтенные господа, прыгающие в тот момент по предметам мебели.Когда все успокоились, мы перешли к следующей задаче. Что покажет следующая проверка?
static_assert(0x80000000 == -0x80000000); // ?
Покажет только одно - ваше присутствие на интервью нежелательно.
На самом деле выражение пройдет проверку. За разъяснениями опять обратимся к стандарту.
Раздел 5.13.2 Integer literals [lex.icon]
подскажет нам, что тип целочисленного литерала выбирается согласно таблице Table 8: Types of integer-literals
.Судя по ней, целочисленные литералы в десятичном виде без суффикса сначала пробует стать
int
, а потом long int
и т.д.Целочисленные литералы без суффикса не в десятичном представлении после неудачной попытки стать
int
, пробуют себя в unsigned int
, затем в long int
и т.п.Поэтому
static_assert(std::is_same_v<decltype(0x80000000), uint32_t>);
static_assert(std::is_same_v<decltype(2147483648), int64_t>);
хотя это одно и то же число.
И еще деталь. Негативных целочисленных литералов не бывает! Это минус впереди указывает, что нужно применить унарный минус к значению литерала. Тут уже
7.6.2.2 Unary operators
говорит нам, как действовать. В общем, в результате получается 0x80000000
, поэтому изначальная проверка будет пройдена. Жаль, что не все верят в такое объяснение.Конечно, компания имеет полное право задавать любые вопросы и считать верными какие угодно ответы. Я не осуждаю, ведь главная цель собеседования - узнать насколько адекватна контора и люди, там работающие.
Однако это мне уже давно не стыдно за проваленные собеседования. Могу морозить чушь, как холодильник Снайге. Возможно, мне скучно отвечать, и я думаю о чем-нибудь другом, утопая в дальнем дорогом. Имею право что-то забыть и невразумительно мычать. В конце концов я даже не готовлюсь, и это привилегия соискателя. Чего интервьюеры никак не могут себе позволить.
👍6🤣4💯1