Привет, я Антон Полухин из Техплатформы Екома и Райдтеха Яндекса. Моя команда разрабатывает userver — современный опенсорсный асинхронный фреймворк с богатым набором абстракций для быстрого и комфортного создания микросервисов, сервисов и утилит на C++.

Когда мы пишем какой‑то код для userver и для таких сложных проектов, как Boost, периодически мы сталкиваемся с нестандартными проблемами. И эти нестандартные проблемы требуют нестандартных решений. Вот о таких решениях мы сегодня и поговорим.

А именно:

  • Посмотрим, как работают исключения на платформе Linux x86, и сделаем с ними что‑то интересное.

  • Залезем ещё глубже под капот исключений и сделаем их ещё быстрее.

  • Сделаем висячую ссылку на невалидный объект, и всё будет хорошо.

  • А под конец то, что все любим, — погрузимся в шаблонное метапрограммирование.


Исключения

Во что компилятор превращает ключевое слово throw? Напишем функцию test, напишем внутри него throw, выкинем число как исключение.

void test(int i) {
    throw i;
}

Не пишите так в реальных проектах, этот код только для осознания того, как работают исключения! 

Давайте посмотрим, во что превратит компилятор написанную функцию. Для этого воспользуемся замечательным сайтом godbolt.org. Поставим флажки для оптимизаций и увидим, что два топовых компилятора GCC и Clang превращают вот эту жалкую одну строчечку throw в целый набор ассемблерных команд, внутри которых аж два вызова функций.

Первая функция называется __cxa_allocate_exception. И как несложно догадаться из названия, она аллоцирует память для исключения.

Наверху, перед вызовом функции, есть число 4 — размер нашего int. Это число — входной параметр функции. После него идёт код с записыванием typeinfo в какие‑то части проаллоцированной памяти и вызов __cxa_throw.

Функция __cxa_allocate_exception располагается в C++ Runtime на Linux‑платформах. То есть в библиотеке, которая линкуется со всеми бинарниками, которые написаны на C++.

Реализации у C++ Runtime разные, у каждого компилятора — своя. Посмотрим на реализацию от GCC, она не сильно отличается от clang. И в этой реализации есть интересный момент.

extern void *__cxa_allocate_exception (size_t) _ITM_NOTHROW WEAK;
extern void __cxa_free_exception (void *) _ITM_NOTHROW WEAK;
extern void __cxa_throw (void *, void *, void (*) (void *)) WEAK;

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

extern "C" void *
__cxxabiv1::__cxa_allocate_exception(std::size_t thrown_size) noexcept
{
  thrown_size += sizeof (__cxa_refcounted_exception);

  void *ret = malloc (thrown_size);

#if USE_POOL
  if (!ret)
    ret = emergency_pool.allocate (thrown_size);
#endif

  if (!ret)
    std::terminate ();

  memset (ret, 0, sizeof (__cxa_refcounted_exception));

  return (void *)((char *)ret + sizeof (__cxa_refcounted_exception));
}

__cxa_allocate_exception на самом деле аллоцирует память не только под тело исключения — наш integer —, но ещё заранее резервирует память под какую‑то служебную структуру __cxa_refcounted_exсeption. В неё записываются данные для исключения.

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

Итак, написали throw что‑то. Получили:

  • аллокацию под заголовок исключения и тело исключения;

  • зануление заголовка;

  • возврат указателя на место для тела исключения;

  • по указателю компилятор размещает значение исключения, служебную информацию и вызывает __cxa_throw.

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

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

Подмешиваем stacktrace

Итак, создаём свою функцию __cxa_allocate_exception

extern "C" BOOST_SYMBOL_EXPORT
void* __cxa_allocate_exception(size_t thrown_size) throw() {
  static const auto orig_allocate_exception = []() {
    void* const ptr = ::dlsym(RTLD_NEXT, "__cxa_allocate_exception");
    BOOST_ASSERT_MSG(ptr, "Failed to find '__cxa_allocate_exception'");
    return reinterpret_cast<void*(*)(size_t)>(ptr);
  }();

  if (!boost::stacktrace::impl::ref_capture_stacktraces_at_throw()) {
    return orig_allocate_exception(thrown_size);
  }

#ifndef NDEBUG
  static thread_local std::size_t in_allocate_exception = 0;
  BOOST_ASSERT_MSG(in_allocate_exception < 10, "Suspicious recursion");
  ++in_allocate_exception;
  const decrement_on_destroy guard{in_allocate_exception};
#endif

  static constexpr std::size_t kAlign = alignof(std::max_align_t);
  thrown_size = (thrown_size + kAlign - 1) & (~(kAlign - 1));

  void* const ptr = orig_allocate_exception(thrown_size + kStacktraceDumpSize);
  char* const dump_ptr = static_cast<char*>(ptr) + thrown_size;

  constexpr size_t kSkip = 1;
  boost::stacktrace::safe_dump_to(kSkip, dump_ptr, kStacktraceDumpSize);

#if !BOOST_STACKTRACE_ALWAYS_STORE_IN_PADDING
  if (is_libcpp_runtime()) {
    const std::lock_guard<std::mutex> guard{g_mapping_mutex};
    g_exception_to_dump_mapping[ptr] = dump_ptr;
  } else
#endif
  {
    BOOST_ASSERT_MSG(
      reference_to_empty_padding(ptr) == nullptr,
      "Not zeroed out, unsupported implementation"
    );
    reference_to_empty_padding(ptr) = dump_ptr;
  }

  return ptr;
}

