Виртуальные функции в compile-time Ч2
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
class error_code
{
private:
int val_;
const error_category* cat_;
public:
constexpr error_code() noexcept;
constexpr error_code(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code(ErrorCodeEnum e) noexcept;
constexpr void assign(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code& operator=(ErrorCodeEnum e) noexcept;
constexpr void clear() noexcept;
constexpr int value() const noexcept;
constexpr const error_category& category() const noexcept;
constexpr explicit operator bool() const noexcept;
error_condition default_error_condition() const noexcept;
string message() const;
};
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
error_category*, наследникам которого мы и можем делигировать принятие решения о том, является ли значение ошибкой или нет.class error_category
{
public:
// ...
virtual bool failed(int ev) const noexcept;
// ...
};
// И добавляем метод в класс error_code
class error_code
{
// ...
bool failed() const noexcept { return cat_->failed(val_); }
// ...
};
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
❤18👍13🔥10⚡1
Еще одно отличие С и С++
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
В плюсах такое можно сделать только с помощью нескольких инструкций.
Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
int array[100] = {[13] = 1, [45] = 1, [79] = 1};В плюсах такое можно сделать только с помощью нескольких инструкций.
int array[100] = {};
array[13] = array[45] = array[79] = 1;Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
int array[100] = {[13] = 1, [30 ... 40] = 1, [45] = 1, [79] = 1};Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
struct point { int x, y, z; };
struct point p1 = { .y = 2, .x = 3 };
struct point p2 = { y: 2, x: 3 };
struct point p3 = { x: 1};Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
1🔥29👍10❤6❤🔥1
Designated initialization
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
Хоть
Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
Несмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
struct Person {
std::string name;
std::string surname;
std::string id;
};
struct Item {
std::string name;
double price;
std::string id;
};
struct Order {
Person person;
Item purchase;
std::string pick_up_address;
};Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
Order order{.person = {.name = "Golum",
.surname = "Iz shira",
.id = "666"},
.purchase = {.name = "Precious",
.price = 9999999.9,
.id = "13"},
.pick_up_address = "Mordor"};То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
struct Person
{
std::string name;
std::string surname;
unsigned age;
};
struct Employee : Person
{
unsigned salary;
};
Employee e1{ { .name{"John"}, .surname{"Wick"}, .age{40} }, 50000 };
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
struct Point{
int x, y, z;
};
Point p{.x = 2, .z = 3}; // y is not mentioned, but it will have value of 0Хоть
y строит в середине, но это не мешает нам не указывать его при создании класса и это поле гарантированно будет равно 0.Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
struct Point{
int x, y;
};
Point p{.y = 2, .x = 3}; // not valid in C++!Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
struct Point{
int x, y;
};
Point p{2, .y = 3}; // Not allowedНесмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
2🔥34👍15❤10
Тип возвращаемого значения тернарного оператора
#опытным
Представьте, что вам пришел какой-то запрос с json'ом и вам его нужно переложить в плюсовую структуру и дальше как-то ее обрабатывать. В джейсоне записаны какие-то персональные данные человека, но они не всегда присутствуют в полном составе. Давайте посмотрим на структуру, чтобы было понятнее:
Пусть мы обрабатываем какие-то анкетные данные или что-то в таком духе. И человеку обязательно указать свои ФИО, но адрес, эл. почту и телефон - не обязательно.
Берем джейсон и перекладываем(то есть занимаемся тем, чему 6 лет учат в тех вузах):
Просто, чтобы кучу if'ов не плодить, воспользуемся тернарным оператом. Если в json'е есть данное поле, то инициализируем опциональное поле им, если нет, то std::nullopt'ом.
Ничего криминального не сделали. Вдобавок использовали designated initialization из с++20.
Компилируем ииииииии..... Ошибка компиляции.
Пишет, что тернарный оператор не может возвращать разные типы.
Дело в том, что std::nullopt - это константа типа nullopt_t. А поле джейсона имеет тип строки. Конечно, из обоих типов можно сконструировать объект std::optional. Но тернарный оператор не знает, что мы хотим. Ему просто не разрешается возвращать разные типы.
Но почему? Это же так удобно.
С++ - это не всегда про удобство)
Представьте себе шаблонную функцию, возвращаемый тип которой выводит сам компилятор. Условно - try_stoi. Если строка может быть преобразована в int, то возвращаем число, если нет - то возвращаем нетронутую строку.
Разные ветки возвращают неконвертируемые друг в друга типы, поэтому компилятор не сможет вывести единый тип и произойдет ошибка компиляции. Если же явно проставить тип возвращаемого значения std::optional, то все заработает.
Однако для тернарного оператора мы не можем определить тип возвращаемого значения, за нас это неявно делает компилятор. Поэтому, если две ветки условия возвращают неконвертируемые типы, то мозги компилятора бухнут и он отказывается работать..
Придется явно оборачивать все в std::optional:
Это немного снижает читаемость и привносит кучу повторения кода, но имеем, что имеем.
Be flexible. Stay cool.
#cppcore #cpp17 #cpp20
#опытным
Представьте, что вам пришел какой-то запрос с json'ом и вам его нужно переложить в плюсовую структуру и дальше как-то ее обрабатывать. В джейсоне записаны какие-то персональные данные человека, но они не всегда присутствуют в полном составе. Давайте посмотрим на структуру, чтобы было понятнее:
struct PersonalData {
std::string name;
std::string surname;
std::string patronymic;
std::optional<std::string> address;
std::optional<std::string> email;
std::optional<std::string> phone;
};Пусть мы обрабатываем какие-то анкетные данные или что-то в таком духе. И человеку обязательно указать свои ФИО, но адрес, эл. почту и телефон - не обязательно.
Берем джейсон и перекладываем(то есть занимаемся тем, чему 6 лет учат в тех вузах):
PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? json["address"] : std::nullopt,
.email = json.HasMember("email") ? json["email"] : std::nullopt,
.phone = json.HasMember("phone") ? json["phone"] : std::nullopt};Просто, чтобы кучу if'ов не плодить, воспользуемся тернарным оператом. Если в json'е есть данное поле, то инициализируем опциональное поле им, если нет, то std::nullopt'ом.
Ничего криминального не сделали. Вдобавок использовали designated initialization из с++20.
Компилируем ииииииии..... Ошибка компиляции.
Пишет, что тернарный оператор не может возвращать разные типы.
Дело в том, что std::nullopt - это константа типа nullopt_t. А поле джейсона имеет тип строки. Конечно, из обоих типов можно сконструировать объект std::optional. Но тернарный оператор не знает, что мы хотим. Ему просто не разрешается возвращать разные типы.
Но почему? Это же так удобно.
С++ - это не всегда про удобство)
Представьте себе шаблонную функцию, возвращаемый тип которой выводит сам компилятор. Условно - try_stoi. Если строка может быть преобразована в int, то возвращаем число, если нет - то возвращаем нетронутую строку.
auto try_stoi(const std::string& potential_num) {
if (can_be_converted_to_int(potential_num)) {
return std::stoi(potential_num);
} else {
return potential_num;
}
}Разные ветки возвращают неконвертируемые друг в друга типы, поэтому компилятор не сможет вывести единый тип и произойдет ошибка компиляции. Если же явно проставить тип возвращаемого значения std::optional, то все заработает.
Однако для тернарного оператора мы не можем определить тип возвращаемого значения, за нас это неявно делает компилятор. Поэтому, если две ветки условия возвращают неконвертируемые типы, то мозги компилятора бухнут и он отказывается работать..
Придется явно оборачивать все в std::optional:
PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? std::optional(json["address"]) : std::nullopt,
.email = json.HasMember("email") ? std::optional(json["email"]) : std::nullopt,
.phone = json.HasMember("phone") ? std::optional(json["phone"]) : std::nullopt};Это немного снижает читаемость и привносит кучу повторения кода, но имеем, что имеем.
Be flexible. Stay cool.
#cppcore #cpp17 #cpp20
🔥37👍24❤10😁1
Что будет если бросить исключение в деструкторе? Ходим по тонкому льду.
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "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
2👍30🔥13❤8
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
❤35🔥10👍8😁3
Ответ
Мы прям недавно обсуждали, что 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
❤🔥29👍15❤10🔥6😱4
std::mem_fn
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
Какие варианты у нас есть в такой ситуации?
❗️ std::function
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
struct Task {
int execute() {
return 42;
}
};
std::vector<Task> tasks(10);
std::vector<int> results;Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
results.reserve(tasks.size());
for (auto& task: tasks) {
result.emplace_back(task.execute());
}
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Use standard algorithms where appropriate, instead of writing some own implementation.Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
std::vector results = tasks |
std::views::transform(&Task::execute) |
std::ranges::to<std::vector>();
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), &Task::execute); // Not working!
Какие варианты у нас есть в такой ситуации?
❗️ std::function
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::function<int(Task&)>(&Task::execute));
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), [](auto& input) { return input.execute(); });
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так
(x.*&Item::Foo)(), а вот так: (&Item::Foo)(x). То есть позволяет унифицировать синтаксис вызова указателя на метод класса с синтаксисом вызова обычной функции. С помощью std::mem_fn наш код трансформации выглядит вот так:std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::mem_fn(&Task::execute));
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL
❤26👍15🔥11❤🔥2
Capture this
#новичкам
Бывают ситуации, когда вы хотите зарегистрировать коллбэк, в котором будет выполняться метод текущего класса.
Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку graceful shutdown. Для этого получаем объект обработчика сигнала и регистрируем коллбэк на SIGINT и SIGTERM:
Сработает ли такой код?
Он не соберется. Будет ошибка:
Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:
До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:
Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.
Be explicit. Stay cool.
#cppcore #cpp11 #cpp20
#новичкам
Бывают ситуации, когда вы хотите зарегистрировать коллбэк, в котором будет выполняться метод текущего класса.
Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку graceful shutdown. Для этого получаем объект обработчика сигнала и регистрируем коллбэк на SIGINT и SIGTERM:
struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [](){
Shutdown();
});
}
void Shutdown() {...}
};Сработает ли такой код?
Он не соберется. Будет ошибка:
'this' was not captured for this lambda function. Методу Shutdown нужен объект, на котором его нужно вызвать.Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:
struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [this](){
// this->Shutdown(); - don't need this syntax
Shutdown();
});
}
void Shutdown() {...}
};До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [= /*& also works*/](){ // works
Shutdown();
});Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.
Be explicit. Stay cool.
#cppcore #cpp11 #cpp20
👍23❤5❤🔥4🔥2👏1
std::to_address
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
Use the right tool. Stay cool.
#cpp20 #cppcore
#опытным
В этом посте мы поговорили о том, как доставать настоящий адрес объекта с помощью функции std:addressof. В основном она предназначена для получения настоящего адреса любых объектов, даже тех, у кого перегружен оператор взятия адреса.
Однако есть и другая, похожая задача. Вам приходит на вход объект, который представляет из себя какого-то рода указатель на объект и из него нужно получить адрес самого объекта.
Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение:
obj.operator->(). Однако для простых указателей это не прокатит: они не классы и у них нет методов. Да и не прокатит для любых других объектов, у которых не определен этот оператор. Что делать?Использовать C++20 функцию std::to_address! Вот ее примерная реализация:
template<class T>
constexpr T* to_address(T* p) noexcept {
static_assert(!std::is_function_v<T>);
return p;
}
template<class T>
constexpr auto to_address(const T& p) noexcept {
if constexpr (requires{ std::pointer_traits<T>::to_address(p); })
return std::pointer_traits<T>::to_address(p);
else
return std::to_address(p.operator->());
}
То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().
Обычно эта функция требуется для вызова сишного апи в обобщенном коде:
void c_api_func(const int*);
template<typename T>
void call_c_api_func(T && obj) {
c_api_func(std::to_address(obj));
}
std::vector<int> data{10, 20, 30};
call_c_api_func(data.begin()); // works
auto ptr = std::make_unique<int>(42);
call_c_api_func(ptr); // works
call_c_api_func(ptr.get()); // also works
Use the right tool. Stay cool.
#cpp20 #cppcore
2❤24👍10🔥7😁3