Грокаем C++
8.37K subscribers
34 photos
1 video
3 files
458 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Сравниваем производительности оператора<
#опытным

В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .

Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.

Сравним 4 реализации operator<:
struct Time_comparison_unreadable {
int hours;
int minutes;
int seconds;

bool operator<(const Time_comparison_unreadable& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes) || (hours == other.hours && minutes == other.minutes && seconds < other.seconds))
return true;
else
return false;
}
};

struct Time_comparison_readable {
// fields
bool operator<(const Time_comparison_readable& other) {
if (hours < other.hours) return true;
if (hours > other.hours) return false;
if (minutes < other.minutes) return true;
if (minutes > other.minutes) return false;
if (seconds < other.seconds) return true;
return false;
}
};

struct Time_tie {
// fields
bool operator<(const Time_tie& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};

struct Time_spaceship {
// fields
auto operator<=>(const Time_spaceship &) const = default;
};


Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.

Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:

std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(1,100);

template <typename TimeClass>
static void time_comparison_experiment(benchmark::State& state) {
std::vector<TimeClass> v(1'000'000);
std::generate(v.begin(), v.end(), [&] () -> TimeClass { return TimeClass{ dist(rng) % 24, dist(rng) % 60, dist(rng) % 60 }; });
while (state.KeepRunning()) {
auto start = std::chrono::high_resolution_clock::now();
std::sort(v.begin(), v.end());
auto end = std::chrono::high_resolution_clock::now();

auto elapsed_seconds =
std::chrono::duration_cast<std::chrono::duration<double>>(
end - start);
state.SetIterationTime(elapsed_seconds.count());
std::shuffle(v.begin(), v.end(), rng);
}
}

BENCHMARK(time_comparison_experiment<Time_comparison_unreadable>)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment<Time_comparison_readable>)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment<Time_tie>)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment<Time_spaceship>)->UseManualTime()->Iterations(20);


Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.

В конце мы запускаем бенчмарк над функцией с измерением времени выполнения, говорим ему, что мы сами будет мерять время(UseManualTime), и сколько итераций цикла нужно выполнить(Iterations(20)).

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Compare things. Stay cool.

#performance #cpp20
Виртуальные функции в compile-time
#опытным

Виртуальные функции являются средством реализации динамического полиморфизма в С++. Почему он вообще называется динамическим?

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

Но что, если я вам скажу, что мы можем реализовывать полиморфизм времени компиляции с помощью виртуальных функций?

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

Начиная с С++11 у нас есть constexpr функции. Эти функции могут быть вычислены на этапе компиляции, если их аргументы также известны на этом этапе. Аргументы могут быть константами, литералами, constexpr переменными или результатом вычисления других constexpr функций.

constexpr int double_me(int n)
{
return n * 2;
}
// условие верное и мы не падаем
static_assert(double_me(4) == 8);
// условие ложно и компиляция прервется на этой строчке
static_assert(double_me(4) == 7);


В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.

Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.

Начиная с С++20 constexpr функции могут быть виртуальными!

struct VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const = 0;
};

struct Impl: VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const override
{
return 2 * n;
}
};

constexpr auto impl = Impl{};
// для полиморфизма с виртуальными функциями нужна ссылка
constexpr const VeryComplicatedCaclulation& impl_ref = impl;

constexpr auto a = impl_ref.double_me(4);
static_assert(a == 8); // true


Все как мы привыкли: делаем иерархию классов с виртуальной функцией, только везде на всех этапах приписываем constexpr. И это работает!

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

Increase your usability. Stay cool.

#cpp11 #cpp20 #cppcore
Виртуальные функции в compile-time Ч2
#опытным

Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у 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
Еще одно отличие С и С++
#опытным

Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.

В С давно можно инициализировать структуры с помощью так называемой 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
Designated initialization
#новичкам

В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое 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
Гайд на тестовое задание
#новичкам

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

👉🏿 вероятное отсутствие нормальных проектов на гитхабе у кандидата.

👉🏿 возможно единственное, что от джуна нужно - это чтобы он мог писать более менее адекватный код по четким расписанным требованиям. Это сложно проверить в рамках короткого собеседования. И очень легко в форме тестового.