Тут всё просто: имя функции должно совпадать с именем из C++ Runtime. Дальше нам понадобится указатель на изначальную функцию __cxa_allocate_exception. Мы не хотим заморачиваться с какими‑то занулениями памяти, хитрыми аллокациями, подсчётом размера служебных заголовков. Хотим просто позвать изначальную функцию из рантайма C++. Для этого пишем лямбду, которую тут же и вызываем, и внутри неё вызываем метод dlsym.

  static const auto orig_allocate_exception = []() {
    void* const ptr = ::dlsym(RTLD_NEXT, "__cxa_allocate_exception");
    BOOST_ASSERT_MSG(ptr, "Failed to find '__cxa_allocate_exception'");
    return reinterpret_cast<void*(*)(size_t)>(ptr);
  }();

Передаём туда имя функции, которую ищем. Вернётся указатель не на нашу функцию, а на функцию из C++ Runtime. Проверяем, что указатель не нулевой (он всегда должен быть не нулевым), а дальше делаем опять платформо‑специфичную вещь — преобразовываем void* в указатель на функцию.

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

Подправляем размер для аллоцирования и зовём изначальный метод __cxa_allocate_exception с новым размером:

static constexpr std::size_t kAlign = alignof(std::max_align_t);
thrown_size = (thrown_size + kAlign - 1) & (~(kAlign - 1));

void* const ptr = orig_allocate_exception(thrown_size + kStacktraceDumpSize);

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

char* const dump_ptr = static_cast<char*>(ptr) + thrown_size;

constexpr size_t kSkip = 1;
boost::stacktrace::safe_dump_to(kSkip, dump_ptr, kStacktraceDumpSize);

Метод safe_dump_to записывает указатели на фреймы трейса в память по указателю dump_ptr. Никакой символизации там не происходит, в человекочитаемый вид трейс не приводится.

Метод получения исключения

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

Можно кинуть char, и он будет размером 1 байт. Можно кинуть std::string, размер будет побольше. Можно кинуть что‑то размером в десятки килобайт. А чтобы воспользоваться stacktrace, нам надо знать, где начинается кусок памяти, куда мы его записали.

Нужно где‑то этот указатель «прикопать».

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

Нам везёт: в GCC и в Clang есть прям кусок памяти в служебном заголовке размером с указатель, куда нужную нам информацию можно записать. Но есть и плохая новость: в зависимости от того, какой у нас C++ Runtime, это место разбрелось по разным офсетам. То есть придётся немного повозиться, чтобы попасть в нужное неиспользуемое место служебного заголовка.

Есть второй вариант, менее стрёмный. Заводим std::unordered_map и в неё записываем проаллоцированные исключения и местонахождение его stacktrace. Но вариант медленный — std::unordered_map будет аллоцировать память.

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

Полдела сделано — мы записываем трейсы во все исключения. Теперь всего лишь нужно из выкинутого исключения достать stacktrace. Для этого всё в той же библиотеке, где мы подменяем __cxa_allocate_exception, пишем вспомогательную функцию current_exception_stacktrace:

namespace impl {
const char* current_exception_stacktrace() noexcept {
    auto exc_ptr = std::current_exception();
    void* const exc_raw_ptr = get_current_exception_raw_ptr(&exc_ptr);
    if (!exc_raw_ptr) {
        return nullptr;
    }
    return reference_to_empty_padding(exc_raw_ptr);
}
}

В ней вызываем std::current_exception и получаем умный указатель на текущее исключение. С помощью арифметики из него достаём именно тот указатель, который был возвращён из __cxa_allocate_exception. И если всё пошло по плану, мы получаем нужное смещение, где записан указатель на stacktrace, или получаем этот указатель из std::unordered_map. Возвращаем указатель наружу из функции current_exception_stacktrace.

Дальнейшая магия происходит уже в заголовочном файле:

basic_stacktrace from_current_exception(Allocator alloc) noexcept {
    const char* trace = impl::current_exception_stacktrace();
    if (trace) {
        try {
            // Matches the constant from implementation
            constexpr std::size_t kStacktraceDumpSize = 4096;
            return from_dump(trace, kStacktraceDumpSize, alloc);
        } catch (const std::exception&) {
            // ignore
        }
    }
    return basic_stacktrace{0, 0, alloc};
}

Первым делом зовём служебную функцию, получаем указатель на stacktrace. Если трейса нет, возвращаем пустой. Если указатель есть, то данные копируем в объект типа boost::stacktrace, всё ещё не производя символизацию, и возвращаем boost::stacktrace. Теперь у нас есть boost::stacktrace, который содержит stacktrace из исключения.

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

Вот пример работы:

    try {
      foo();
    } catch (const std::exception&) {
      auto trace = boost::stacktrace::stacktrace::from_current_exception();
      std::cout << "Trace: " << trace << '\n';
    }

 А это результат:

