Какой результат попытки компиляции и запуска кода выше?
Anonymous Poll
12%
Ошибка компиляции
6%
UB
8%
0 0 0 0
37%
0 2 0 0
3%
0 0 0 2
25%
0 2 0 2
9%
S O S
Дефолтные параметры виртуальных методов
#опытным
Как и в обычных функциях и методах класса, в виртуальных методах тоже можно определять параметры по-умолчанию. Однако, они могут вести себя не совсем так, как вы этого ожидаете. Спасибо, @d7d1cd, за идею для поста)
Правильный ответ из поста выше -
Дело вот в чем. Если реализация виртуальных методов выбирается по динамическому типу, то выбор дефолтных параметров определяется статическим типом.
То есть, если мы вызываем метод у объекта наследника(или через ссылку, или указатель), то выбирается дефолтное значение метода наследника.
А если мы вызываем метод по указателю или ссылке на базовый класс, то дефолтное значение будет взято из объявления метода в базовом классе.
Из-за такого поведения в коммьюнити не принято переопределять дефолтные параметры виртуальных методов в наследниках, потому что это может сильно осложнить отладку.
Вообще говоря, если так хочется задать значение по-умолчанию, то возможно стоит рассмотреть перегрузку с последующим перенаправлением вызова в общую реализацию. Но это уже по ситуации нужно смотреть, чтобы не плодить много виртуальных методов.
Или можно использовать идиому публичного невиртуального интерфейса. Тогда ваш публичный метод будет устанавливать значения по умолчанию и их никто не сможет переопределить, потому что метод будет сам невиртуальный.
Слышится легкое: "Эх, я когда-нибудь смогу выучить плюсы?...".
Don't be confusing. Stay cool.
#cppcore
#опытным
Как и в обычных функциях и методах класса, в виртуальных методах тоже можно определять параметры по-умолчанию. Однако, они могут вести себя не совсем так, как вы этого ожидаете. Спасибо, @d7d1cd, за идею для поста)
Правильный ответ из поста выше -
0 2 0 0
.#include <iostream>
struct base {
virtual void foo(int a = 0) { std::cout << a << " "; }
virtual ~base() {}
};
struct derived1 : base {};
struct derived2 : base {
void foo(int a = 2) { std::cout << a << " "; }
};
int main() {
derived1 d1{};
derived2 d2{};
base & bs1 = d1;
base & bs2 = d2;
d1.foo();
d2.foo();
bs1.foo();
bs2.foo();
}
Дело вот в чем. Если реализация виртуальных методов выбирается по динамическому типу, то выбор дефолтных параметров определяется статическим типом.
То есть, если мы вызываем метод у объекта наследника(или через ссылку, или указатель), то выбирается дефолтное значение метода наследника.
А если мы вызываем метод по указателю или ссылке на базовый класс, то дефолтное значение будет взято из объявления метода в базовом классе.
Из-за такого поведения в коммьюнити не принято переопределять дефолтные параметры виртуальных методов в наследниках, потому что это может сильно осложнить отладку.
Вообще говоря, если так хочется задать значение по-умолчанию, то возможно стоит рассмотреть перегрузку с последующим перенаправлением вызова в общую реализацию. Но это уже по ситуации нужно смотреть, чтобы не плодить много виртуальных методов.
Или можно использовать идиому публичного невиртуального интерфейса. Тогда ваш публичный метод будет устанавливать значения по умолчанию и их никто не сможет переопределить, потому что метод будет сам невиртуальный.
Слышится легкое: "Эх, я когда-нибудь смогу выучить плюсы?...".
Don't be confusing. Stay cool.
#cppcore
This media is not supported in your browser
VIEW IN TELEGRAM
Британские ученые опубликовали шокирующую историческую хронику. Оказывается, Ленин был не коммунистом, а сиплюсплюсистом!
noexcept
#новичкам
Гарантии безопасности исключений - важная штука в С++. И если мы как-то на уровне кода можем показать коллегам и компилятору, что функция не бросает исключений - это надо делать.
Эту задачу решает спецификатор noexcept. С его помощью мы указываем, что не предполагается, что функция может бросать исключения.
noexcept также принимает опциональный булевый параметр - выражение. В основном это показывают примерно так:
Но в этом виде это особо не имеет практического смысла. Этот опциональный параметр реально может сыграть роль в шаблонном коде, когда для одних типов выражение вычисляется в true, а для других в false. Тогда мы можем гибко генерировать более безопасный с точки зрения исключений код для первых типов и обычный для вторых.
Внимание! Очень важное уточнение. Если мы пометили функцию noexcept, это не значит, что она не бросает исключений! Она может это делать, только будут последствия. При вылете любого исключения из небросающей функции будет вызван std::terminate. И никакие try-catch вам не помогут.
Также стандарт не гарантирует раскрутку стека и вызов деструкторов локальных объектов в этом случае.
Именно поэтому помечайте функции noexcept только тогда, когда точно знаете, что ни при каких условиях она не выбросит. Иначе приложение просто упадет без какой-либо внятной ошибки.
Если вы не уверены в гарантиях безопасности исключений функции, просто не помечайте ее и все. Ничего плохого в этом нет. Если мы живем с исключениями, то надо свыкнуться с мыслью, что каждая функция может бросить исключение.
Ходят слухи, что пометка noexcept разрешает компилятору некоторые оптимизации. Может в каких-то кейсах это и так, но бежать, сломя голову, метить свои функции noexcept, мечтая, что код ваш код будет бежать быстрее скорости звука, не стоит.
Вот где noexcept действительно важен - это специальные методы класса, а конкретно мув-конструктор и перемешающий оператор присваивания. Пометив их noexcept, вы разрешаете вектору при реаллокации перемещать элементы, функции своп менять местами переменные с помощью мув-семантики и многое другое. Стандартная библиотека старается давать сильную гарантию исключений(commit or rollback), поэтому для нее очень важны небросающие перемещающие методы класса.
Stay safe. Stay cool.
#cppcore #cpp11 #STL
#новичкам
Гарантии безопасности исключений - важная штука в С++. И если мы как-то на уровне кода можем показать коллегам и компилятору, что функция не бросает исключений - это надо делать.
Эту задачу решает спецификатор noexcept. С его помощью мы указываем, что не предполагается, что функция может бросать исключения.
void doSomething() noexcept;
noexcept также принимает опциональный булевый параметр - выражение. В основном это показывают примерно так:
void doSomething() noexcept(true);
void doSomething() noexcept(false);
Но в этом виде это особо не имеет практического смысла. Этот опциональный параметр реально может сыграть роль в шаблонном коде, когда для одних типов выражение вычисляется в true, а для других в false. Тогда мы можем гибко генерировать более безопасный с точки зрения исключений код для первых типов и обычный для вторых.
Внимание! Очень важное уточнение. Если мы пометили функцию noexcept, это не значит, что она не бросает исключений! Она может это делать, только будут последствия. При вылете любого исключения из небросающей функции будет вызван std::terminate. И никакие try-catch вам не помогут.
Также стандарт не гарантирует раскрутку стека и вызов деструкторов локальных объектов в этом случае.
Именно поэтому помечайте функции noexcept только тогда, когда точно знаете, что ни при каких условиях она не выбросит. Иначе приложение просто упадет без какой-либо внятной ошибки.
Если вы не уверены в гарантиях безопасности исключений функции, просто не помечайте ее и все. Ничего плохого в этом нет. Если мы живем с исключениями, то надо свыкнуться с мыслью, что каждая функция может бросить исключение.
Ходят слухи, что пометка noexcept разрешает компилятору некоторые оптимизации. Может в каких-то кейсах это и так, но бежать, сломя голову, метить свои функции noexcept, мечтая, что код ваш код будет бежать быстрее скорости звука, не стоит.
Вот где noexcept действительно важен - это специальные методы класса, а конкретно мув-конструктор и перемешающий оператор присваивания. Пометив их noexcept, вы разрешаете вектору при реаллокации перемещать элементы, функции своп менять местами переменные с помощью мув-семантики и многое другое. Стандартная библиотека старается давать сильную гарантию исключений(commit or rollback), поэтому для нее очень важны небросающие перемещающие методы класса.
Stay safe. Stay cool.
#cppcore #cpp11 #STL
std::move_if_noexcept
#опытным
В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.
Вот реализация этой функции в gcc:
Две части: условия мува и сам мув.
Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.
Условие move_if_noexcept_cond истинно только в одном случае: когда мув-конструктор бросающий и есть копирующий конструктор. Получается, что это единственная ситуация, когда мува не произойдет. Во всех остальных случаях значение скастуется к правой ссылке.
Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.
Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:
То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.
В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.
Live legally. Stay cool.
#template #cppcore #STL
#опытным
В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.
Вот реализация этой функции в gcc:
template<typename _Tp>
struct __move_if_noexcept_cond
: public _and<_not<is_nothrow_move_constructible<_Tp>>,
is_copy_constructible<_Tp>>::type { };
template<typename _Tp>
[[nodiscard,gnu::always_inline]]
constexpr
__conditional_t<__move_if_noexcept_cond<_Tp>::value, const _Tp&, _Tp&&>
move_if_noexcept(_Tp& __x) noexcept
{ return std::move(__x); }
Две части: условия мува и сам мув.
Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.
Условие move_if_noexcept_cond истинно только в одном случае: когда мув-конструктор бросающий и есть копирующий конструктор. Получается, что это единственная ситуация, когда мува не произойдет. Во всех остальных случаях значение скастуется к правой ссылке.
Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.
Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:
struct NoMove1
{
// prevents implicit declaration of default move constructor;
// however, the class is still move-constructible because its
// copy constructor can bind to an rvalue argument
NoMove1(const NoMove1&) {}
};
static_assert(std::is_move_constructible_v<NoMove1>); // Here
static_assert(!std::is_trivially_move_constructible_v<NoMove1>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove1>);
struct NoMove2
{
// Not move-constructible since the lvalue reference
// can't bind to the rvalue argument
NoMove2(NoMove2&) {}
};
static_assert(!std::is_move_constructible_v<NoMove2>); // And here
static_assert(!std::is_trivially_move_constructible_v<NoMove2>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove2>);
То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.
В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.
Live legally. Stay cool.
#template #cppcore #STL
Что будет если бросить исключение в деструкторе? Ч1
#новичкам
Вопрос, который часто задают на собеседованиях, но боюсь, что мало кто понимает правильный ответ.
Значит единственный адекватный ответ - вызовется std::terminate. И здесь даже не нужно упоминать никаких double exception. То есть:
В этом случае исключение не поймается, а просто вызовется std::terminate. И точка. Никаких дополнений.
Первое надо понимать, что все деструкторы неявно компилятором помечены, как noexcept. То есть не предполагается, что он выбрасывает исключения.
Если вы определяете деструктор дефолтным, то он noexcept. И даже если вы определяете кастомный деструктор, но не указываете ему политику исключений, он все равно помечен noexcept.
Однако мы можем сделать деструктор бросающим. Мы должны явно прописывать политику исключений:
Только в этом случае на консоли появится
И вот здесь уже можно говорить, что будет при раскрутке стека, втором исключении и прочем. Можете сами проверить на годболте.
Пока вы явно не пометили деструктор бросающим, при вылете исключения из деструктор будет вызван std::terminate.
Be explicit in your intentions. Stay cool.
#cppcore #cpp11 #interview
#новичкам
Вопрос, который часто задают на собеседованиях, но боюсь, что мало кто понимает правильный ответ.
Значит единственный адекватный ответ - вызовется std::terminate. И здесь даже не нужно упоминать никаких double exception. То есть:
struct Class {
~Class() {throw 1;}
};
int main() {
try {
{Class cl;}
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}
В этом случае исключение не поймается, а просто вызовется std::terminate. И точка. Никаких дополнений.
Первое надо понимать, что все деструкторы неявно компилятором помечены, как noexcept. То есть не предполагается, что он выбрасывает исключения.
Если вы определяете деструктор дефолтным, то он noexcept. И даже если вы определяете кастомный деструктор, но не указываете ему политику исключений, он все равно помечен noexcept.
Однако мы можем сделать деструктор бросающим. Мы должны явно прописывать политику исключений:
struct Class {
// HERE
~Class() noexcept(false) {throw 1;}
};
int main() {
try {
{Class cl;}
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}
Только в этом случае на консоли появится
caught exception
.И вот здесь уже можно говорить, что будет при раскрутке стека, втором исключении и прочем. Можете сами проверить на годболте.
Пока вы явно не пометили деструктор бросающим, при вылете исключения из деструктор будет вызван std::terminate.
Be explicit in your intentions. Stay cool.
#cppcore #cpp11 #interview
Что будет если бросить исключение в деструкторе? Ч2
#новичкам
Теперь мы разобрались с основным кейсом. Давайте подробнее рассмотрим, что будет, если мы будет работать с явно бросающими деструктором.
Единственный важный вопрос, который здесь можно задать: а что будет, если деструктор вызовется при раскрутке стека?
Вот так:
В этом случае при бросании 1.0, исключение увидит блок catch и перед входом в него начнет раскручивать стек, вызывая деструкторы всех локальных объектов во всех фреймах, которые пролетело исключение.
В нашем коде деструктор Class будет вызван до блока catch и получается, что у нас ситуация, в которой есть 2 необработанных исключения. Эта ситуация называется double exception и она приводит к немедленному вызову std::terminate.
Именно поэтому вы не должны бросать исключения в деструкторах. Именно поэтому все деструкторы по умолчанию noexcept. Потому что невозможно безопасно работать с классом, у которого бросающий деструктор, в мире, где другие сущности тоже могут сгенерировать исключения.
Теперь вы профессионально будете отвечать всем интервьюерам, заслужите много уважения и конфеты(но это не точно).
Stay safe. Stay cool.
#cppcore #cpp11 #interview
#новичкам
Теперь мы разобрались с основным кейсом. Давайте подробнее рассмотрим, что будет, если мы будет работать с явно бросающими деструктором.
Единственный важный вопрос, который здесь можно задать: а что будет, если деструктор вызовется при раскрутке стека?
Вот так:
struct Class {
// HERE
~Class() noexcept(false) {throw 1;}
};
int main() {
try {
Class cl;
throw 1.0;
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}
В этом случае при бросании 1.0, исключение увидит блок catch и перед входом в него начнет раскручивать стек, вызывая деструкторы всех локальных объектов во всех фреймах, которые пролетело исключение.
В нашем коде деструктор Class будет вызван до блока catch и получается, что у нас ситуация, в которой есть 2 необработанных исключения. Эта ситуация называется double exception и она приводит к немедленному вызову std::terminate.
Именно поэтому вы не должны бросать исключения в деструкторах. Именно поэтому все деструкторы по умолчанию noexcept. Потому что невозможно безопасно работать с классом, у которого бросающий деструктор, в мире, где другие сущности тоже могут сгенерировать исключения.
Теперь вы профессионально будете отвечать всем интервьюерам, заслужите много уважения и конфеты(но это не точно).
Stay safe. Stay cool.
#cppcore #cpp11 #interview
Что будет если бросить исключение в деструкторе? Ходим по тонкому льду.
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "throw" в деструкторе. Думаю, что на самом деле вы хотите использовать в деструкторе функцию, которая потенциально может бросить исключение:
Логгер может писать в файл с нестандартным именем, но хочет в деструкторе сделать симлинк на него в системной директории /system/logs/log.txt.
Одна проблема - std::filesystem::create_symlink может кинуть исключение, если например прав доступа к директории нет.
Было бы классно определять, находится ли сейчас программа в состоянии разворачивания стека. Если находится, то не делать опасные мувы, а если нет, то гуляй душа.
И такой инструмент есть, называется он std::uncaught_exception(). Это функция проверяет, есть ли сейчас живой объект исключения и исполнение еще не дошло до блока catch. То есть программа находится в режима разворачивания стека. И можно на основе этого знания какую-то логику строить.
Например, не создавать симлинк в логгере, если есть живой объект исключений:
В этом случае мы не будем вызывать опасную функцию, потому что это потенциально может привести к std::terminate.
Исключения - это все-таки исключительные ситуации. При их обработке важно правильно себя повести и сделать хоть что-то, но сохранить работоспособность программы, чем жонглировать ножами и в итоге выткнуть себе глаз.
И std::uncaught_exception() позволяет динамически изменять поведение программы, если вы уже попали в исключительную ситуацию.
Однако в C++17 эта функция призвана устаревшей и была удалена в C++20. В следующий раз посмотрим, почему так и что пришло ей на замену.
Walk on a thin ice. Stay cool.
#cpp17 #cpp20
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "throw" в деструкторе. Думаю, что на самом деле вы хотите использовать в деструкторе функцию, которая потенциально может бросить исключение:
constexpr std::string_view defaultSymlinkPath = "/system/logs/log.txt";
class Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName }
{}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
};
Логгер может писать в файл с нестандартным именем, но хочет в деструкторе сделать симлинк на него в системной директории /system/logs/log.txt.
Одна проблема - std::filesystem::create_symlink может кинуть исключение, если например прав доступа к директории нет.
Было бы классно определять, находится ли сейчас программа в состоянии разворачивания стека. Если находится, то не делать опасные мувы, а если нет, то гуляй душа.
И такой инструмент есть, называется он std::uncaught_exception(). Это функция проверяет, есть ли сейчас живой объект исключения и исполнение еще не дошло до блока catch. То есть программа находится в режима разворачивания стека. И можно на основе этого знания какую-то логику строить.
Например, не создавать симлинк в логгере, если есть живой объект исключений:
~Logger()
{
fileStream.close();
if (!std::uncaught_exception())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
В этом случае мы не будем вызывать опасную функцию, потому что это потенциально может привести к std::terminate.
Исключения - это все-таки исключительные ситуации. При их обработке важно правильно себя повести и сделать хоть что-то, но сохранить работоспособность программы, чем жонглировать ножами и в итоге выткнуть себе глаз.
И std::uncaught_exception() позволяет динамически изменять поведение программы, если вы уже попали в исключительную ситуацию.
Однако в C++17 эта функция призвана устаревшей и была удалена в C++20. В следующий раз посмотрим, почему так и что пришло ей на замену.
Walk on a thin ice. Stay cool.
#cpp17 #cpp20
Что будет если бросить исключение в деструкторе? Уверенно ходим по тонкому льду.
#опытным
std::uncaught_exception() в С++17 заменилась на std::uncaught_exceptions(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.
Есть уже знакомый нам класс логгера. В деструкторе калькулятора логируем какую-то информацию(не забываем про try-catch). В функции Process создаем калькулятор и вызываем его функцию Calc, которая вдруг кинула исключение. Давайте проследим цепочку событий и поймем в чем здесь загвоздка.
Из Calc вылетает исключение -> видит блок catch -> запускается раскрутка стека и деструктор калькулятора -> в деструкторе калькулятора создается логгер и записывает сообщение в файл -> при выходе из скоупа вызывается деструктор логгера -> std::uncaught_exception возвращает true и создание симлинка не происходит.
Однако в этом случае вы можете попробовать создать символическую ссылку! Дело в том, что деструктор Logger не будет вызываться непосредственно в результате размотки стека — он будет вызван после создания нового объекта из деструктора калькулятора. Таким образом, вы можете выбросить исключение из деструктора Logger'- вам нужно только поймать это исключение, прежде чем оно выйдет из деструктора калькулятора.
Если вы создали объект уже в процессе раскрутки стека, то вы можете кинуть исключение из деструктора. Надо только его поймать.
Чтобы исправить подобное поведение придумали std::uncaught_exceptions(), которая возвращает количество активных исключений на данный момент. И вот как выглядит логгер с использованием std::uncaught_exceptions():
Вот в чем фокус. Если мы создаем объект уже во время размотки стека и количество исключений на момент создания объекта равно количеству исключений при его разрушении, то мы можем вызывать потенциально бросающие функции.
Кейсы применения этой функции ужасно узкие, чуть более подробно можете почитать ее предложение в стандарт от Саттера. Поэтому вы должны очень хорошо понимать, чего вы такими неочевидными приемами хотите достичь. В современной разработке на С++ даже определение кастомного деструктора - вещь редкая, не то, что выкидывать отткуда исключения.
Помните, что такая возможность есть, но используйте ее в самом крайнем случае.
Walk on a thin ice. Stay cool.
#cpp17 #cppcore
#опытным
std::uncaught_exception() в С++17 заменилась на std::uncaught_exceptions(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.
constexpr std::string_view defaultSymlinkPath = "system/logs/log.txt";
struct Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName } {}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
if (!std::uncaught_exception()) {
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
struct Calculator
{
int64_t Calc(const std::vector<std::string> ¶ms);
// ....
~Calculator()
{
try {
Logger logger("log.txt");
Logger.Log("Calculator destroyed");
}
catch (...) {
// ....
}
}
};
int64_t Process(const std::vector<std::string> ¶ms) {
try {
Calculator calculator;
return Calculator.Calc(params);
}
catch (...) {
// ....
}
}
Есть уже знакомый нам класс логгера. В деструкторе калькулятора логируем какую-то информацию(не забываем про try-catch). В функции Process создаем калькулятор и вызываем его функцию Calc, которая вдруг кинула исключение. Давайте проследим цепочку событий и поймем в чем здесь загвоздка.
Из Calc вылетает исключение -> видит блок catch -> запускается раскрутка стека и деструктор калькулятора -> в деструкторе калькулятора создается логгер и записывает сообщение в файл -> при выходе из скоупа вызывается деструктор логгера -> std::uncaught_exception возвращает true и создание симлинка не происходит.
Однако в этом случае вы можете попробовать создать символическую ссылку! Дело в том, что деструктор Logger не будет вызываться непосредственно в результате размотки стека — он будет вызван после создания нового объекта из деструктора калькулятора. Таким образом, вы можете выбросить исключение из деструктора Logger'- вам нужно только поймать это исключение, прежде чем оно выйдет из деструктора калькулятора.
Если вы создали объект уже в процессе раскрутки стека, то вы можете кинуть исключение из деструктора. Надо только его поймать.
Чтобы исправить подобное поведение придумали std::uncaught_exceptions(), которая возвращает количество активных исключений на данный момент. И вот как выглядит логгер с использованием std::uncaught_exceptions():
struct Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
int m_exceptions = std::uncaught_exceptions(); // <=
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName } {}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
if (m_exceptions == std::uncaught_exceptions())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
Вот в чем фокус. Если мы создаем объект уже во время размотки стека и количество исключений на момент создания объекта равно количеству исключений при его разрушении, то мы можем вызывать потенциально бросающие функции.
Кейсы применения этой функции ужасно узкие, чуть более подробно можете почитать ее предложение в стандарт от Саттера. Поэтому вы должны очень хорошо понимать, чего вы такими неочевидными приемами хотите достичь. В современной разработке на С++ даже определение кастомного деструктора - вещь редкая, не то, что выкидывать отткуда исключения.
Помните, что такая возможность есть, но используйте ее в самом крайнем случае.
Walk on a thin ice. Stay cool.
#cpp17 #cppcore
decltype(auto) vs auto&&. Вывод типа переменной
#опытным
decltype(auto) и auto&& позволяют нам не беспокоиться о конкретных типах сущностей, включая ref квалификацию. Основные кейсы применения decltype(auto) - это автоматический вывод типа возвращаемого значения функции и переменной на основе инициализатора. Но чем отличаются decltype(auto) и auto&& в контексте этих кейсов? Спасибо @SoulslikeEnjoyer за идею для постов в этом комменте https://t.me/c/2009887601/47858.
Так как вывод типов можно применять в разных ситуациях, то разобьем тему на части и сегодня мы поговорим конкретно про это автоматический вывод типа переменной на основе инициализатора.
Чтобы продемонстрировать различия нам нужна вот такая заготовка:
Здесь касательно вывода типа нас интересует, как ссылочность типа возвращаемого значения функции (или ее отсутствие), сказывается на типе переменных.
Чтобы заглянуть под капот этого дела, я буду использовать полезнейший инструмент - cppinsights. Эта тулза позволяет снимать слои магии и показывает суть. И вот, что она показала:
Здесь можно четко увидеть разницу: при возврате из функции не-ref-квалифицированного типа, decltype(auto) в точности передает тип, а вот auto&& определяет тип как правую ссылку.
Проблема ли это?
a1 по идее должна быть висячей ссылкой, так как объект тут же разрушится. Но на самом деле в этом случае правая ссылка продлевает время жизни объекта, возвращенного из функции, до конца времени жизни ссылки.
Поэтому в большинстве кейсов при объявлении переменной использование decltype(auto) избыточно, auto&& прекрасно справляется.
Единственное, когда его нужно использовать - в тестах всякого шаблонного непотребства, чтобы получить точный тип возвращаемого значения и сравнить его с образцом.
В следующий раз обсудим, какие различия между ними при использовании автоматического вывода типа возвращаемого значения функции.
Be laconic. Stay cool.
#cppcore #cpp14
#опытным
decltype(auto) и auto&& позволяют нам не беспокоиться о конкретных типах сущностей, включая ref квалификацию. Основные кейсы применения decltype(auto) - это автоматический вывод типа возвращаемого значения функции и переменной на основе инициализатора. Но чем отличаются decltype(auto) и auto&& в контексте этих кейсов? Спасибо @SoulslikeEnjoyer за идею для постов в этом комменте https://t.me/c/2009887601/47858.
Так как вывод типов можно применять в разных ситуациях, то разобьем тему на части и сегодня мы поговорим конкретно про это автоматический вывод типа переменной на основе инициализатора.
Чтобы продемонстрировать различия нам нужна вот такая заготовка:
int g = 0;
int foo() {return 42;}
int& foo1() {return g;}
int&& foo2() {int i = 42; return std::move(i);}
auto&& a1 = foo();
auto&& a2 = foo1();
auto&& a3 = foo2();
decltype(auto) b1 = foo();
decltype(auto) b2 = foo1();
decltype(auto) b3 = foo2();
Здесь касательно вывода типа нас интересует, как ссылочность типа возвращаемого значения функции (или ее отсутствие), сказывается на типе переменных.
Чтобы заглянуть под капот этого дела, я буду использовать полезнейший инструмент - cppinsights. Эта тулза позволяет снимать слои магии и показывает суть. И вот, что она показала:
int && a1 = foo();
int & a2 = foo1();
int && a3 = foo2();
int b1 = foo();
int & b2 = foo1();
int && b3 = foo2();
Здесь можно четко увидеть разницу: при возврате из функции не-ref-квалифицированного типа, decltype(auto) в точности передает тип, а вот auto&& определяет тип как правую ссылку.
Проблема ли это?
a1 по идее должна быть висячей ссылкой, так как объект тут же разрушится. Но на самом деле в этом случае правая ссылка продлевает время жизни объекта, возвращенного из функции, до конца времени жизни ссылки.
Поэтому в большинстве кейсов при объявлении переменной использование decltype(auto) избыточно, auto&& прекрасно справляется.
Единственное, когда его нужно использовать - в тестах всякого шаблонного непотребства, чтобы получить точный тип возвращаемого значения и сравнить его с образцом.
В следующий раз обсудим, какие различия между ними при использовании автоматического вывода типа возвращаемого значения функции.
Be laconic. Stay cool.
#cppcore #cpp14
decltype(auto) vs auto&&. Вывод типа возвращаемого значения
#опытным
Второе место, в котором целесообразно использовать decltype(auto) и auto&& - это автоматический вывод типа возвращаемого значения функции. Вот тут между ними есть важные различия, которые мы сейчас и обсудим.
Вот 2 семейства функций, которые возвращают объекты разной ссылочности и выводит тип возвращаемого значения по-разному. Давайте с помощью cppinsights посмотрим, какие реально типы возвращаемых значений выведутся:
Единственная разница появляется, когда мы возвращаем из функции lvalue или prvalue объект. В этом случае auto&& выводит тип правой ссылки, а decltype(auto) - обычный бессылочный lvalue объект. И в этом факте вся проблема. Может быть ее не видно сейчас, когда мы возвращаем целочисленный литерал. Но задние ряды уже догадались. Давайте же перейдем к реальной проблеме:
Делаем простой класс, чтобы отследить его время жизни. И два варианта функции, которые возвращают rvalue объекты. Вот вывод для этого кода:
Вывод:
Здесь видно, что деструктор первого объекта вызывается в самом конце. При присваивании результата функции get_class_decltype() локальному объекту происходит оптимизация copy-elision, благодаря которой объект созданный внутри функции "бесшовно" становится локальным объектом caller'а без всяких мув- и копи- конструкторов.
А вот деструктор второго объекта, который мы планировали возвращать из get_class_auto, вызывается сразу после конструирования. На самом деле сигнатура функции get_class_auto выглядит так:
Мы пытаемся вернуть локальный объект функции через правую ссылку. Такое в С++ хоть и скомпилируется, но приведет к висячей ссылке и в последствии к UB при использовании ссылки.
Аналогичное будет происходить, если возвращать lvalue.
Какой вывод?
Если вам нужен вывод типов в возвращаемом значении функции - всегда используйте в типе возвращаемого значения функции decltype(auto) вместо auto&&, особенно в шаблонном коде. На это кстати есть еще одна причина, о которой речь пойдет в следующем посте.
Be safe. Stay cool.
#cppcore
#опытным
Второе место, в котором целесообразно использовать decltype(auto) и auto&& - это автоматический вывод типа возвращаемого значения функции. Вот тут между ними есть важные различия, которые мы сейчас и обсудим.
decltype(auto) bar() {
int i;
int& ref = i;
return ref;
}
decltype(auto) bar1() {
int i = 42;
return i;
}
decltype(auto) bar2() {
return 42;
}
decltype(auto) bar3() {
int i;
return std::move(i);
}
auto&& baz() {
int i;
int& ref = i;
return ref;
}
auto&& baz1() {
int i = 42;
return i;
}
auto&& baz2() {
return 42;
}
auto&& baz3() {
int i;
return std::move(i);
}
Вот 2 семейства функций, которые возвращают объекты разной ссылочности и выводит тип возвращаемого значения по-разному. Давайте с помощью cppinsights посмотрим, какие реально типы возвращаемых значений выведутся:
int & bar() {
int i;
int & ref = i;
return ref;
}
int bar1() {
int i = 42;
return i;
}
int bar2() {
return 42;
}
typename std::remove_reference<int &>::type && bar3() {
int i;
return std::move(i);
}
int & baz() {
int i;
int & ref = i;
return ref;
}
int & baz1() {
int i = 42;
return i;
}
int && baz2() {
return 42;
}
typename std::remove_reference<int &>::type && baz3() {
int i;
return std::move(i);
}
Единственная разница появляется, когда мы возвращаем из функции lvalue или prvalue объект. В этом случае auto&& выводит тип правой ссылки, а decltype(auto) - обычный бессылочный lvalue объект. И в этом факте вся проблема. Может быть ее не видно сейчас, когда мы возвращаем целочисленный литерал. Но задние ряды уже догадались. Давайте же перейдем к реальной проблеме:
struct CLASS {
CLASS(int i) : num{i} {
std::cout << "ctor " << num << std::endl;
}
~CLASS() {
std::cout << "dtor " << num << std::endl;
}
int num;
};
decltype(auto) get_class_decltype() {
return CLASS(0);
}
auto&& get_class_auto() {
return CLASS(42);
}
int main() {
decltype(auto) a = get_class_decltype();
std::cout << "separator" << std::endl;
decltype(auto) b = get_class_auto();
std::cout << "separator" << std::endl;
}
Делаем простой класс, чтобы отследить его время жизни. И два варианта функции, которые возвращают rvalue объекты. Вот вывод для этого кода:
Вывод:
ctor 0
separator
ctor 42
dtor 42
separator
dtor 0
Здесь видно, что деструктор первого объекта вызывается в самом конце. При присваивании результата функции get_class_decltype() локальному объекту происходит оптимизация copy-elision, благодаря которой объект созданный внутри функции "бесшовно" становится локальным объектом caller'а без всяких мув- и копи- конструкторов.
А вот деструктор второго объекта, который мы планировали возвращать из get_class_auto, вызывается сразу после конструирования. На самом деле сигнатура функции get_class_auto выглядит так:
CLASS&& get_class_auto();
Мы пытаемся вернуть локальный объект функции через правую ссылку. Такое в С++ хоть и скомпилируется, но приведет к висячей ссылке и в последствии к UB при использовании ссылки.
Аналогичное будет происходить, если возвращать lvalue.
Какой вывод?
Если вам нужен вывод типов в возвращаемом значении функции - всегда используйте в типе возвращаемого значения функции decltype(auto) вместо auto&&, особенно в шаблонном коде. На это кстати есть еще одна причина, о которой речь пойдет в следующем посте.
Be safe. Stay cool.
#cppcore
decltype(auto) vs auto&&. Прокси объекты и висячие ссылки.
#опытным
В прошлом посте мы видели, что разница в использовании decltype(auto) и auto&& при выводе типа возвращаемого значения функции проявляется только при возврате объектов, на которые явно не навесили ссылки. Иногда мы можем это отследить глазами и потенциально использовать более простую версию вывода типа. Однако не все мы можем отследить глазами.
Доступ к элементам std::deque всегда возвращает честную левую ссылку. И это почти полностью справедливо для std::vector, кроме одного исключения.
Это std::vector<bool>. Эта специализация вектора возвращает не честную ссылку на объект типа bool, в временный proxy объект. Дело в том, что тип bool занимает как минимум 1 байт, так как это минимально адресуемая ячейка памяти. Но логически он хранит всего 1 бит информации. Если бы мы могли как-то по-хитрому хранить булевы значения, чтобы каждое из них занимало всего 1 бит, то мы бы уменьшили потребление памяти как минимум в 8 раз! Именно это и делается в специализации для bool. Там булевы значения хранятся в виде битов более вместительного типа(int), а для получения доступа к значениям используется proxy объект reference, который неявно приводится bool.
Посмотрим, к чему приводит эта маленькая особенность:
Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.
При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.
Proxy объекты не так часто используются. Один из основных кейсов - это доступ к элементам многомерных структур. Однако при проектировании и написании кода, которым будут пользоваться другие люди, нужно учитывать такие вещи и писать в первую очередь безопасный код.
Спасибо @thonease за предоставления исходного кода)
По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).
Be safe. Stay cool.
#cppcore #template
#опытным
В прошлом посте мы видели, что разница в использовании decltype(auto) и auto&& при выводе типа возвращаемого значения функции проявляется только при возврате объектов, на которые явно не навесили ссылки. Иногда мы можем это отследить глазами и потенциально использовать более простую версию вывода типа. Однако не все мы можем отследить глазами.
Доступ к элементам std::deque всегда возвращает честную левую ссылку. И это почти полностью справедливо для std::vector, кроме одного исключения.
Это std::vector<bool>. Эта специализация вектора возвращает не честную ссылку на объект типа bool, в временный proxy объект. Дело в том, что тип bool занимает как минимум 1 байт, так как это минимально адресуемая ячейка памяти. Но логически он хранит всего 1 бит информации. Если бы мы могли как-то по-хитрому хранить булевы значения, чтобы каждое из них занимало всего 1 бит, то мы бы уменьшили потребление памяти как минимум в 8 раз! Именно это и делается в специализации для bool. Там булевы значения хранятся в виде битов более вместительного типа(int), а для получения доступа к значениям используется proxy объект reference, который неявно приводится bool.
Посмотрим, к чему приводит эта маленькая особенность:
template<typename Container, typename Index>
auto&& processAndAccess(Container& c, Index i) {
// do something
// ...
return c[i];
}
std::vector<int> v = {1, 2, 3};
// OK - returns int&
processAndAccess(v, 1) = 3;
std::vector<bool> v2 = {true, false, false};
// NOT OK - returns vector<bool>::reference&& which is a dangling reference
processAndAccess(v2, 1) = true;
Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.
При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.
Proxy объекты не так часто используются. Один из основных кейсов - это доступ к элементам многомерных структур. Однако при проектировании и написании кода, которым будут пользоваться другие люди, нужно учитывать такие вещи и писать в первую очередь безопасный код.
Спасибо @thonease за предоставления исходного кода)
По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).
Be safe. Stay cool.
#cppcore #template
Помогите Доре найти проблему в коде
#опытным
Наткнулся на просторах всемирной сети на интересный пример:
Код работает и пример довольно игрушечный. Однако в этом С++ коде есть проблема/ы. Сможете ли вы их найти?
Это не то, чтобы рубрика #ревью, особо никакой цели и предназначения у кода нет. Просто интересно, как много разнообразных проблем и недостатков вы сможете найти в этом небольшом отрывке кода. Пишите свои мысли в комментариях под этим постом.
Critique your solutions. Stay cool.
#fun
#опытным
Наткнулся на просторах всемирной сети на интересный пример:
#include <cstdio>
void bar(char * s) {
printf("%s", s);
}
void foo() {
char s[] = "Hi! I'm a kind of a loooooooooooooooooooooooong string myself, you know...";
bar(s);
}
int main() {
foo();
}
Код работает и пример довольно игрушечный. Однако в этом С++ коде есть проблема/ы. Сможете ли вы их найти?
Это не то, чтобы рубрика #ревью, особо никакой цели и предназначения у кода нет. Просто интересно, как много разнообразных проблем и недостатков вы сможете найти в этом небольшом отрывке кода. Пишите свои мысли в комментариях под этим постом.
Critique your solutions. Stay cool.
#fun
Ответ
Поговорим о том, что не так в коде из предыдущего поста:
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
Поговорим о том, что не так в коде из предыдущего поста:
#include <cstdio>
void bar(char * s) {
printf("%s", s);
}
void foo() {
char s[] =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
#include <print>
#include <string_view>
void bar(std::string_view s) {
std::println("{}", s);
}
void foo() {
constexpr std::string_view s =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
std::midpoint
#новичкам
Простая задача - получить среднее арифметическое двух чисел. Берем и пишем, как на уроке математики:
И дело в шляпе. Или нет?
На самом деле это некорректная реализация, потому что не учитывает переполнение целых чисел. Если сумма (a + b) будет больше, чем помещается в int, то произойдет переполнение, а вы в итоге получите неправильный ответ.
Что же делать?
Если несколько способов обойти эту проблему.
❗️ Складываем половинки двух чисел:
Даже если a и b - максимальные инты, все будет гуд. Проблему с переполнением решили.
Однако здесь появляются проблемы с двойным отбрасыванием остатка от деления. В случае передачи двух нечетных чисел, результат будет неверный:
💥 Первое число складываем с разницей двух чисел:
Если раскрыть скобки, то выходит тоже самое.
И проблем с корректностью нет.
⚡️ std::midpoint. С++20 мы наконец получили стандартную функцию, считающую среднее арифметическое двух объектов. Давайте посмотрим на ее реализацию из gcc:
Не будем вдаваться в подробности, однако стоит заметить, что стандартная функция использует оба подхода в разных ситуациях. Для целых чисел используется второй подход с вычитанием, а для чисел с плавающей точкой - с располовиниваем(так как не теряем остаток) и, даже, оригинальный подход, когда нет риска переполнения.
Да, может быть эта реализация не такая эффективная, зато гарантировано безопасная. Стандарт об этом явно говорит.
К тому же std::midpoint можно использовать для реализации бинарного поиска при нахождении индекса серединного элемента последовательности. Или для реализации алгоритмов «разделяй и властвуй», когда нужно найти индекс элемента, по которому будут разбивать последовательность пополам
В общем, если вы не упарываетесь по перфу, то она станет вашим верным другом.
Stay safe. Stay cool.
#cpp20 #cppcore
#новичкам
Простая задача - получить среднее арифметическое двух чисел. Берем и пишем, как на уроке математики:
int avg(int a, int b) {
return (a + b) / 2;
}
И дело в шляпе. Или нет?
На самом деле это некорректная реализация, потому что не учитывает переполнение целых чисел. Если сумма (a + b) будет больше, чем помещается в int, то произойдет переполнение, а вы в итоге получите неправильный ответ.
Что же делать?
Если несколько способов обойти эту проблему.
❗️ Складываем половинки двух чисел:
int avg(int a, int b) {
return a/2 + b/2;
}
Даже если a и b - максимальные инты, все будет гуд. Проблему с переполнением решили.
Однако здесь появляются проблемы с двойным отбрасыванием остатка от деления. В случае передачи двух нечетных чисел, результат будет неверный:
avg(5, 7) = 5 что неверно
💥 Первое число складываем с разницей двух чисел:
int avg(int a, int b) {
return a > b ? b + (a - b) / 2 : a + (b - a) / 2;
}
Если раскрыть скобки, то выходит тоже самое.
И проблем с корректностью нет.
⚡️ std::midpoint. С++20 мы наконец получили стандартную функцию, считающую среднее арифметическое двух объектов. Давайте посмотрим на ее реализацию из gcc:
// midpoint
#ifdef __cpp_lib_interpolate // C++ >= 20
template<typename _Tp>
constexpr
enable_if_t<__and_v<is_arithmetic<_Tp>, is_same<remove_cv_t<_Tp>, _Tp>,
_not<is_same<_Tp, bool>>>,
_Tp>
midpoint(_Tp __a, _Tp __b) noexcept
{
if constexpr (is_integral_v<_Tp>)
{
using _Up = make_unsigned_t<_Tp>;
int __k = 1;
_Up __m = __a;
_Up __M = __b;
if (__a > __b)
{
__k = -1;
__m = __b;
__M = __a;
}
return __a + __k * _Tp(_Up(__M - __m) / 2);
}
else // is_floating
{
constexpr _Tp __lo = numeric_limits<_Tp>::min() * 2;
constexpr _Tp __hi = numeric_limits<_Tp>::max() / 2;
const _Tp __abs_a = __a < 0 ? -__a : __a;
const _Tp __abs_b = __b < 0 ? -__b : __b;
if (__abs_a <= __hi && __abs_b <= __hi) [[likely]]
return (__a + __b) / 2; // always correctly rounded
if (__abs_a < __lo) // not safe to halve __a
return __a + __b/2;
if (__abs_b < __lo) // not safe to halve __b
return __a/2 + __b;
return __a/2 + __b/2; // otherwise correctly rounded
}
}
template<typename _Tp>
constexpr enable_if_t<is_object_v<_Tp>, _Tp*>
midpoint(_Tp* __a, _Tp* __b) noexcept
{
static_assert( sizeof(_Tp) != 0, "type must be complete" );
return __a + (__b - __a) / 2;
}
#endif // __cpp_lib_interpolate
Не будем вдаваться в подробности, однако стоит заметить, что стандартная функция использует оба подхода в разных ситуациях. Для целых чисел используется второй подход с вычитанием, а для чисел с плавающей точкой - с располовиниваем(так как не теряем остаток) и, даже, оригинальный подход, когда нет риска переполнения.
Да, может быть эта реализация не такая эффективная, зато гарантировано безопасная. Стандарт об этом явно говорит.
К тому же std::midpoint можно использовать для реализации бинарного поиска при нахождении индекса серединного элемента последовательности. Или для реализации алгоритмов «разделяй и властвуй», когда нужно найти индекс элемента, по которому будут разбивать последовательность пополам
В общем, если вы не упарываетесь по перфу, то она станет вашим верным другом.
Stay safe. Stay cool.
#cpp20 #cppcore
std::invoke
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
Согласитесь, что этот синтаксис отличается от
Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?
Использовать std::invoke. Это функция буквально создана, чтобы вызывать все, что только можно вызвать. Она конечно же написана на вариабельных шаблонах, чтобы вы могли передать туда все, что душе угодно:
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#cppcore #template
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::forward<Callback>(callback)(std::forward<Args>(args)...);
}
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
class Data {
public:
void memberFunction(int value) {
std::cout << "Data::memberFunction called with value: " << value << "\n";
}
};
Data data;
// Создаем указатель на метод класса
auto methodPtr = &Data::memberFunction;
// Вызываем метод через указатель на объекте
(data.*methodPtr)(42);
Согласитесь, что этот синтаксис отличается от
std::forward<Callback>(callback)(std::forward<Args>(args)...);
. Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?
Использовать std::invoke. Это функция буквально создана, чтобы вызывать все, что только можно вызвать. Она конечно же написана на вариабельных шаблонах, чтобы вы могли передать туда все, что душе угодно:
template< class F, class... Args >
std::invoke_result_t<F, Args...> invoke( F&& f, Args&&... args );
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::invoke(std::forward<Callback>(callback), std::forward<Args>(args)...);
}
struct S {
void foo(int i) {}
};
process_and_call(&S::foo, S{}, 42);
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#cppcore #template
Квиз
#опытным
В тему std::invoke закину вам интересный #quiz. Будем вызыватьведьм мемберы класса.
Что выведется на консоль в результате попытки компиляции и запуска следующего кода?
Explore details. Stay cool.
#опытным
В тему std::invoke закину вам интересный #quiz. Будем вызывать
Что выведется на консоль в результате попытки компиляции и запуска следующего кода?
#include <functional>
#include <iostream>
struct Data {
int memberFunction(int value) {
return value;
}
int field = 42;
};
int main() {
Data data;
auto methodPtr = &Data::memberFunction;
auto fieldPtr = &Data::field;
std::cout << std::invoke(methodPtr, data, std::invoke(fieldPtr, data)) << std::endl;
}
Explore details. Stay cool.
Что выведется на консоль в результате попытки компиляции и запуска следующего кода?
Anonymous Poll
24%
Я отказываюсь говорить без адвоката!
23%
Ошибка компиляции
8%
Возможно консоль вообще перестанет существовать, потому что в коде UB
38%
42
3%
Hello, World!
4%
0
Ответ
Мы прям недавно обсуждали, что std::invoke позволяет вызвать указатель на метод класса, это у вас не должно было вызвать вопросов. Самая загвоздка вот тут:
fieldPtr здесь - это указатель на нестатическое поле класса.
Скорее всего у вас возникли такие вопросы: "Что значит вызвать поле класса?! Это вообще легально?".
И ответ на этот вопрос довольно контринтуитивный. Да, std::invoke помогает единообразно вызвать все похожие на функции сущности. То есть запускать на выполнение код. Но поле класса - это вообще говоря не код, а участок памяти. А указатель на поле класса - это оффсет от начала объекта. На функции это вообще не похоже.
Тем не менее вызов поля класса с помощью std::invoke - легальная операция. И ее результат - значение этого поля.
Странно? Безусловно. Есть ли у этого применения? Конечно!
В C++20 алгоритмах библиотеки ranges есть специальный параметр проекции. Это вызываемая сущность, которая помогает преобразовывать элементы диапазона перед обработкой.
Допустим мы хотим найти в диапазоне объект с максимальным значением определенного поля. Как бы мы это делали без диапазонов:
Классика: определяем кастомный компаратор для сравнения элементов. Но заметьте сколько кода повторяется. Не легче ли просто один раз указать, что сравнивать надо по полю amount? И библиотека диапазонов позволяет нам это сделать!
Последний параметр проекции позволяет нам сказать, как нужно преобразовывать элементы последовательности перед сравнениями.
Но это все еще не идеал. Лямбда здесь кажется оверкиллом. Вот здесь-то вызов поля класса и пригождается:
Вот и все. Просто и красиво.
Под капотом алгоритмы рэнджей обязаны использовать std::invoke, чтобы универсально вызывать все переданные коллбэки. Поэтому такой финт ушами работает в них работает. И не работает в привычной STL.
Пользуйтесь диапазонами и проекторами. Это полезные штуки, которые ощутимо упрощают код.
Be laconic. Stay cool.
#cpp20 #STL
Мы прям недавно обсуждали, что std::invoke позволяет вызвать указатель на метод класса, это у вас не должно было вызвать вопросов. Самая загвоздка вот тут:
auto fieldPtr = &Data::field;
std::invoke(fieldPtr, Data{});
fieldPtr здесь - это указатель на нестатическое поле класса.
Скорее всего у вас возникли такие вопросы: "Что значит вызвать поле класса?! Это вообще легально?".
И ответ на этот вопрос довольно контринтуитивный. Да, std::invoke помогает единообразно вызвать все похожие на функции сущности. То есть запускать на выполнение код. Но поле класса - это вообще говоря не код, а участок памяти. А указатель на поле класса - это оффсет от начала объекта. На функции это вообще не похоже.
Тем не менее вызов поля класса с помощью std::invoke - легальная операция. И ее результат - значение этого поля.
Странно? Безусловно. Есть ли у этого применения? Конечно!
В C++20 алгоритмах библиотеки ranges есть специальный параметр проекции. Это вызываемая сущность, которая помогает преобразовывать элементы диапазона перед обработкой.
Допустим мы хотим найти в диапазоне объект с максимальным значением определенного поля. Как бы мы это делали без диапазонов:
struct Payment {
double amount;
std::string category;
}
std::vector<Payment>
payments = {{100.0, "food"}, {200.0, "transport"}, {150.0, "food"},
{300.0, "entertainment"}, {50.0, "transport"}, {250.0, "food"},
{120.0, "food"}};
auto max = *std::max_element(
transactions.begin(), transactions.end(),
[](const auto& item1, const auto& item2) { return item1.amount < item2.amount; });
Классика: определяем кастомный компаратор для сравнения элементов. Но заметьте сколько кода повторяется. Не легче ли просто один раз указать, что сравнивать надо по полю amount? И библиотека диапазонов позволяет нам это сделать!
auto max = *std::ranges::max_element(payments, {}, [](const auto& elem){return elem.amount;});
Последний параметр проекции позволяет нам сказать, как нужно преобразовывать элементы последовательности перед сравнениями.
Но это все еще не идеал. Лямбда здесь кажется оверкиллом. Вот здесь-то вызов поля класса и пригождается:
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);
Вот и все. Просто и красиво.
Под капотом алгоритмы рэнджей обязаны использовать std::invoke, чтобы универсально вызывать все переданные коллбэки. Поэтому такой финт ушами работает в них работает. И не работает в привычной STL.
Пользуйтесь диапазонами и проекторами. Это полезные штуки, которые ощутимо упрощают код.
Be laconic. Stay cool.
#cpp20 #STL
Все вызываемые сущности в С++. Ч1
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
👉🏿 Ссылки на функции
Да, и такие тоже есть:
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
👉🏿 Статические методы классов
Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
Уже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
#cppcore
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
free_function(42);
👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
std::invoke(free_function, 42);
Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
auto fun_ptr = &free_function;
fun_ptr(42);
(*fun_ptr)(42);
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
void free_function(int a) {
std::println("Free function: {}", a);
}
call_callback(free_function, 42);
👉🏿 Ссылки на функции
Да, и такие тоже есть:
auto& fun_ref = free_function;
fun_ptr(42);
(*fun_ptr)(42); // интересно, что такой синтаксис разрешен
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
void check_name(auto& obj) {
// by passing free_function to check_name, obj become function reference
}
check_name(free_function);
👉🏿 Статические методы классов
struct Handler {
static void static_method(int a) {
std::cout << "Static method: " << a << std::endl;
}
};
Handler::static_method(42);
Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
auto static_method_ptr = &Handler::static_method;
std::invoke(static_method_ptr, 42);
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
class SequenceGenerator {
int current;
int step;
public:
SequenceGenerator(int start = 0, int step_size = 1)
: current(start), step(step_size) {}
// Оператор вызова без аргументов - возвращает следующее число
int operator()() {
int val = current;
current += step;
return val;
}
};
SequenceGenerator gen;
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen); // заполняем вектор с помощью функтора
Уже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
#cppcore
Все вызываемые сущности в С++. Ч2
#опытным
Продолжаем перечислять все сущности, которые можно "вызывать" в С++.
И начинаем с продолжения перечисления всего, что относится к функторам:
👉🏿👉🏿 Лямбды
Там, где говорят про функторы, всегда водятся лямбды. Лямбды - это такие карманные функциональные объекты, которые определяются на ходу. У них также определен соответствующий оператор вызова operator(). Вот как может выглядеть код из предыдущего пункта с помощью лямбды:
Накидайте огней, если хотите узнать, как 3 строчки из лямбды в предыдущем посте превратились в одну с помощью std::exchange)
👉🏿👉🏿 Объект класса с определенным оператором приведения к указателю на функцию
Солидная часть подписчиков даже не слышала о такой сущности. Но работает это тривиально: при "вызове" такого объекта он кастится к указателю на функцию и уже с его помощью реально делается вызов:
Но даже, если вы ни разу не слышали про функторы такого вида, то с высокой долей вероятности все равно неявно пользовались им. Это конечно же лямбды без захвата.
👉🏿👉🏿 Лямбды без захвата. Именно с помощью оператора приведения к указателю на функцию лямбды без захвата можно маскировать под указатели на функции. Это позволяет использовать их в качестве коллбэков к сишным функциям или колдовать такую магию.
std::qsort принимает последним параметром именно указатель на функцию, поэтому конкретно в этом кейсе и срабатывает приведение к этому указателю.
В случае привычного вызова самой лямбды используется operator().
👉🏿 Нестатический метод
👉🏿 Указатель на нестатический метод. Если вы хотите передать метод класса в качестве коллбэка, то вам необходимо это делать через указатель на метод. Метод класса - это такая же функция, только первым параметром она неявно принимает this. Если делать явный вызов метода через указатель на него, то эта неявная передача становится явной:
Чтобы вызвать метод класса по его указателю через std::invoke нужно вторым параметром передать объект класса, на котором мы хотим вызвать метод. Далее идут аргументы в порядке, определенном в методе.
Замечу, что использования оператора взятия адреса(&) перед именем метода здесь обязательно. Имена нестатических методов не имеют желания неявно приводится к указателям, как имена функций. Вот так нельзя:
Это синтаксис для использования статических методов и компилятор будет ругаться.
👉🏿 Указатель на поле класса. Недавно обсуждали, что с помощью std::invoke можно "вызывать" поля класса. Неочевидно, но бывает полезно.
Фух. Вроде все. Пишите, если что забыл.
Have all the tools. Stay cool.
#cppcore
#опытным
Продолжаем перечислять все сущности, которые можно "вызывать" в С++.
И начинаем с продолжения перечисления всего, что относится к функторам:
👉🏿👉🏿 Лямбды
Там, где говорят про функторы, всегда водятся лямбды. Лямбды - это такие карманные функциональные объекты, которые определяются на ходу. У них также определен соответствующий оператор вызова operator(). Вот как может выглядеть код из предыдущего пункта с помощью лямбды:
int start = 5;
int step = 2;
auto gen = [current = start, step]() mutable {
return std::exchange(current, current + step);
};
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen);
Накидайте огней, если хотите узнать, как 3 строчки из лямбды в предыдущем посте превратились в одну с помощью std::exchange)
👉🏿👉🏿 Объект класса с определенным оператором приведения к указателю на функцию
Солидная часть подписчиков даже не слышала о такой сущности. Но работает это тривиально: при "вызове" такого объекта он кастится к указателю на функцию и уже с его помощью реально делается вызов:
class FunctionObjectCast {
public:
using fun_ptr = int ()(int);
// Оператор приведения к указателю на функцию
operator fun_ptr() {
return &staticMethod;
}
// Статический метод, который будет вызываться через указатель
static int staticMethod(int x) {
return x * 2;
}
};
FunctionObjectCast obj;
obj(42); // cast here
Но даже, если вы ни разу не слышали про функторы такого вида, то с высокой долей вероятности все равно неявно пользовались им. Это конечно же лямбды без захвата.
👉🏿👉🏿 Лямбды без захвата. Именно с помощью оператора приведения к указателю на функцию лямбды без захвата можно маскировать под указатели на функции. Это позволяет использовать их в качестве коллбэков к сишным функциям или колдовать такую магию.
std::vector<int> nums = {5, 3, 9, 1};
std::qsort(nums.data(), nums.size(), sizeof(int),
[](const void* a, const void* b) -> int {
return ((int)a - (int)b);});
std::qsort принимает последним параметром именно указатель на функцию, поэтому конкретно в этом кейсе и срабатывает приведение к этому указателю.
В случае привычного вызова самой лямбды используется operator().
👉🏿 Нестатический метод
struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};
Handler{}.foo(42);
👉🏿 Указатель на нестатический метод. Если вы хотите передать метод класса в качестве коллбэка, то вам необходимо это делать через указатель на метод. Метод класса - это такая же функция, только первым параметром она неявно принимает this. Если делать явный вызов метода через указатель на него, то эта неявная передача становится явной:
struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};
auto non_static_method_ptr = &Handler::foo;
std::invoke(non_static_method_ptr, Handler{}, 42);
Чтобы вызвать метод класса по его указателю через std::invoke нужно вторым параметром передать объект класса, на котором мы хотим вызвать метод. Далее идут аргументы в порядке, определенном в методе.
Замечу, что использования оператора взятия адреса(&) перед именем метода здесь обязательно. Имена нестатических методов не имеют желания неявно приводится к указателям, как имена функций. Вот так нельзя:
std::invoke(Handler::foo, Handler{}, 42);
Это синтаксис для использования статических методов и компилятор будет ругаться.
👉🏿 Указатель на поле класса. Недавно обсуждали, что с помощью std::invoke можно "вызывать" поля класса. Неочевидно, но бывает полезно.
struct Payment {
double amount;
std::string category;
}
Payment p{100500, "confetki"};
std::cout << std::invoke(&Payment:amount, p) << std::endl;
// OUTPUT:
// 100500
Фух. Вроде все. Пишите, если что забыл.
Have all the tools. Stay cool.
#cppcore