Виртуальный шаблонный метод
#опытным
Во влажных мечтах С++ программистов всегда находится место для возможностей одновременного использования статического и динамического полиморфизмов. И тут прям напрашивается иметь виртуальный шаблонный метод. Давайте напишем:
Выглядит привычно, пробуем скомпилировать.... И оно не билдится.
С++ не разрешает вам иметь виртуальный шаблонный метод.
Но почему?
Надо понимать, как под капотом работают виртуальные и шаблонные функции.
При определении шаблона, никакой низкоуровневый код не генерируется. Код генерируется только, когда компилятор видит использование шаблона с каким-то конкретным типом. Подробнее можно тут почитать.
Так вот реальный код инстанциаций шаблона может быть раскидан по очень большому множеству единиц трансляции.
Теперь про виртуальные функции. В абсолютном большинстве компиляторов они реализованы через vtable. Подробнее тут. Но вкратце: во время компиляции класса с виртуальным формируется статический массив, состоящий из адресов скомпилированных виртуальных методов. Порядок нахождения в массиве определяется порядком объявления методов в классе.
Почему вообще возможна такая генерация? Потому что компилятор уже в этой конкретной единице трансляции знает весь набор виртуальных методов в базовом классе и в наследнике. Поэтому он заранее может выбрать правильный размер vtable.
Теперь пытаемся совместить. Для генерации vtable на этапе компиляции нам нужно знать весь набор виртульных методов. И это прямо противоречит тому факту, что шаблон ничего не знает о своих потенциальных инстанциациях, которые разбросаны по куче единиц трансляции. Каждая единица трансляции компилируется независимо, поэтому получить информацию об адресах конкретных инстанциаций из других TU невозможно.
Хорошо, что в С++ есть множество других инструментов и концепций, которые помогут решить вашу техническую проблему в без виртуальных шаблонных методов. Это type erasure, crtp, visitor, std::function и std::any.
Combine the incompatible. Stay cool.
#cppcore #interview
#опытным
Во влажных мечтах С++ программистов всегда находится место для возможностей одновременного использования статического и динамического полиморфизмов. И тут прям напрашивается иметь виртуальный шаблонный метод. Давайте напишем:
struct Base
{
template <typename T>
virtual void do_something(T arg) = 0;
virtual ~Base() = default;
};
struct Derived : Base
{
template <typename T>
void do_something(T arg) override
{
// do something
}
};
Выглядит привычно, пробуем скомпилировать.... И оно не билдится.
С++ не разрешает вам иметь виртуальный шаблонный метод.
Но почему?
Надо понимать, как под капотом работают виртуальные и шаблонные функции.
При определении шаблона, никакой низкоуровневый код не генерируется. Код генерируется только, когда компилятор видит использование шаблона с каким-то конкретным типом. Подробнее можно тут почитать.
Так вот реальный код инстанциаций шаблона может быть раскидан по очень большому множеству единиц трансляции.
Теперь про виртуальные функции. В абсолютном большинстве компиляторов они реализованы через vtable. Подробнее тут. Но вкратце: во время компиляции класса с виртуальным формируется статический массив, состоящий из адресов скомпилированных виртуальных методов. Порядок нахождения в массиве определяется порядком объявления методов в классе.
Почему вообще возможна такая генерация? Потому что компилятор уже в этой конкретной единице трансляции знает весь набор виртуальных методов в базовом классе и в наследнике. Поэтому он заранее может выбрать правильный размер vtable.
Теперь пытаемся совместить. Для генерации vtable на этапе компиляции нам нужно знать весь набор виртульных методов. И это прямо противоречит тому факту, что шаблон ничего не знает о своих потенциальных инстанциациях, которые разбросаны по куче единиц трансляции. Каждая единица трансляции компилируется независимо, поэтому получить информацию об адресах конкретных инстанциаций из других TU невозможно.
Хорошо, что в С++ есть множество других инструментов и концепций, которые помогут решить вашу техническую проблему в без виртуальных шаблонных методов. Это type erasure, crtp, visitor, std::function и std::any.
Combine the incompatible. Stay cool.
#cppcore #interview
❤17👍5🔥4
std::source_location
#новичкам
Если вы до сих пор не знали, как платформонезависимо(безо всяких PRETTY_FUNCTION) получать человеческий доступ к имени файла, имени функции и строчке кода, где вы сейчас находитесь, то пост для вас.
В С++20 появился класс std::source_location, который инкапсулирует в себе информацию о точке исходного кода, где был создан соответствующий объект.
Применение до боли простое. У класса есть набор методов, которые в сумме и дают понимание о том, где был создан объект. Вы просто создаете объект через единственный статический метод std::source_location::current и получаете, что нужно. Пример:
В момент вызова функции log компилятор должен вычислить все параметры, переданные в функцию. Поэтому параметр location с дефолтным параметром std::source_location::current() как раз и делает нужный трюк - location неявно присваивается расположение вызова функции в коде всякий раз, когда кто-то ее дергает.
После чего нужно сериализовать location через вызовы методов в нужном порядке и вуаля, красивый лог:
В больших фреймворках, типа userver вам конечно же не нужно о таком беспокоиться. Но если вы пилите что-то свое кастомное, то std::source_location - хорошая тула для дебаггинга и логирования.
Define your place. Stay cool.
#cpp20
#новичкам
Если вы до сих пор не знали, как платформонезависимо(безо всяких PRETTY_FUNCTION) получать человеческий доступ к имени файла, имени функции и строчке кода, где вы сейчас находитесь, то пост для вас.
В С++20 появился класс std::source_location, который инкапсулирует в себе информацию о точке исходного кода, где был создан соответствующий объект.
Применение до боли простое. У класса есть набор методов, которые в сумме и дают понимание о том, где был создан объект. Вы просто создаете объект через единственный статический метод std::source_location::current и получаете, что нужно. Пример:
#include <iostream>
#include <source_location>
#include <string_view>
void log(const std::string_view message,
const std::source_location location =
std::source_location::current()) {
std::clog << "file: "
<< location.file_name() << '('
<< location.line() << ':'
<< location.column() << ") `"
<< location.function_name() << "`: "
<< message << '\n';
}
int main() {
log("Hello world!");
}
В момент вызова функции log компилятор должен вычислить все параметры, переданные в функцию. Поэтому параметр location с дефолтным параметром std::source_location::current() как раз и делает нужный трюк - location неявно присваивается расположение вызова функции в коде всякий раз, когда кто-то ее дергает.
После чего нужно сериализовать location через вызовы методов в нужном порядке и вуаля, красивый лог:
file: /app/example.cpp(17:8) int main(): Hello world!
В больших фреймворках, типа userver вам конечно же не нужно о таком беспокоиться. Но если вы пилите что-то свое кастомное, то std::source_location - хорошая тула для дебаггинга и логирования.
Define your place. Stay cool.
#cpp20
❤20👍11🔥5🐳1
Агенты добрались до продакшена
Про AI-агентов сейчас не говорит только ленивый. Обычно это выглядит так: «мы прикрутили LLM, дали ей пару тулов, теперь она сама все делает».
А потом это пытаются выкатить в прод, и внезапно оказывается, что агент — это не чатик с промптом, а распределенная система с дорогущими GPU, долгими запросами, динамическим графом исполнения, ретраями, которые нельзя просто так делать, и метриками качества, которые надо в принципе уметь измерять и делать это каждый день.
На недавнем deep tech night прошел показательный доклад «От поиска к агентам: путь Алисы», который я глянул.
Круто, когда говорят не про то, как ллм и агенты делают космолеты, а какие вызовы несет в себе их использование и как сделать работу с языковыми моделями надежной, отслеживаемой и универсальной в рамках большой компании.
Изыскания ребят привели их к AGTS - агентской транспортной системе.
Если коротко, это сервис-медиатор между агентами, тулами и моделями. Все общаются не напрямую друг с другом, а через AGTS.
И вот тут начинается нормальный backend:
🔸 агенты и тулы могут быть написаны на разных языках — Python, C++, Java/Kotlin, Go;
🔸 можно переиспользовать одну инфраструктуру для прода и приемки качества;
🔸 есть «железные пользователи» — агенты, которые имитируют пользовательское поведение;
🔸 LLM-as-a-judge - модели оценки качества "услуг агентов" - тоже живут как агенты на той же платформе;
🔸 AGTS кэширует запросы и ответы к моделям и тулам, чтобы при сбое не перезапускать всю дорогую LLM-мясорубку с нуля.
Последний пункт особенно важен.
Агент упал не в самом начале, а где-нибудь на пятой минуте размышлений? Не надо снова жечь GPU и молиться. Система делает fast-forward по закэшированным данным до точки падения и продолжает оттуда.
Пожалуй один из главных поинтов доклада: harness > model.
То есть качество можно серьезно растить не только заменой модели на новую большую и дорогую, а обвязкой вокруг нее. И эти обвязки централизовано добавляются и управляются в AGTS.
Вот некоторые достижения харнесса:
🔥 +9 п.п. на BrowseComp за счет фильтрации поисковой выдачи перед подачей в модель;
🔥 +8,5 п.п. на GAIA после добавления работы с файлами, мультимодальности и OCR;
🔥 стоимость запроса снизили в 6,6 раза;
🔥 latency p90 ужали с 30 минут до 2,5 минут;
🔥 GPU на проде стало нужно в 5 раз меньше.
В общем, очень ценный доклад именно с точки зрения раскрытия внутрянки такой сложной системы. Без AI мы уже никуда, но включение человека всё еще необходимо, чтобы заставить его работать с профитом и предсказуемостью на больших масштабах.
Про AI-агентов сейчас не говорит только ленивый. Обычно это выглядит так: «мы прикрутили LLM, дали ей пару тулов, теперь она сама все делает».
А потом это пытаются выкатить в прод, и внезапно оказывается, что агент — это не чатик с промптом, а распределенная система с дорогущими GPU, долгими запросами, динамическим графом исполнения, ретраями, которые нельзя просто так делать, и метриками качества, которые надо в принципе уметь измерять и делать это каждый день.
На недавнем deep tech night прошел показательный доклад «От поиска к агентам: путь Алисы», который я глянул.
Круто, когда говорят не про то, как ллм и агенты делают космолеты, а какие вызовы несет в себе их использование и как сделать работу с языковыми моделями надежной, отслеживаемой и универсальной в рамках большой компании.
Изыскания ребят привели их к AGTS - агентской транспортной системе.
Если коротко, это сервис-медиатор между агентами, тулами и моделями. Все общаются не напрямую друг с другом, а через AGTS.
И вот тут начинается нормальный backend:
🔸 агенты и тулы могут быть написаны на разных языках — Python, C++, Java/Kotlin, Go;
🔸 можно переиспользовать одну инфраструктуру для прода и приемки качества;
🔸 есть «железные пользователи» — агенты, которые имитируют пользовательское поведение;
🔸 LLM-as-a-judge - модели оценки качества "услуг агентов" - тоже живут как агенты на той же платформе;
🔸 AGTS кэширует запросы и ответы к моделям и тулам, чтобы при сбое не перезапускать всю дорогую LLM-мясорубку с нуля.
Последний пункт особенно важен.
Агент упал не в самом начале, а где-нибудь на пятой минуте размышлений? Не надо снова жечь GPU и молиться. Система делает fast-forward по закэшированным данным до точки падения и продолжает оттуда.
Пожалуй один из главных поинтов доклада: harness > model.
То есть качество можно серьезно растить не только заменой модели на новую большую и дорогую, а обвязкой вокруг нее. И эти обвязки централизовано добавляются и управляются в AGTS.
Вот некоторые достижения харнесса:
🔥 +9 п.п. на BrowseComp за счет фильтрации поисковой выдачи перед подачей в модель;
🔥 +8,5 п.п. на GAIA после добавления работы с файлами, мультимодальности и OCR;
🔥 стоимость запроса снизили в 6,6 раза;
🔥 latency p90 ужали с 30 минут до 2,5 минут;
🔥 GPU на проде стало нужно в 5 раз меньше.
В общем, очень ценный доклад именно с точки зрения раскрытия внутрянки такой сложной системы. Без AI мы уже никуда, но включение человека всё еще необходимо, чтобы заставить его работать с профитом и предсказуемостью на больших масштабах.
🔥7❤5👍3
Identity объекта
#опытным
На пространстве интернетов мне попался шортс, где довольно опытный в С++ чувак рассказывает про некий "трюк" в контексте такой задачи:
трюк в том, что если вам нужен идентификатор объекта, то вы можете использовать его уникальный адрес. Приводился такой пример:
Выглядит с первого взгляда круто. Действительно, у каждого объекта свой уникальный адрес. Два объекта в программе не могут занимать одну ячейку памяти. Получается, что адрес можно использовать как id и это "просто работает".
Только вот сходу можно назвать кучу проблем, к которому ведет этот подход:
🔞 id объекта меняется, если он перемещается
🔞 id объекта меняется, если он копируется(должны ли такие объекты копироваться и перемещать вообще - это вопрос, но проблемы остаются)
🔞 такой айди тесно связан с временем жизни объекта и не существует о отрыве объекта в программе
🔞 id переиспользуется другими объектами и на стеке, и на куче. Как только разрушился объект, новый объект может получить тот же самый id. И как различить объекты в таком случае непонятно.
🔞 id нельзя безопасно использовать как внешний идентификатор, аля передавать в другие сервисы или сохранять в хранилища.
Я понимаю, что скорее всего на интервью, где-то в отдельных модулях приложения или в каких-то игрушечных проектах такой подход может сработать. Но в каких-то больших распределенных системах вряд ли. Хотя мир большой и вы в комментариях расскажете, где такой подход применяется на практике.
Если хочется что-то простое и на коленке сделанное, то можно использовать приватный статический(потенциально атомарный) счетчик внутри класса и при создании объекта инкрементировать его:
При копировании, мувах Id не меняется(но это можно исправить, если надо), а чтобы новый объект заимел тот же айди нужно 100500 лет.
Однако при рестарте приложения, счетчик все еще обнуляется и будут дубли. Если вам критично, чтобы не существовало двух объектов с одинаковым id, можно использовать UUIDv7. Там 2¹²² вариантов айдишников, вероятностью совпадения при использовании uuid обычно пренебрегают.
Have your own identity. Stay cool.
#design
#опытным
На пространстве интернетов мне попался шортс, где довольно опытный в С++ чувак рассказывает про некий "трюк" в контексте такой задачи:
По сути, нужно построить центральный сервис, который отправляет обновления цен на станции, но новые желаемые цены могут поступать, пока старые обновления ещё в пути. Вас волнует только то, чтобы каждая станция в конечном итоге получила самую последнюю желаемую цену, при этом подтверждения (acknowledgements) должны приходить по порядку, а лишние обновления следует избегать. Таким образом, обновления, которые вы отправляете, могут прибывать на станцию не по порядку, но подтверждения, которые вы получаете обратно, всегда должны быть правильно упорядочены в том порядке, в котором станция применила обновление цены. И, по сути, спроектировать класс так, чтобы минимизировать время и тд.
трюк в том, что если вам нужен идентификатор объекта, то вы можете использовать его уникальный адрес. Приводился такой пример:
struct StationClient {
[[nodiscard]] uintptr_t ClientId() const {
return reinterpret_cast<uintptr_t>(this);
}
};Выглядит с первого взгляда круто. Действительно, у каждого объекта свой уникальный адрес. Два объекта в программе не могут занимать одну ячейку памяти. Получается, что адрес можно использовать как id и это "просто работает".
Только вот сходу можно назвать кучу проблем, к которому ведет этот подход:
🔞 id объекта меняется, если он перемещается
🔞 id объекта меняется, если он копируется(должны ли такие объекты копироваться и перемещать вообще - это вопрос, но проблемы остаются)
🔞 такой айди тесно связан с временем жизни объекта и не существует о отрыве объекта в программе
🔞 id переиспользуется другими объектами и на стеке, и на куче. Как только разрушился объект, новый объект может получить тот же самый id. И как различить объекты в таком случае непонятно.
🔞 id нельзя безопасно использовать как внешний идентификатор, аля передавать в другие сервисы или сохранять в хранилища.
Я понимаю, что скорее всего на интервью, где-то в отдельных модулях приложения или в каких-то игрушечных проектах такой подход может сработать. Но в каких-то больших распределенных системах вряд ли. Хотя мир большой и вы в комментариях расскажете, где такой подход применяется на практике.
Если хочется что-то простое и на коленке сделанное, то можно использовать приватный статический(потенциально атомарный) счетчик внутри класса и при создании объекта инкрементировать его:
struct StationClient {
StationClient() {
id = Id.fetch_add(1);
}
[[nodiscard]] std::size_t ClientId() const {
return id;
}
private:
static inline std::atomic<std::size_t> Id = 0;
int id;
};При копировании, мувах Id не меняется(но это можно исправить, если надо), а чтобы новый объект заимел тот же айди нужно 100500 лет.
Однако при рестарте приложения, счетчик все еще обнуляется и будут дубли. Если вам критично, чтобы не существовало двух объектов с одинаковым id, можно использовать UUIDv7. Там 2¹²² вариантов айдишников, вероятностью совпадения при использовании uuid обычно пренебрегают.
Have your own identity. Stay cool.
#design
❤14👍9❤🔥3🔥2
std::spanstream
#опытным
Радостная весть для всех, кто пользуется iostreams! В C++23 добавлены новые классы, позволяющие работать с массивами символов как c потоками, без лишних копирований и динамических выделений памяти.
Пусть у нас есть какой-то сырой буфер данных и мы хотим безопасно считать его с удобным и привычным iostream'овским апи, при этом не делая никаких копий. Для этого ввели std::ispanstream. Он принимает С++20 std::span в качестве буфера и сам не производит никаких аллокаций и копирований:
Для чтения данных фиксированного размера std::ispanstream фактически заменяет std::istringstream, потому что гарантирует отсутствие аллокаций.
В общем, если вы как-то получили доступ к непрерывному сырому участку памяти и вам нужен удобный доступ к парсингу -
Можно даже парсить данные из файла, если использовать mmap и замаппить данные файла в виртуальное пространство процесса. Тогда можно получить std::span на данные этого файла и создать std::ispanstream, парсящий их.
Правда, если вы уже используете mmap, то скорее всего хотите какое-то производительное решение. А парсер стандартных потоков ввода-вывода мягко говоря не самый быстрый. Тот же std::from_chars будет быстрее и не менее стандартным.
Don't allocate. Stay cool.
#cpp23
#опытным
Радостная весть для всех, кто пользуется iostreams! В C++23 добавлены новые классы, позволяющие работать с массивами символов как c потоками, без лишних копирований и динамических выделений памяти.
Пусть у нас есть какой-то сырой буфер данных и мы хотим безопасно считать его с удобным и привычным iostream'овским апи, при этом не делая никаких копий. Для этого ввели std::ispanstream. Он принимает С++20 std::span в качестве буфера и сам не производит никаких аллокаций и копирований:
char input[] = "10 20 30";
std::ispanstream is{span<char>{input}};
int i;
is >> i;
ASSERT_EQUAL(10,i);
is >> i;
ASSERT_EQUAL(20,i);
is >> i;
ASSERT_EQUAL(30,i);
is >> i;
std::ispanstream может работать с любыми символами, расположенными последовательно в памяти (другими словами, со всем, от чего можно сконструировать std::span).Для чтения данных фиксированного размера std::ispanstream фактически заменяет std::istringstream, потому что гарантирует отсутствие аллокаций.
В общем, если вы как-то получили доступ к непрерывному сырому участку памяти и вам нужен удобный доступ к парсингу -
std::ispanstream вам подойдет.Можно даже парсить данные из файла, если использовать mmap и замаппить данные файла в виртуальное пространство процесса. Тогда можно получить std::span на данные этого файла и создать std::ispanstream, парсящий их.
Правда, если вы уже используете mmap, то скорее всего хотите какое-то производительное решение. А парсер стандартных потоков ввода-вывода мягко говоря не самый быстрый. Тот же std::from_chars будет быстрее и не менее стандартным.
Don't allocate. Stay cool.
#cpp23
❤15👍6🔥4🤣2
Stacktrace
#опытным
Одна из проблема исключений - непонятно, откуда оно прилетело. Ну да, по сообщению об ошибке и типу исключения обычно можно отследить нужное место в программе. Но как исполнение дошло до этой точки? А это ведь самое важное - понимание, при каком сценарии наступила исключительная ситуация.
Когда мы дебажим программу, мы видим, что привело к точке останова. В любом дебаггере есть команда аля backtrace, которая выводит визуализированный стек вызовов. Например так:
В данном случае мы видим, что ошибка произошла в функции apply_discount, которую вызвал калькулятор цены при обработке запроса платежа. То есть в данном случае мы видим четкий сценарий возникновения ошибки
И вот было бы круто видеть такую картину каждый раз, когда в catch залетает исключение. Да и в принципе в любой ситуации, когда нам интересна информация из трейса.
С++23 предоставил нам возможность конструировать кастомные исключения с описанием стека вызова внутри. Делается это через класс std::stacktrace. Работает он очень просто:
С помощью статического метода std::stacktrace::current() можно получить стек вызовов в моменте использования метода. Для типа std::stacktrace даже перегружен оператор вывода в поток, поэтому вы спокойно можете вывести сырой трейс на консоль. Вывод может быть примерно таким:
Вот примерчик, чтобы поиграться.
Теперь легко сделать свое исключение со стектрейсом и прочими прелестями.
Трейс захватываем в конструкторе, собственно ровно в том месте, откуда и летит исключение. И скипаем один фрейм сверху, чтобы не учитывать конструктор, с помощью параметра метода std::stacktrace::current. В целом, использование очень похоже на std::source_location.
Know your origin. Stay cool.
#cpp23
#опытным
Одна из проблема исключений - непонятно, откуда оно прилетело. Ну да, по сообщению об ошибке и типу исключения обычно можно отследить нужное место в программе. Но как исполнение дошло до этой точки? А это ведь самое важное - понимание, при каком сценарии наступила исключительная ситуация.
Когда мы дебажим программу, мы видим, что привело к точке останова. В любом дебаггере есть команда аля backtrace, которая выводит визуализированный стек вызовов. Например так:
(gdb) bt
#0 0x0000555555556a2a in apply_discount (order_id=1001, total=250.5) at discount.c:45
#1 0x0000555555556b10 in calculate_final_price (order=0x7fffffffde40) at order_engine.c:89
#2 0x0000555555556c45 in process_payment (order=0x7fffffffde40, method=0x555555758020 "credit") at payment.c:120
#3 0x0000555555556d8a in handle_order_request (request=0x7fffffffdf00) at api_handler.c:56
#4 0x0000555555556e20 in main () at server.c:32
В данном случае мы видим, что ошибка произошла в функции apply_discount, которую вызвал калькулятор цены при обработке запроса платежа. То есть в данном случае мы видим четкий сценарий возникновения ошибки
И вот было бы круто видеть такую картину каждый раз, когда в catch залетает исключение. Да и в принципе в любой ситуации, когда нам интересна информация из трейса.
С++23 предоставил нам возможность конструировать кастомные исключения с описанием стека вызова внутри. Делается это через класс std::stacktrace. Работает он очень просто:
#include <iostream>
#include <stacktrace>
std::stacktrace bar() {
auto trace = std::stacktrace::current();
return trace;
}
std::stacktrace foo() {
return bar();
}
int main() {
auto trace = foo();
std::cout << trace << '\n';
}
С помощью статического метода std::stacktrace::current() можно получить стек вызовов в моменте использования метода. Для типа std::stacktrace даже перегружен оператор вывода в поток, поэтому вы спокойно можете вывести сырой трейс на консоль. Вывод может быть примерно таким:
0# bar() at /app/example.cpp:5 [0x4033ee]
1# foo() at /app/example.cpp:10 [0x40340d]
2# main at /app/example.cpp:14 [0x403428]
3# <unknown> [0x74eed1a2a1c9]
4# libc_start_main [0x74eed1a2a28a]
5# start [0x403304]
Вот примерчик, чтобы поиграться.
Теперь легко сделать свое исключение со стектрейсом и прочими прелестями.
class TracedException : public std::exception {
std::string message;
std::stacktrace trace;
mutable std::string full_what; // cached formatted output
public:
explicit TracedException(std::string msg)
: message_(std::move(msg))
// Capture the stack RIGHT when the exception is created
// skip = 1 means skip this constructor from the trace
, trace_(std::stacktrace::current(/skip=/1))
{}
const std::stacktrace& trace() const noexcept {
return trace_;
}
const char* what() const noexcept override {
if (full_what_.empty()) {
full_what_ = message_ + "\n\n--- Stack Trace ---\n"
+ std::to_string(trace_);
}
return full_what_.c_str();
}
};Трейс захватываем в конструкторе, собственно ровно в том месте, откуда и летит исключение. И скипаем один фрейм сверху, чтобы не учитывать конструктор, с помощью параметра метода std::stacktrace::current. В целом, использование очень похоже на std::source_location.
Know your origin. Stay cool.
#cpp23
❤12👍6🔥6😁2
Stacktrace. Tips
#опытным
Чтобы полноценно работать со стандартными трейсами, нужно знать несколько вещей:
✅ Чтобы фича заработала с новыми версиями gcc и clang, вам нужно залинковать либу
✅ Собирайте код с дебаг символами. Если хотите видеть трейсы и получать из них полезную информацию об именах функций и файлов, вместо прочтения чистых адресов, то надо собирать в дебаге. Итого полная команда сборки:
✅ Да, вы можете включать оптимизации компилятора и добавить в бинарник дебаг символы одновременно. Оптимизации конечно могу сильно преобразовывать потенциальные трейсы за счет инлайнинга, но без оптимизаций мы не можем.
✅ Дебаг символы обычно занимают довольно внушительный объем и не всегда хочется их тянуть везде вместе с бинарем. В таких случаях можно использовать утилиту
Ну или можно собирать код с флагом -g1, который включает только самую необходимую отладочную информацию
✅ Мы рассматривали стектрейсы в разрезе исключений, но их конечно же можно использовать и отдельно от них.
Однако стоит помнить, что получение трейса - не бесплатная штука. Логи-то не бесплатный, а проход по стеку вызовов и сбор информации из дебаг символов - очень затратная штука. Не стоит использовать трейсы в нагруженных частях программы. Только в ошибочных путях программы в некритичном коде.
✅ Сам класс стектрейсов предоставляет много гибкости. Можно отдельно проходиться по каждому фрейму и в принципе контролировать количество захватываемых фреймов:
Трейсы скорее всего рано или поздно заедут в каком-то виде в стандарт, чтобы их можно было использовать со стандартными исключениями и конктейнерами. Поэтому полезно же сейчас понимать, как это работает под капотом и как этим правильно пользоваться.
Know your origin. Stay cool.
#cpp23
#опытным
Чтобы полноценно работать со стандартными трейсами, нужно знать несколько вещей:
✅ Чтобы фича заработала с новыми версиями gcc и clang, вам нужно залинковать либу
-lstdc++exp. В туториалах обычно используется линковка с -lstdc++_libbacktrace, которая не работает на свежих версиях компиляторов✅ Собирайте код с дебаг символами. Если хотите видеть трейсы и получать из них полезную информацию об именах функций и файлов, вместо прочтения чистых адресов, то надо собирать в дебаге. Итого полная команда сборки:
g++ -std=c++23 -g -lstdc++exp -O2 main.cpp -o app
✅ Да, вы можете включать оптимизации компилятора и добавить в бинарник дебаг символы одновременно. Оптимизации конечно могу сильно преобразовывать потенциальные трейсы за счет инлайнинга, но без оптимизаций мы не можем.
✅ Дебаг символы обычно занимают довольно внушительный объем и не всегда хочется их тянуть везде вместе с бинарем. В таких случаях можно использовать утилиту
strip и вырезать из бинаря отдельный файл с дебаг символами. При обнаружении проблемы с программой можно в дебаггере подгрузить файл с символами и наслаждаться прелестями человекочитаемой отладки.Ну или можно собирать код с флагом -g1, который включает только самую необходимую отладочную информацию
✅ Мы рассматривали стектрейсы в разрезе исключений, но их конечно же можно использовать и отдельно от них.
int foo(int a, int b) {
if (b == 0) {
LOG_ERROR << "Second operand is zero. Trace:\n" << std::stacktrace::current();
}
return a / b;
}Однако стоит помнить, что получение трейса - не бесплатная штука. Логи-то не бесплатный, а проход по стеку вызовов и сбор информации из дебаг символов - очень затратная штука. Не стоит использовать трейсы в нагруженных частях программы. Только в ошибочных путях программы в некритичном коде.
✅ Сам класс стектрейсов предоставляет много гибкости. Можно отдельно проходиться по каждому фрейму и в принципе контролировать количество захватываемых фреймов:
void inspectTrace(const std::stacktrace& trace) {
for (const auto& frame : trace) {
std::cout << "Function : " << frame.description()
<< '\n'
<< "Source file : " << frame.source_file()
<< '\n'
<< "Line : " << frame.source_line()
<< '\n'
<< "---\n";
}
}
// Capture at most 10 frames except of the last one.
auto trace = std::stacktrace::current(/skip=/1, /max_depth=/10);Трейсы скорее всего рано или поздно заедут в каком-то виде в стандарт, чтобы их можно было использовать со стандартными исключениями и конктейнерами. Поэтому полезно же сейчас понимать, как это работает под капотом и как этим правильно пользоваться.
Know your origin. Stay cool.
#cpp23
🔥13❤7👍4
Откуда spurious wakeup на кондваре?
#опытным
У кондваров есть метод std::condition_variable::wait, который по документации блокирует исполнение потока до тех пор, пока не придет уведомление или не произойдет тн spurious wakeup(ложное пробуждение). Поэтому нам говорят, что нужно ставить wait в цикл while:
или использовать перегрузку, передавая в кондвар предикат:
Но что это за такие ложные пробуждения, из-за которых на нужно крутиться в цикле?
По сути, ложными пробуждениями можно назвать любую ситуацию, когда поток просыпается и условие предиката неудовлетворяется. Со стороны кажется, что поток просто в рандомный момент сам проснулся, когда условия еще не создались для него. Но это не совсем так. Давайте покопаемся в причинах.
👉🏿 Поток может проснуться без уведомления. Звучит как баг или признаки рождения скайнета, но все прозаичнее. На реальных системах есть свои реальные причины такого поведения.
В линуксе раньше для управления потоками(запуск, остановка и тд) система использовала сигналы. В такой модели условная переменная физически могла быть разбужена сигналом, предназначенным для совершенно другой цели — например, менеджер потоков решил передать потоку команду, а сигнал попал в тот же обработчик, что и пробуждение от
POSIX стандарт должен был учитывать эту особенность, поэтому прям в доке прописал возможность пробуждения без сигнала.
Свежих версиях линукса такой ситуации уже не может быть и из pthread'ной функции ожидания cv управление не вернется в юзерспейс при отправке любых сигналов.
Но наследие осталось.
👉🏿 А вот тут уже реальная проблема с race condition. Поток реально получил сигнал на пробуждение, проснулся, начал выполняться и увидел, что условие ложно. Потому что по сути какой-то другой поток уже успел обработать появившееся событие. Это может произойти по многим причинам, но может быть вот такая ситуация:
1. Поток A спит на
2. Поток B меняет условие и вызывает
3. Поток A просыпается, но ещё не запланирован шедулером.
4. Поток C успевает изменить условие обратно (забрал задачу из очереди, сбросил флаг).
5. Поток A наконец получает процессор, проверяет условие — оно
или такая
1. Поток A спит на
2. Поток B меняет условие и вызывает
3. Поток A просыпается, но ещё не запланирован шедулером.
4. Поток B успевает изменить условие еще раз(или несколько).
5. Поток A наконец получает процессор, проверяет условие — оно
Да, в вашем конкретном случае такого может и не произойти просто по логике программы. А можно не думать и просто использовать цикл с кондваром, хуже точно не будет.
Спасибо, @mike_hd, за вопрос в чате и идею для поста. И всем комментаторам, вовлекшимся в обсуждение, за предоставление фактуры по вопросу.
Don't get stolen. Stay cool.
#concurrency
#опытным
У кондваров есть метод std::condition_variable::wait, который по документации блокирует исполнение потока до тех пор, пока не придет уведомление или не произойдет тн spurious wakeup(ложное пробуждение). Поэтому нам говорят, что нужно ставить wait в цикл while:
while (!pred())
cv.wait(lock);
или использовать перегрузку, передавая в кондвар предикат:
cv.wait(lock, pred);
Но что это за такие ложные пробуждения, из-за которых на нужно крутиться в цикле?
По сути, ложными пробуждениями можно назвать любую ситуацию, когда поток просыпается и условие предиката неудовлетворяется. Со стороны кажется, что поток просто в рандомный момент сам проснулся, когда условия еще не создались для него. Но это не совсем так. Давайте покопаемся в причинах.
👉🏿 Поток может проснуться без уведомления. Звучит как баг или признаки рождения скайнета, но все прозаичнее. На реальных системах есть свои реальные причины такого поведения.
В линуксе раньше для управления потоками(запуск, остановка и тд) система использовала сигналы. В такой модели условная переменная физически могла быть разбужена сигналом, предназначенным для совершенно другой цели — например, менеджер потоков решил передать потоку команду, а сигнал попал в тот же обработчик, что и пробуждение от
cond_signal. POSIX стандарт должен был учитывать эту особенность, поэтому прям в доке прописал возможность пробуждения без сигнала.
Свежих версиях линукса такой ситуации уже не может быть и из pthread'ной функции ожидания cv управление не вернется в юзерспейс при отправке любых сигналов.
Но наследие осталось.
👉🏿 А вот тут уже реальная проблема с race condition. Поток реально получил сигнал на пробуждение, проснулся, начал выполняться и увидел, что условие ложно. Потому что по сути какой-то другой поток уже успел обработать появившееся событие. Это может произойти по многим причинам, но может быть вот такая ситуация:
1. Поток A спит на
cond_wait.2. Поток B меняет условие и вызывает
notify_one.3. Поток A просыпается, но ещё не запланирован шедулером.
4. Поток C успевает изменить условие обратно (забрал задачу из очереди, сбросил флаг).
5. Поток A наконец получает процессор, проверяет условие — оно
false. Это называется stolen wakeupили такая
1. Поток A спит на
cond_wait.2. Поток B меняет условие и вызывает
notify_one.3. Поток A просыпается, но ещё не запланирован шедулером.
4. Поток B успевает изменить условие еще раз(или несколько).
5. Поток A наконец получает процессор, проверяет условие — оно
false. То есть событие было фактически потеряно.Да, в вашем конкретном случае такого может и не произойти просто по логике программы. А можно не думать и просто использовать цикл с кондваром, хуже точно не будет.
Спасибо, @mike_hd, за вопрос в чате и идею для поста. И всем комментаторам, вовлекшимся в обсуждение, за предоставление фактуры по вопросу.
Don't get stolen. Stay cool.
#concurrency
👍12❤8🔥3🤣3
Где-то баг...
#опытным
Вот вам код:
Все ли в нем хорошо? Вы уверены? В чем здесь могут быть проблемы и какие у них проявления?
Напишите в комментах.
Prevent bugs. Stay cool.
#опытным
Вот вам код:
struct Order {
int price;
int id;
};
int main() {
std::vector<Order> book;
for (int i = 0; i < 40; ++i) {
book.push_back({100, i});
}
std::sort(book.begin(), book.end(),
[](const Order& a, const Order& b) {
return a.price <= b.price;
});
std::cout << "book.size() = " << book.size() << '\n';
return 0;
}Все ли в нем хорошо? Вы уверены? В чем здесь могут быть проблемы и какие у них проявления?
Напишите в комментах.
Prevent bugs. Stay cool.
❤10🔥6👍4
Strict weak ordering
#опытным
На первый взгляд, всё выглядит рабочим: мы создаём 40 заказов с одинаковой ценой 100 и уникальными
Но почему так? Мы же вообще ничего криминального не делали, а получили UB.
Баг на самом деле в том, что компаратор не удовлетворяет требованию strict weak ordering. Конкретно проблема в иррефлексивности компаратора. Для любых двух элементов с одинаковой ценой (в нашем случае все цены равны 100) компаратор возвращает
-
-
Это означает, что
Ну окей. "А почему эффекты проявляются только после определенного размера последовательности?"
Потому что реализация
1️⃣ Для малых подмассивов (обычно < 16 элементов) используется сортировка вставками (insertion sort). На каждом шаге работы эта сортировка проходится по всем предыдущим элементам и меняет текущий, если компаратор возвращает true. Есть четкая граница остановки - первый элемент. Поэтому даже если компаратор будет возвращать true для одинаковых элементов, то это приведет лишь к лишним обменам элементов.
2️⃣ Для больших диапазонов introsort переключается на быструю сортировку (quicksort) с разбиением (partitioning) вокруг опорного элемента (pivot).
Именно в фазе разбиения и прячется баг. Алгоритм разбиения тут примерно следующий. Обычно выбирается опорный элемент
Если
-
-
Таким образом, левый указатель будет двигаться вправо, пока не выйдет за границы, а правый — влево, также за пределы. Это приводит к чтению/записи за пределами массива и, в конечном счёте, к крашу.
Решение тут простое и даже есть соответствующая мемная паста на кодфосес. "Компаратор в С++ должен возвращать false, если аргументы равны". Или в нашем примере:
Reflect on your life. Stay cool.
#STL
#опытным
На первый взгляд, всё выглядит рабочим: мы создаём 40 заказов с одинаковой ценой 100 и уникальными
id, затем сортируем по возрастанию цены, используя <=. Программа компилируется и даже выполняется без ошибок… пока не достигнет определённого размера. Тогда она падает с segmentation fault.struct Order {
int price;
int id;
};
int main() {
std::vector<Order> book;
for (int i = 0; i < 40; ++i) {
book.push_back({100, i});
}
std::sort(book.begin(), book.end(),
[](const Order& a, const Order& b) {
return a.price <= b.price;
});
std::cout << "book.size() = " << book.size() << '\n';
return 0;
}Но почему так? Мы же вообще ничего криминального не делали, а получили UB.
Баг на самом деле в том, что компаратор не удовлетворяет требованию strict weak ordering. Конкретно проблема в иррефлексивности компаратора. Для любых двух элементов с одинаковой ценой (в нашем случае все цены равны 100) компаратор возвращает
true в обе стороны:-
comp(a, b) = 100 <= 100 → true-
comp(b, a) = 100 <= 100 → trueЭто означает, что
comp(a,a) тоже true (так как 100 <= 100). Согласно стандарту, компаратор должен возвращать false для сравнения объекта с самим собой. Это и есть требование иррефлексивности. Нарушение этого правила приводит к тому, что алгоритм сортировки некорректно сравнивает одинаковые элементы и по итогу входит за границы последовательностиНу окей. "А почему эффекты проявляются только после определенного размера последовательности?"
Потому что реализация
std::sort в стандартной библиотеке обычно использует интроспективную сортировку (introsort), которая выбирает стратегию в зависимости от размера диапазона:1️⃣ Для малых подмассивов (обычно < 16 элементов) используется сортировка вставками (insertion sort). На каждом шаге работы эта сортировка проходится по всем предыдущим элементам и меняет текущий, если компаратор возвращает true. Есть четкая граница остановки - первый элемент. Поэтому даже если компаратор будет возвращать true для одинаковых элементов, то это приведет лишь к лишним обменам элементов.
2️⃣ Для больших диапазонов introsort переключается на быструю сортировку (quicksort) с разбиением (partitioning) вокруг опорного элемента (pivot).
Именно в фазе разбиения и прячется баг. Алгоритм разбиения тут примерно следующий. Обычно выбирается опорный элемент
pivot. Затем два указателя двигаются навстречу друг другу, пока не найдут элементы, которые должны быть поменяны местами:while (comp(*left, pivot)) ++left; // пока левый < pivot
while (comp(pivot, *right)) --right; // пока pivot < правый
Если
comp использует <=, то для равных элементов оба условия вернут true:-
comp(*left, pivot) вернёт true (если *left == pivot, то pivot <= pivot → true)-
comp(pivot, *right) тоже true (если *right == pivot).Таким образом, левый указатель будет двигаться вправо, пока не выйдет за границы, а правый — влево, также за пределы. Это приводит к чтению/записи за пределами массива и, в конечном счёте, к крашу.
Решение тут простое и даже есть соответствующая мемная паста на кодфосес. "Компаратор в С++ должен возвращать false, если аргументы равны". Или в нашем примере:
std::sort(book.begin(), book.end(),
[](const Order& a, const Order& b) {
return a.price < b.price;
});
Reflect on your life. Stay cool.
#STL
🔥24👍9❤5
WAT
#новичкам
Как вы думаете, результат выражения:
будет правдой или ложью?
Нормальный человек думает, что математика работает.
Но С++ выкидывает ее в мусор и по итогу math == false.
#ЧЗХ ?
Вместо того, чтобы приводить беззнаковое число к знаковому, С++ делает наоборот. В итоге -1 должен конвертироваться в неотрицательное число. В дополненном коде -1 это 0xFFFFFFFF ака все единички, поэтому при касте к беззнаковому числу оно просто становится UINT32_MAX. И оно конечно больше 1.
Выход тут один. Не сравнивать знаковые и беззнаковые числа в одном выражении. Включайте -Wall -Werror и получите ошибку компиляции. После исправления арифметика будет работать как ожидается.
Mix things consciously. Stay cool
#cppcore
#новичкам
Как вы думаете, результат выражения:
bool math = -1 < 1U;
будет правдой или ложью?
Нормальный человек думает, что математика работает.
Но С++ выкидывает ее в мусор и по итогу math == false.
#ЧЗХ ?
Вместо того, чтобы приводить беззнаковое число к знаковому, С++ делает наоборот. В итоге -1 должен конвертироваться в неотрицательное число. В дополненном коде -1 это 0xFFFFFFFF ака все единички, поэтому при касте к беззнаковому числу оно просто становится UINT32_MAX. И оно конечно больше 1.
Выход тут один. Не сравнивать знаковые и беззнаковые числа в одном выражении. Включайте -Wall -Werror и получите ошибку компиляции. После исправления арифметика будет работать как ожидается.
Mix things consciously. Stay cool
#cppcore
❤14👍7🔥4😭3