Trace:
 0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
 1# bar(std::string_view) at /home/axolm/basic.cpp:6
 2# foo() at /home/axolm/basic.cpp:87
 3# main at /home/axolm/basic.cpp:17

Полученный механизм весьма удобен, если вы, скажем, пользуетесь Continuous Integration. Когда какой‑то тест проваливается, из него вылетает исключение, попробуй разберись, откуда оно вылетело и почему так произошло. И зачастую воспроизвести эту ситуацию на локальной машине весьма нетривиально. Добавляете stacktrace, печатаете все исключения, которые вылетели из неожиданных мест — и готово, у вас сильно упростилась отладка.

Выглядит решение страшновато и сложно? Хорошая новость. Всё это реализовано для платформы Linux в Boost, начиная с версии 1.85. Для платформы Windows всё совершенно по‑другому. Там исключения аллоцируются на стеке, и происходит совсем другая магия. Она доступна в Boost начиная с версии 1.86.

А тем временем мы переходим к холиварной теме.

Что лучше — исключение или коды возврата? 

Давайте разбираться. Пишем два кусочка кода.

В одном используем функцию nonthrowing_foo. Она сообщает о том, что ей плохо, с помощью кодов возврата. Если что‑то пошло не по плану, надо эту ошибку как‑то обработать. Мы обрабатываем её самым простым способом — убиваем приложение.

Если мы используем исключения, то просто зовём функцию, которая может кинуть исключение. Уже здесь видна разница между подходами. Подход с кодами возврата заставляет вас писать обработку ошибок внутри вашей бизнес‑логики. Соответственно код становится сложнее читать, и есть шанс что‑то забыть обработать в кодах возврата. Особенно если функция, которая сообщает об ошибке через коды возврата, не использует атрибут [[nodiscard]]. С исключениями всё просто: взяли, написали код, а обработка ошибок вынесена отдельно от бизнес‑логики и не мешается под ногами.

Что ж с производительностью? Опять воспользуемся Godbolt, закинем туда эти два примера. Увидим, что в коде, который занимается возвратом ошибок, есть ассемблерные инструкции на проверку кода возврата: если что‑то пошло не по плану, они перепрыгивают на то место, где идёт обработка ошибки.

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

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

На практике такого не бывает (надеюсь), и разница составляет единицы процентов. Есть замечательная статья, где автор сравнивает скорость работы парселки XML, написанной с исключениями и кодами возврата. Рекомендую для ознакомления.

Но все эти результаты для случая, когда мы исключения не выбрасываем. В противном случае, вы уже видели, какой ужас там творится: аллокация памяти, зануление этой памяти, какой‑то typeinfo куда‑то сохраняется, потом зовётся __cxa_throw. Кто‑то ещё может взять и какой‑нибудь stacktrace писать в это исключение, чтобы всё совсем уж было медленно.

Поэтому выбирайте инструмент под ваши нужды. Если ошибки редки и действительно относятся к разряду «исключительной ситуации» или если вам хочется разгрузить бизнес‑логику — присмотритесь к исключениям. Если ошибка возникает довольно часто, скорее всего, вам нужны аналоги кодов возврата.

Что снижает скорость исключений

Этим вопросом задались люди в комитете по стандартизации C++. И вот такие интересные результаты мы получили:

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

Вот история из нашей практики. Мы пишем асинхронный фреймворк userver и хотим, чтобы всё работало супер‑пупер‑быстро. У нас ошибки — это редкий кейс, мы о них сообщаем через исключение.

Например, возьмём драйвер базы данных для Mongo. Большую часть времени с ним всё замечательно: база работает как часы, запросы быстренько идут, быстренько асинхронно обрабатываются. Но если вдруг базе стало очень плохо и она «прилегла отдохнуть», или сеть решила потупить, или какой‑нибудь роутер решил перезагрузиться и сетка пропала на несколько секунд…

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

Но на самом деле всё не так страшно в современных системах. В стандартной библиотеке glibc версии 2.35 появился метод dl_find_object. Его также можно использовать для раскрутки стеков и построения механизма исключений без захвата мьютекса, чем сразу же и воспользовались компиляторы Clang и GCC. Если они замечают функцию dl_find_object, то используют её, глобальный мьютекс не захватывается, и исключения из разных потоков друг другу не вредят.

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

Подменяем dl_iterate_phdr

Эту идею мы позаимствовали от клёвых ребят из ScillaDB и творчески доработали. Итак, зовём изначальный dl_iterate_phdr и всё кешируем в std::vector. После чего подменяем WEAK-функцию dl_iterate_phdr своей, которая смотрит в кеш: если он есть, берёт данные с кеша, если нет, фолбетчится на изначальную оригинальную функцию dl_iterate_phdr

Готово! Вроде бы. 

Есть такой нюанс. Когда вылетает исключение, механизм обработки этих исключений зовёт dl_iterate_phdr, чтобы понять, к какому куску кода в бинарных файлах принадлежат указатели, как‑то связанные с исключением. То есть если мы что‑то закешировали, а потом пользователь взял и позвал dlopen, то появился ещё один кусочек бинарного кода, которого нет в нашем кеше.