👉🏿 конкуренция на джуновскую вакансию высокая, но качество кандидатов очень низкое. Людей надо ненапряжно отсеивать. Тестовое задание отлично подходит для этой работы. Не все даже закончат его. А ревьюить небольшое тестовое джуна дома в халатике за чашечкой чайного гриба намного быстрее и комфортнее, чем ходить на кучу очных встреч.

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

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

Не знаете, как оформлять тестовое? Тогда Грокаем С++ идет к вам!

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

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

Текст получился достаточно большим, поэтому опубликовали его в telegraph. Вот ссылочка на гайд.

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

ЗЫ: Как только решите сдавать тестовое на проверку - пройдитесь по этому чеклисту. Возможно в ходе разработки вы на что-то не обратили внимание и сможете своевременно исправить эти недостатки.

Желаю всем писать как можно меньше неоплачиваемых тестовых. А если уже пишите, то делайте это качественно.

The end.

Provide quality. Stay cool.
Тип возвращаемого значения тернарного оператора
#опытным

Представьте, что вам пришел какой-то запрос с 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
Особый день

Хоть весна нас особо не греет, сегодня очень важный и теплый для нашей страны праздник - День Победы.

К нему можно по-разному относиться, непростая ситуация сейчас в мире. Но, на наш взгляд, это не имеет значения.

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

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

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

С праздником, дорогие подписчики! Благодарность свернет горы.

Tip your hat to your ancestors. Stay cool.
Частичная специализация шаблонов функций
#опытным

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

Частичной специализации шаблонов функции не существует. И точка!

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

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

Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.

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

void f(int) { std::cout << "int-overload" << std::endl; }; 
void f(int*){ std::cout << "int-p-overload" << std::endl; }

template<class T> void f(T) { std::cout << "T-overload" << std::endl; };
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }


Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.

Частичная специализация же характерна только для шаблонов классов и переменных.


template<typename T>
class Foo {};

template<typename T>
class Foo<T*> {};


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

template <typename T1>
struct Foo<T1> {};
// Так нельзя делать, это несвязанные шаблоны
template <typename T1, typename T2>
struct Foo<T1,T2> {};


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

template<class T> void f(T) { std::cout << "T-overload" << std::endl; }; 
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }

template<class T> void f<T*>(T*){std::cout << "T-p-specialization" << std::endl;}


Ну и как компилятору выбирать между T-p-overload и T-p-specialization?

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

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

Don't be confused. Stay cool.

#template #cppcore
Перегружаем шаблоны классов
#опытным

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

В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.

Но как и практически любое ограничение в С++, его можно хакнуть.

Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.

template <typename... T>
struct Foo;

template <typename T1>
struct Foo<T1> {};

template <typename T1, typename T2>
struct Foo<T1,T2> {};


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

Вот такие фокусы.

Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:

template <typename T, int N>
struct Foo<T, N> {}; // forbidden


Но все равно это хороший инструмент, которым можно пользоваться.

Hack the boundaries. Stay cool.

#template #cppcore
Квиз

Сегодня будет интересный #quiz из малоизвестной области плюсов. А именно дефолтные параметры виртуальных методов. У них немного неинтуитивное поведение. Так что давайте проверим, насколько ваша интуиция вам врет.

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

У меня к вам всего один вопрос. Каков результат попытки компиляции и запуска следующего кода под С++20?

#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();
}


Challenge your life. Stay cool.
Какой результат попытки компиляции и запуска кода выше?
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, за идею для поста)

Правильный ответ из поста выше - 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. С его помощью мы указываем, что не предполагается, что функция может бросать исключения.

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:

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. То есть:

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
#новичкам

Теперь мы разобрались с основным кейсом. Давайте подробнее рассмотрим, что будет, если мы будет работать с явно бросающими деструктором.

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

Вот так:

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" в деструкторе. Думаю, что на самом деле вы хотите использовать в деструкторе функцию, которая потенциально может бросить исключение:

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(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.

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> &params);
// ....
~Calculator()
{
try {
Logger logger("log.txt");
Logger.Log("Calculator destroyed");
}
catch (...) {
// ....
}
}
};

int64_t Process(const std::vector<std::string> &params) {
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