Если из этого кусочка кода кинуть исключение, будет Undefined Behaviour. Исключение может не размотать стек или вовсе не пойматься. Для нас это неприемлемо. Мы должны гарантировать, что пользователь не накосячит, как бы он ни старался. Поэтому нужно сразу ему давать информацию о том, что он делает что‑то не так. А именно — что нельзя звать dlopen вот в этот момент, мы уже всё закешировали.

Что мы сделаем? Да как всегда — подменим изначальные WEAK‑функции для работы с динамическими библиотеками своими. В них получаем указатель на изначальную функцию, а потом зовем assert, что кеш ещё не заполнен. Если кеш уже взведён, значит, пользователь неправильно пользуется фреймворком и ему надо подсказать, как воспользоваться правильно.

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

Страшное число 42

Хватит на сегодня исключений. Внимание, загадка! Есть вот такой код.

template <class T>
constexpr T unsafe_do_something() noexcept {
    typename std::remove_reference<T>::type* ptr = nullptr;
    ptr += 42;
    return static_cast<T>(*ptr);
}

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

Внимание, вопрос: зачем такое писать?

Отгадка. Это написанная на коленке замена std::declval.

То есть вы пишете какой‑то сложный шаблонный код с метапрограммированием. Вы работаете с непонятными типами данных, которые вам передал пользователь, скажем, T. Но вот вам нужна для compile‑time‑вычислений ссылка на этот тип данных, чтобы что‑то из неё вывести. Обычно для этого вызывается std::declval, но некоторые компиляторы определяют, что ссылка потенциально может позваться в runtime, и не дают приложению скомпилироваться.

Вторая загадка. А зачем здесь += 42?

Отгадка. На разыменование nullptr ругнулся PVS‑Studio на статическом анализе. Инструмент сказал: «Ты что делаешь, человек? Ты же нулевой указатель разадресовываешь!» Некоторые компиляторы со временем подтянулись и стали выдавать похожую диагностику. Поэтому чтобы статические анализаторы не ругались, надо их запутать. Поэтому добавляем 42, и статические анализаторы начинают думать, что вы знаете, что делаете.

При этом очень не хочется, чтобы эту функцию кто‑то вызвал на runtime. Тогда вместо compile‑time‑проверки можно сделать LinkTime‑проверку. Для этого пишем функцию, у которой нет тела. Какую‑нибудь report_if_you_see_this_link_error_with_this_function. После чего эту функцию мы используем внутри нашей реализации declval.

Дальше происходит интересное. Компилятору на этапе компиляции безразлично, что мы зовём внутри нашего declval. Но если вдруг он оказывается в том месте, которое потенциально можно позвать на runtime, то линкеру потребуется тело функции report_if_you_see_this_link_error_with_this_function. И соответственно, если кто‑то попытается использовать наш declval неправильно, приложение его не слинкуется.

Пример реализации можно найти в Boost.PFR.

Compile-time-трюки

Переходим к тому, что все любят — compile-time-трюкам. Смотрите, что будет, если написать вот такую невзрачную функцию print()

#include <iostream>

template <auto member_ptr>
void print() {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
}

Тут функция использует шаблонные параметры. Другая особенность этого print в том, что он использует платформоспецифичный макрос компилятора __PRETTY_FUNCTION__. Он разворачивается в строку с именем функции, внутри которой написан этот макрос. И ещё в этой строке содержатся все шаблонные параметры, с которыми была проинстанцирована эта функция print().

Например:

struct S {
    int the_member_name;
} s;

int main() { print<&s.the_member_name>(); }

Выведет это:

void print() [with auto member_ptr = (& s.S::the_member_name)]

Дальше интереснее. А именно попробуем разбить на части структуру с помощью Structured Binding.

struct S2 {
    int the_member_name;
    short other_name;
} s2;

int main() {
    const auto& [a, b] = s2;
    print<&a>();
    print<&b>();
}

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

void print() [member_ptr = &s2.the_member_name]
void print() [member_ptr = &s2.other_name]

__PRETTY_FUNCTION__ можно звать в compile‑time внутри constexpr и constval, а значит, с помощью небольших страданий с парсингом, можно из этой длинной строчки вычленить имя поля произвольной структуры.

Именно это было сделано очень клёвыми энтузиастами. Они сделали pull request в Boost PFR, и теперь там доступна из коробки функциональность по доставанию имён полей по их индексу.

Таким образом, мы получаем инструмент для написания универсального сериaлизатора любых агрегатов в строки (JSON, YAML).


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

А если вам интересно посмотреть на то, о чём я сегодня рассказывал во фреймворке userver, то заходите к нам на Гитхаб.

Комментарии (54)


  1. Kelbon
    05.11.2024 07:31

     А именно — что нельзя звать dlopen вот в этот момент, мы уже всё закешировали.

    то есть теперь запрещено открывать динамические библиотеки после старта программы? А в scilla db проверка эта добавлена или там просто запрещено?


    1. antoshkka Автор
      05.11.2024 07:31

      Запрещено открывать динамические библиотеки после инициализации всех компонентов. То есть, в конструкторах можно спокойно использовать dlopen|dlclose|dlsym, но когда уже сервер полностью стартовал и начал обрабатывать пользовательские запросы - нельзя.

      В исходниках scilla найти переопределение dlopen|dlclose|dlsym не получилось, так что наверное у них просто запрещено в документации (или запрещено как-то более хитро).


  1. Melirius
    05.11.2024 07:31

    Добавлять что-то к nullptr - это UB.


    1. mayorovp
      05.11.2024 07:31

      Только если до этой операции дойдёт выполнение.


      1. Melirius
        05.11.2024 07:31

        Согласен, действительно грязный трюк :)


      1. Cfyz
        05.11.2024 07:31

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

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

        То есть компилятор например может перекособочить выражение где используется этот "declval на коленке", рассудив что вся конструкция в таком виде невозможна раз это выглядит как UB. И что нужно проигнорировать данную попытку SFINAE, выдать другое значение, взять другой тип и так далее в том же духе.

        Примеров творческой интерпретации программы компилятором из-за наличия в ней UB полно и кажется наивным полагать, что это ни в коем случае не касается compile-time выражений.


        1. sabudilovskiy
          05.11.2024 07:31

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


          1. Cfyz
            05.11.2024 07:31

            Вы в курсе, что все гарантии стандарта распространяются только на программу без UB?


            1. KanuTaH
              05.11.2024 07:31

              UB - это runtime concept, а не compile-time. Пока код с UB не исполняется в реальности, UB не существует. Например, здесь нет UB:

              if (false) {
                int *i = nullptr;
                *i += 1;
              }

              Здесь UB может быть, а может и не быть:

              int incr(int x)
              {
                return ++x;
              }

              Когда пишется код типа такого:

              T x;
              decltype(do_something(x)) y;

              никакого UB быть не может, поскольку никакого кода, который мог бы выполняться при вызове do_something(), вообще не генерируется. Выполняется только type inference, а его правила четко определены в стандарте и никак не зависят от конкретных входных данных.


              1. Cfyz
                05.11.2024 07:31

                никакого UB быть не может, поскольку никакого кода, который мог бы выполняться при вызове do_something(), вообще не генерируется

                template<typename T> auto do_something(T&&) {
                  /* какое-нибудь UB */
                }

                Гипотетически компилятор натыкается на нестыковку, оптимизирует do_something() до return nullptr; и вуаля, decltype() возвращает вообще не то. Хотя никакого кода не генерируется и более того, в стандарте прямым текстом написано, что передаваемое выражение НЕ выполняется.

                Пример конечно очень частный, но иллюстрирует что лучше никогда не говорить "никогда" =) особенно если поведение программы буквально не определено.

                Пока код с UB не исполняется в реальности, UB не существует.

                Но возможное наличие UB сказывается на генерируемой программе еще во время ее компиляции.

                UB - это runtime concept, а не compile-time.

                Потому что нет, это не только runtime concept.

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

                На основе этого обещания компилятор делает самые разные выводы как раз-таки в compile time, в основном в целях оптимизации. Думаю многие видели как потенциальное UB приводит к изменению фактического алгоритма вплоть до неузнаваемости еще задолго до его выполнения.


                1. KanuTaH
                  05.11.2024 07:31

                  Гипотетически компилятор натыкается на нестыковку, оптимизирует do_something() до return nullptr; и вуаля, decltype() возвращает вообще не то. Хотя никакого кода не генерируется и более того, в стандарте прямым текстом написано, что передаваемое выражение НЕ выполняется.

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

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

                  Да, в рантайме. И она их не производит (практически как в моем примере с if (false) {...}). Слово "поведение" в словосочетании "неопределенное поведение" относится к поведению виртуальной машины C++, которую описывает стандарт, при выполнении сгенерированного компилятором кода. Нет выполняемого кода - нет и виртуальной машины, которая может "неопределенно себя повести". Не нужно распространять понятие UB на компилятор, там у него есть свои заморочки типа IFNDR, но это уже совсем другая история.


                  1. Cfyz
                    05.11.2024 07:31

                    Это так не работает. Вывод типа выполняется до генерации какого-либо кода

                    Это как это вывод типа auto без trailing return type работает до генерации кода return?

                    Да, в рантайме. И она их не производит

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

                    Слово "поведение" в словосочетании "неопределенное поведение" относится к поведению виртуальной машины C++ <...> Не нужно распространять понятие UB на компилятор

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

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

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

                    Поэтому изначальное утверждение, которому я оппонировал:

                    Добавлять что-то к nullptr - это UB, только если до этой операции дойдёт выполнение

                    Не выглядит справедливым. Наличие UB в коде может привести и нередко приводит к непредсказуемым последствиям в плане генерируемого кода, делая поведение не определенным стандартом даже если фактически выполнение до этой конкретной операции еще не дошло.


                    1. KanuTaH
                      05.11.2024 07:31

                      Это как это вывод типа auto без trailing return type работает до генерации кода return?

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


  1. Demevag
    05.11.2024 07:31

    А для чего вообще нужно тело в замене std::declval?
    Разве так не достаточно?

    template<typename T>
    constexpr T my_declval(); 
    
    struct NonDefault
    {
        NonDefault() = delete;
        int foo() const { return 1; }
    };
    
    int main()
    {
       decltype(my_declval<NonDefault>().foo()) n = 42;
       return n;
    }

    https://godbolt.org/z/aoEKhfcs7


    1. antoshkka Автор
      05.11.2024 07:31

      Отличная идея! Но увы, так не срабатывает: `constexpr operator T();` без тела заставляет компилятор выдавать предупреждение "constexpr функция без тела в данной единице трансляции не сможет быть объявлена позднее", а это сообщение в некоторых компиляторах невозможно отключить :(


  1. xibir
    05.11.2024 07:31

    "Compile-time-трюки" вообще не компилится (g++ 14.2).


    1. antoshkka Автор
      05.11.2024 07:31

      Требуется C++20 стандарт с поддержкой auto в шаблонных параметрах


      1. xibir
        05.11.2024 07:31

        Ничего из: -std=c++20 -std=c++23 -std=c++26 не помогает
        всё равно ругается на вызовы print:
        print<&s.the_member_name>();
        и
        print<&a>();
        print<&b>();


        1. antoshkka Автор
          05.11.2024 07:31

          Вот так работает https://godbolt.org/z/b3TMxnj39


          1. xibir
            05.11.2024 07:31

            Работает только если s и s2 глобальные, если не глобальные, уже не работает.


            1. antoshkka Автор
              05.11.2024 07:31

              Да, есть ограничения :(

              Некоторые из них обходятся в Boost.PFR


  1. Melirius
    05.11.2024 07:31

    А почему бы не воспользоваться consteval для функции unsafe_do_something, чтобы запретить вызов в рантайме?


    1. antoshkka Автор
      05.11.2024 07:31

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


  1. AndrewSu
    05.11.2024 07:31

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

    Но ведь проще в CI тесты под GDB запустить, а ему передать команду печатать stacktrace в случае падения. Никаких грязных трюков.


    1. antoshkka Автор
      05.11.2024 07:31

      Исключение в тесте не ведёт к падению программы в большинстве тестовых фреймворков. А вот стектрейс от неожиданного исключения пришёлся бы кстати


  1. 9241304
    05.11.2024 07:31

    Когда же в плюсах наконец откажутся от исключений...


    1. HiItsYuri
      05.11.2024 07:31

      А зачем плюсам от них отказываться? Не нравятся - не используйте. Гугл и Qt как то же живут.


  1. buldo
    05.11.2024 07:31

    Про имена.

    А почему такое прямо в язык на завезут? Типа шарпового nameof() который тоже работает в момент компиляции.


    1. antoshkka Автор
      05.11.2024 07:31

      Завозят в C++26. Как раз добавляют рефлексию времени компиляции


  1. Panzerschrek
    05.11.2024 07:31

    Парсинг __PRETTY_FUNCTION__ - дикий костыль, вызванный тем, что в стандарт языка всё ещё не завезли статичной рефлексии. Есть информация, когда её можно ожидать?


    1. Dooez
      05.11.2024 07:31

      C++26


  1. alexandrustinov
    05.11.2024 07:31

    Во что компилятор превращает ключевое слово throw?

    В весьма своеобразную форму goto

    Что лучше — исключение или коды возврата? 

    Конечно коды возврата. Авторы Go и Rust могут только подтвердить (если С ники не в авторитете).

    Коды возврата более менее реально покрыть и статическим анализатором (в части вот тут добавляем еще один код возврата в enum, айда теперь смотреть где у нас там выпали константы из switch (errcode) обработчиков), ну и автотестами с покрытием, в отличие от.

    Но если проект унаследован или хочет C++ библиотек - то ничего не попишешь, придется сидеть с exceptions, вымирать как мамонт и далее.


    1. antoshkka Автор
      05.11.2024 07:31

      Конечно коды возврата. Авторы Go и Rust могут только подтвердить

      Python, C#, Java, JS и многие другие языки программирования с вами не согласятся. Да и в Rust паника - это исключение.

      Статический анализ switch - хороший инструмент (есть нюансы с `default:`, и с большими кодовыми базами, с бинарными поставками). С исключениями же достаточно покрыть тестами catch блоки, которых будет значительно меньше чем проверок кодов возврата. И логика приложения не замусоривается лишними if err != nil и последующими обработками ошибок.

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


      1. alexandrustinov
        05.11.2024 07:31

        Python, C#, Java, JS и многие другие языки программирования с вами не согласятся. Да и в Rust паника - это исключение.

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

        А про Python, C#, Java, JS это вообще не аргумент, это все legacy из 90-х с заложенными еще тогда очень модными заблуждениями, а по сути - ошибками проектирования - мало кто понимал в те годы, как на самом деле нужно в надежность и прочие моменты.

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

        Странный аргумент. Чего больше, чего меньше. Видимо пока еще не наступило понимание сути обработки ошибок в реальном коде.

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

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

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

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


        1. KanuTaH
          05.11.2024 07:31

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

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

          А про Python, C#, Java, JS это вообще не аргумент, это все legacy из 90-х с заложенными еще тогда очень модными заблуждениями, а по сути - ошибками проектирования - мало кто понимал в те годы, как на самом деле нужно в надежность и прочие моменты.

          Ну да, дураки были, не знали, что обработчики со switch (errcode) из C образца семидесятых годов - это верх совершенства. Хорошо, что пришли вы, и развенчали их заблуждения.


          1. alexandrustinov
            05.11.2024 07:31

            Ну да, дураки были, не знали, 

            Ну почему дураки. Когда-то и на паровозах ездили по дорогам, и на телегах, не все же дураки поди были. Просто так тогда было модно или не могли иначе.
            Вот и сейчас так делают по привычке, т.к. отказаться нельзя, терабайты legacy кода куда девать?

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

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

            Да и вообще коды ошибок - это да, явно нечто плохое, нужно срочно в шредер их все, от 404 до ORA-00060 и далее.

            Ну не хотите писать статически верифицируемый и чуть более надежный код без goto (зачем-то называя их exceptions) - продолжайте и дальше использовать исключения, кто запрещает? Тем более в случае Modern C++ и выбора иного реально нет.

            Хотя конечно забавно - goto/setjmp/longjmp это значит бубу, ни за что нельзя использовать! А exceptions - это найс, это можно, нужно и даже полезно. Хотя они в принципе, по сути - мало чем между собой отличаются (хоть и реализованы чуть иначе, но ведут-то себя абсолютно аналогично, к примеру при выходе из циклов for/while).

            А почему так? Почему goto это плохо, а exceptions - это хорошо? В чем между ними разница? Чуть иной и более приятный глазу синтаксис, что еще? Одобрение и неодобрение старших товарищей и прочих авторов явно умных книг?

            А почему при goto безусловные нелокальные переходы не одобряются, а при exceptions - наоборот одобряются? Только потому что другим синтаксисом оформлены? Самим это не кажется странным?


    1. mayorovp
      05.11.2024 07:31

      Не надо путать коды возврата и обработку ошибок через типы-суммы. Для второй в языке, как минимум, должны быть эти самые типы-суммы. А чтобы она была ещё и удобной - требуется ещё несколько фич: проверка тотальности при работе с перечислениями (и типами-суммами), try-оператор, параметрический полиморфизм для обобщений по типу результата или по коду ошибки...


      1. alexandrustinov
        05.11.2024 07:31

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

        Типы-суммы? Какой-то хаскель головного мозга, простите. Достаточно просто возвращать обычный составной тип, struct/class в терминах С++

        С введением в практику destructuring assignment и type introspection это все довольно просто и удобно в итоге, к примеру в С++ уже сейчас вполне возможно писать нечто вида:

        Проверяется через clang ./test.cpp --std=c++23 && ./a.out

        #include <stdlib.h>
        #include <stdio.h>
        
        #define var auto
        #define func auto
        #define HANDLE_ERROR(val) fprintf(stderr, "Error in %s, wrong value: %d\n", __FUNCTION__, val); abort(); 
        
        struct SomeResult {
          enum Errors {
            OK = 0,
            ERROR_BAD_X,
            ERROR_BAD_Y,
            ERROR_BAD_Z
          } error;
          int value;
        
          SomeResult(Errors error, int value): error(error), value(value) {};
          SomeResult(int value): error(Errors::OK), value(value) {};
        
          static func doSomething(int x, int y) -> SomeResult {
            if (x < 0) 
              return { Errors::ERROR_BAD_X, 0 };
            if (y < 0) 
              return { Errors::ERROR_BAD_Y, 0 };
            return x + y ;
          }
        
        };
        
        
        func myFunc(int z) -> int {
          using enum SomeResult::Errors;
        
          var x = z / 2, y = z * 3;
          var [error, result] = SomeResult::doSomething(x, y);   
        
          switch (error) {
            case OK: return result * result;
            case ERROR_BAD_X: HANDLE_ERROR(x);
            case ERROR_BAD_Y: HANDLE_ERROR(y);
          }
        };
        
        
        int main() {
          var result = myFunc(33);
          printf("Ook, result: %d\n", result);
          return 0;
        }
        

        При этом конкретные типы составных кортежей, как и типы для уже error и result задает не пользователь/вызывающий doSomething(), а разработчик doSomething, самому выводить эти составные типы в местах пользования не нужно. Разве не удобно?

        При этом компилятор сразу тебя носом тыкает, разве это не прелесть?

        ./test.cpp:44:11: warning: enumeration value 'ERROR_BAD_Z' not handled in switch [-Wswitch]

        ./test.cpp:40:1: warning: non-void function does not return a value in all control paths [-Wreturn-type] 40 | };

        Сможете такое повторить на "стандартном" С++, чтоб введение нового типа Exception сразу автоматически компилятором подсказало все места, где теперь нужно озаботиться о его обработке?

        Про зло в виде switch default говорить не нужно, просто не нужно его использовать


        1. mayorovp
          05.11.2024 07:31

          Фундаментальная проблема пары [error, result] - в том, что и error, и result существуют одновременно. Что вы там про статическую верификацию в соседней ветке говорили? Так вот, типы-суммы нужны чтобы обеспечить статическую верификацию по построению того факта, что result никогда не используется при наличии ошибки.

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

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

          if (error == ERROR_BAD_Y) HANDLE_ERROR(y);
          


          1. alexandrustinov
            05.11.2024 07:31

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

            Но зачем по стеку десяток раз передавать наверх код ошибки? Вот чтобы что?

            Оно конечно понятно, что типовой Java и не только код stacktrace подразумевает от 70 уровней вложенности вызовов, если меньше - коллеги засмеют, но а вот если подумать? Взять реальный код, и посмотреть, на сколько уровней выше нужно к примеру передавать код ошибок EAGAIN или EACCES?

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

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

            Недолго музыка играла - мозг увидел макросы (аааа, макросы же это плохо, говорили мне умные книжки и старшие, более опытные товарищи), они дают НЕОЧЕВИДНЫЕ ошибки.

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

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


            1. zzzzzzerg
              05.11.2024 07:31

              У нас построена система на основе libmdbx, система на Python. Нам в Python надо было сигнализировать, что с kv произошли какие-то проблемы. Местами это было 7-8 уровней вложенности. Изначально все было написано на error codes (из libmdbx они заворачивались в свои), переход на исключения код заметно упростил.

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


              1. alexandrustinov
                05.11.2024 07:31

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

                В иронии выше про EAGAIN и EACCESS было просто два реальных сценария.

                Первый - это вовсе не ошибка, а просто такой себе очень странный API в виде "ой, что-то пошло не так, попробуй еще раз тож самое повторить, на этот раз точно получится". Если подобное делать на Exceptions - то это лишь делает код нечитабельным, ну и может просадить производительность, хоть и не так чтоб сильно.

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

                И... и все.

                Попытка же писать некую прикладную логику на exceptions - это просто закамуфлированное goto программирование, и не более того. В говнокоде допустимо, там не критично, в системах 24/7 (серверы баз данных, ядра ОС и т.п.) за подобный "стиль" - тебя просто не поймут.

                Но в индустрии в какой-то момент что-то пошло не так, и уже вроде и 30 лет прошло, но нет, продолжают эти goto exceptions двигать в массы, и никакого Go с Дейкстрой и прочей функциональщиной на них не хватает. Настолько legacy изначально криво спроектированных прикладных сред придавило создание этих масс, что не видят очевидный обман и подмену понятий.


                1. zzzzzzerg
                  05.11.2024 07:31

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

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

                  Для этого у нас, например, и используются исключения. И об этом я и написал.

                  Например, MDBX_PANIC - тут нам делать нечего и поправить мы не можем - бросаем исключение.

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

                  MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.

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


                  1. alexandrustinov
                    05.11.2024 07:31

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

                    Ок, контекст держать уже не можем? До чего людей ChatGPT доводит в наше время, страшно подумать :) Ну да ладно, пусть это будут голоса в голове виноваты.

                    Например, MDBX_PANIC - тут нам делать нечего и поправить мы не можем - бросаем исключение.

                    И дальше что? Бросили, заллогировали, далее работаем как ни чем не бывало? Тут вообще-то в abort() выпадать надо.

                    MDBX_DBS_FULL - тут мы можем что-то сделать,

                    Кек лол, /** Environment maxdbs reached */ MDBX_DBS_FULL = -30791,

                    вы тут уже ничего не можете сделать, только рестарт процесса с исправлением конфига - т.е. читай выше про abort()

                    MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.

                    MDBX_NOTFOUND The specified database doesn't exist in the * environment and \ref MDBX_CREATE was not specified.

                    Тут тоже аборт и рестарт,

                    хотя вот тут да MDBX_NOTFOUND No matching key found.

                    Тут это просто даже не ошибка, а просто статус - нашли-не нашли, это не Exception ни разу, это банальное EXISTS() / NOT EXISTS(), просто Говарду Чу было проще это через код ошибки сделать, зачем-то.

                    MDBX_NOTFOUND - тут мы можем что-то сделать в зависимости от нашей логики - заворачиваем в свой error codes и обрабатываем в моменте.

                    Но забавно было конечно, вы там вообще чем занимаетесь? Точно программированием?


                    1. zzzzzzerg
                      05.11.2024 07:31

                      Такое комментировать только портить.


                    1. mayorovp
                      05.11.2024 07:31

                      И дальше что? Бросили, заллогировали, далее работаем как ни чем не бывало? Тут вообще-то в abort() выпадать надо.

                      Зависит от того, насколько важная операция делалась на верхнем уровне, и есть ли резервная система.

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


                      1. zzzzzzerg
                        05.11.2024 07:31

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


            1. mayorovp
              05.11.2024 07:31

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


  1. m0xf
    05.11.2024 07:31

    stacktrace можно размещать не после данных исключения, а до структуры __cxa_refcounted_exception, размер которой не меняется. Только нужно как-то получить размер этой структуры. Или просто взять максимальный из всех поддерживаемых платформ.


    1. antoshkka Автор
      05.11.2024 07:31

      Отличный вариант! Сделаю так для Boost. 1.88

      Спасибо!


      1. antoshkka Автор
        05.11.2024 07:31

        Но при этом придётся подменять функцию освобождения памяти исключения. А в libc++ runtime это сделать нельзя, в одном месте эта функция заинлайнивается в самом libc++ runtime. Так что возможно что не получится


    1. mayorovp
      05.11.2024 07:31

      Только нужно как-то получить размер этой структуры.

      Хм, … sizeof?


  1. raven128
    05.11.2024 07:31

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


    1. antoshkka Автор
      05.11.2024 07:31

      В этом конкретном случае мне больше по душе транслитерация :)