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

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Что будет если бросить исключение в деструкторе? Уверенно ходим по тонкому льду.
#опытным

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
👍28🔥117
​​Ответ

Поговорим о том, что не так в коде из предыдущего поста:

#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
27👍16🔥7👎4🤷‍♂1
​​std::from_chars
#новичкам

С++17 нам принес новую прекрасную функцию парсинга строк в числа - std::from_char.

std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
IntegerType& value, // Куда записать результат
int base = 10 // Система счисления (2-36)
);

std::from_chars_result from_chars(
const char* first, // Начало строки (включительно)
const char* last, // Конец строки (не включительно)
FloatType& value, // Куда записать результат
std::chars_format fmt = std::chars_format::general // Формат плавающей точки
);


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

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

Функция возвращает структуру std::from_chars_result:

struct from_chars_result {
const char* ptr; // Указатель на первый НЕпрочитанный символ
std::errc ec; // Код ошибки (если успех — std::errc())
};


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

Если парсинг неудался, то ptr равен first, а код ошибки ec выставляется в  std::errc::invalid_argument.

"123" → удачно распарсили все → ptr == last (конец строки).
"123abc" → распарсили "123" → ptr указывает на 'a'.
"abc" → ошибка → ptr == first (начало строки).


Примеры работы:


const std::string str = "42abc";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
if (res.ec == std::errc()) {
std::cout << "Value: " << value << "\n"; // 42
std::cout << "Remaining: " << res.ptr << "\n"; // "abc"
}

// ----------------

const std::string str = "xyz";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);

assert(res.ec == std::errc::invalid_argument);
assert(res.ptr == str.data()); // ptr остался на начале


К тому же функция может детектировать переполнение:

const std::string str = "99999999999999999999";
int value;
auto res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(res.ec == std::errc::result_out_of_range);


В чем главный прикол этой функции?


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

const std::string str = "123,456,789";
std::vector<int> numbers;
const char* current = str.data();
const char* end = str.data() + str.size();

while (current < end) {
int value;
auto res = std::from_chars(current, end, value);
if (res.ec != std::errc()) {
std::cerr << "Parsing error!\n";
break;
}

numbers.emplace_back(value);
current = res.ptr; // Сдвигаем указатель
// Пропускаем разделитель (запятую)
if (current < end && *current == ',') {
++current;
}
}

for (int num : numbers) {
std::cout << num << " ";
}
// Вывод: 123 456 789


К тому же ее целочисленный вариант с С++23 constexpr, что позволить вам парсить строку в числа даже во время компиляции.

Если вы не любите исключения - std::from_char ваш выбор.

Be efficient. Stay cool.

#cpp17 #cpp23
1🔥4218👍15
​​std::to_chars
#новичкам

В C++17 появилась не только функция для парсинга (std::from_chars), но и её обратная версия — std::to_chars, которая позволяет конвертировать числа (int, float, double и др.) в строки без дополнительных затрат на выделение памяти и поддержку исключений.

std::to_chars_result to_chars(char* first, char* last,
IntegerType value,
int base = 10); // (1)

std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt); // (2)

std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt,
int precision); // (3) (since C++23)
struct to_chars_result {
const char* ptr;
std::errc ec;
};


Единственный случай, когда эта функция может зафейлиться - если вы передали слишком маленький буффер. Тогда ec выставляется в std::errc::value_too_large, а ptr в last.

В чем преимущество функции по сравнению со старой-доброй std::to_string?


💥 Можно задать точность и основание системы счисления.

💥 Отсутствуют динамические аллокации внутри функции.

💥 Возможность использовать любые char буферы, а не только строки.

Вот вам пример работы:
char buffer[20];
int value = 12345;

auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buffer;
std::string_view str_result(buffer, result_length);
std::cout << "Result: " << str_result << "\n"; // "12345"
}

// ------------------------

double pi = 3.1415926535;
char buf[20];

auto result = std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::fixed, 4);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buf;
std::string_view str_result(buf, result_length);
std::cout << "Result: " << str_result << "\n"; // "3.1416"
}


Не всегда проблема точности и аллокаций - это реальная проблема. Но если вы работаете с ограниченной кучей и хотите стандартное средство сериализации числа в буфер - std::to_char как раз для вас.

Be efficient. Stay cool.

#cpp17
24👍15🔥11
​​Разница между std::stoi+std::to_string и std::from_chars+std::to_chars
#опытным

В C++ есть два основных подхода к конвертации чисел в строки и обратно:

Старомодный  — std::stoi, std::to_string (C++11)

Модномолодежный — std::from_chars, std::to_chars (C++17)

В чем принципиальная разница между ними и когда какой подход использовать? Сегодня мы широкими мазками ответим на эти вопросы.

Особенности старомодного подхода:

👉🏿 Основное - это исключения. Все нештатные ситуации обрабатываются с их помощью, что ведет к неким накладным расходам.

👉🏿 Работа в высокоуровневом ООП стиле. Используются классы и возвращаются классы, без всяких сырых буферов.

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

👉🏿 Поддержка локалей. Грубо говоря, это механизм для учёта региональных особенностей представления данных. То есть std::stoi, std::to_string реализованы с учетом возможности спецификации локалей и соотвественно изменения результатов конвертации. С локалями возможна такая штука:
// В США (локаль "en_US"):
std::to_string(3.14); // "3.14" (точка как разделитель)

// В Германии (локаль "de_DE"):
std::to_string(3.14); // Может вернуть "3,14" (запятая)!
Естественно, что поддержка такой фичи чего-то да стоит.

Особенности модномолодежного подхода:

👉🏿 Функции std::from_chars, std::to_chars спроектированы быть настолько легкими и быстрыми, насколько это возможно на таком уровне абстракции.

👉🏿 Отсутствие намеренных динамических аллокаций. Только вы решаете, где расположена память по данные.

👉🏿 Отсутствие исключений. Функции возвращает объект ошибки, который явно нужно проверять руками.

👉🏿 Не проверяет локали.

👉🏿 Поддерживают частичный парсинг.

👉🏿 Поддержка явной гарантии round-trip. Если вы запишите в строку число с помощью std::to_chars и прочитаете его с помощью std::from_chars, то вы всегда получите изначальный результат. Главное, чтобы обе функции были вызваны с использованием одинаковой реализации стандартной библиотеки. Но у std::stoi, std::to_string и этого нет.


Если вы работаете в высоконагруженном или ограниченном по производительности окружении, то ваш выбор явно std::from_chars, std::to_chars. Обычно в коде таких приложений отказываются от использования исключений, поэтому проблем с код-стайлом не будет.

Возможность поэтапного парсинга также не оставляет выбора - используйте std::from_chars.

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

Choose the right tool. Stay cool.

#cppcore #cpp11 #cpp17
12🔥2213👍10😱1
constexpr функции сквозь года
#новичкам

constexpr бывают не только переменные, но и функции. Такие функции могут быть выполнены, как в compile-time, так и в runtime, в зависимости от контекста вызова:
- Если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции.
- Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime.
- Если вы попытаетесь присвоить возвращаемое значение функции с runtime аргументом constexpr переменной, то получите ошибку компиляции.

constexpr int square(int x) {
return x * x;
}

int main() {
constexpr int val = square(5); // compile-time calculations
static_assert(val == 25, "Must be 25"); // compile-time check
int arg = rand() % 25;
int res = square(arg); // runtime calculations
assert(res == arg*arg); // runtime check
constexpr int fail = square(arg); // compile error here!
}


Пройдемся по стандартам языка и посмотрим, как в них изменялись constexpr функции.

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


constexpr int factorial(int n) {
// ternary operator is allowed, so recursion
return (n <= 1) ? 1 : n * factorial(n - 1);
}


В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:

constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

static_assert(factorial(5) == 120, "Must be 120");


C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:

constexpr auto factorial = [](int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
};

static_assert(factorial(5) == 120, "Must be 120");


C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.

constexpr std::string create_greeting() {
std::string s = "Hello, ";
s += "C++20!";
return s;
}

static_assert(create_greeting() == "Hello, C++20!");


constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.

В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).

Free up time for important things. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥31👍116👎2
История capture this
#опытным

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

С++11

Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается &(*this) то есть указатель на текущий объект:

struct Foo {
int m_x = 0;

void func() {
int x = 0;

//Explicit capture 'this'
[this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[&]() { /*access m_x and x*/ }();

//Redundant 'this'
[&, this]() { /*access m_x and x*/ }();

//Implcit capture 'this'
[=]() { /*access m_x and x*/ }();

//Error
[=, this]() { }();
}
};


Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.

С++14

Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:

struct Foo {
int m_x = 0;

void func() {
[copy=*this]() mutable {
copy.m_x++;
}();
}
};


В остальном все осталось также.

С++17

Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:

struct Processor {
//Some state data..

std::future<void> process(/*args*/) {
//Pre-process...
//Do the data processing asynchronously
return
std::async(std::launch::async,
[=](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

auto caller() {
Processor p;
return p.process(/*args*/);
}


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

struct Processor {
std::future<void> process(/*args*/) {
return
std::async(std::launch::async,
[*this](/*data*/){
/*
Runs in a different thread.
'this' might be invalidated here
*/

//process data
});
}
};

В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.

С++20

С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.

Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.

Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:

struct Bagel {
int x = 0;
void func() {
//OK until C++20. Warning in C++20.
[=]() { std::cout << x; }();

//Error/warning until C++20. OK in C++20.
[=, this]() { std::cout << x; }();
}
};


Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.

Know the history. Stay cool.

#cpp11 #cpp14 #cpp17 #cpp20
🔥1610👍10🤯6
Динамический полиморфизм: std::variant + std::visit
#опытным

Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же std::function - сочетание виртуальных функций и шаблонов. Но о подробностях реализации в другом посте.

Другой пример - std::variant + std::visit. std::variant - шаблонный класс, который может хранить в себе объект любого типа, который есть среди его шаблонных параметров. Эдакий типобезопасный union, без UB и прочей грязи.

std::variant<int, float, std::string> value;
value = 3.14f; // valid
value = 42; // also valid
value = std::string{"You are the best!"}; // again valid
value = 3.14; // ERROR: 3.14 is double and double is not in template parameter list


Вариант позволяет складывать фиксированный набор типов в один контейнер:
std::vector<std::variant<int, float, std::string>> vec;
vec.push_back(3.14f);
vec.push_back(42);


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

Через std::visit, конечно. Эта функция, которая принимает функциональный объект, который можно вызвать для любого типа, потенциально хранящегося в варианте, к самому объекту варианта. Объект std::variant на самом деле знает, какой тип в нем хранится, просто нам он об этом не рассказывает. А std::visit'у рассказывает:

struct PrintVisitor {
void operator()(int x) { cout << "int: " << x; }
void operator()(float x) { cout << "float: " << x; }
void operator()(string s) { cout << "string: " << s; }
};

std::variant<int, float, string> value;
value = 3.14f;

std::visit(PrintVisitor{}, value); // Prints "float: 3.14"


По-настоящему мощным это сочетание ставится при применении паттерна overload:

template<typename ...Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};

using var_t = std::variant<int, double, std::string>;

void Worker(const std::vector<var_t>& vec){
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}


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

Visit your closest. Stay cool.

#cpp17 #template
👍30🔥1912❤‍🔥2🤯2
Одно значимое улучшение С++17
#опытным

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

Возьмем, например, вызов функции:

f( g(expr1, expr2), h(expr3) );


В каком порядке вызываются expr1, expr2, expr3, g, h и f?

Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.

Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.

Что было до С++17?

Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!

То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.

Полный бардак! И это приводило на самом деле к неприятным последствиям.

Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:

void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}

bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));


Какие тут могут быть проблемы?

Итоговый порядок вычислений может быть следующий:

new SomeClass1{} -> new SomeClass2{} -> std::unique_ptr<SomeClass1> -> std::unique_ptr<SomeClass2>


Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.

Эту проблему решали с помощью std::make_* фабрик умных указаателей:

bar(std::make_unique<SomeClass1>(), std::make_unique<SomeClass2>());


Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.

Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.

Что стало с наступлением С++17?

f(e(), g(expr1, expr2), h(expr3));


До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.

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

Теперь такой код не будет проблемой:

void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}

bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));


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

Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.

Fix problems. Stay cool.

#cppcore #memory #cpp17
👍3615🔥10❤‍🔥4
Идиома IILE
#опытным

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

const int myParam = inputParam * 10 + 5;
// or
const int myParam = bCondition ? inputParam * 2 : inputParam + 10;


Но что делать, если переменная по сути своей константа, но у нее громоздская инициализация на несколько строк?

int myVariable = 0; // this should be const...

if (bFirstCondition)
myVariable = bSecondCindition ? computeFunc(inputParam) : 0;
else
myVariable = inputParam * 2;

// more code of the current function...
// and we assume 'myVariable` is const now


По-хорошему это уносится в какую-нибудь отдельную функцию. Но тогда теряется контекст и нужно будет прыгать по коду.

Хочется и const сделать, и в отдельную функцию не выносить. Кажется, что на двух стульях не усидишь, но благодаря лямбдам мы можем это сделать!

Есть такая идиома IILE(Immediately Invoked Lambda Expression). Вы определяете лямбду и тут же ее вызываете. И контекст сохраняется, и единовременность инициализации присутствует:

const int myVariable = [&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
}(); // call!

Пара лишних символов, зато проблема решена.

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

Тоже не беда! Используем std::invoke:

const int myVariable = std::invoke([&] {
if (bFirstContidion)
return bSecondCondition ? computeFunc(inputParam) : 0;
else
return inputParam * 2;
});


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

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

Be expressive. Stay cool.

#cpp11 #cpp17 #goodpractice
🔥48👍2215👎4🤷‍♂3❤‍🔥2
Возврат ошибки. std::variant
#новичкам

Если у вас есть С++17, то поздравляю, у вас есть std::variant, который решает проблему суперпозиции полей из прошлого поста.

По сути, std::variant - это типобезопасный юнион, который хранит только один тип из списка шаблонных параметров. Объект варианта можно проверить на наличие нужного типа и есть способы no-exceptions сообщения об ошибке, если вы хотите получить доступ не к тому типу. Обычный std::get кидает исключение при неправильном доступе, но std::holds_alternative или std::get_if предоставляют небросающий апи:

struct Error {
std::string message;
};

std::variant<double, Error> safe_divide(double a, double b) {
if (b == 0.0) {
return Error{std::string{"Division by zero"}};
}
return a / b;
}

auto div_result = safe_divide(10.0, 2.0);
if (std::holds_alternative<double>(div_result)) {
std::cout << "Result: " << std::get<double>(div_result) << std::endl;
} else {
std::cout << "Error: " << std::get<Error>(div_result).message << std::endl;
}


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

К этому привело то, что семантика "результат или ошибка" не заложена в самой идее std::variant. Для него все типы изначально равнозначны и равноожидаемы. Нет плохого и хорошего типа.

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

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

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

В небросающем коде нужно обязательно проверять каждый доступ к объекту варианта, потому что std::get кидает исключение(вообще говоря, в любом коде). Ну или сразу используйте std::get_if, если точно знаете, какой должен быть тип, но нужно подстраховаться от ошибок.

Это хороший вариант, но больно пользоваться. Хочется более элегантного решения решения для этой проблемы. И оно есть! О этом в следующем посте.

Use a right semantic. Stay cool.

#cpp17
18👍10🔥7
​​Возврат ошибки. std::optional
#опытным

У std::variant довольно громоздкий интерфейс при возврате ошибки вместе с результатом работы функции. Но в С++17 появился еще один класс, который имеет семантику "Или" для типов + более дружелюбный интерфейс.

Это std::optional. Этот шаблонный класс либо содержит нужный тип, либо не содержит его. Вот так может выглядеть код:

struct Error {
std::string message;
};

std::optional<double> safe_divide(double a, double b) {
if (b == 0.0) { // здесь нужна нормальная проверка на равенство с epsilon
return std::nullopt;
}
return a / b;
}

auto div_result = safe_divide(10.0, 2.0);

if (div_result.has_value()) {
std::cout << "Result: " << div_result.value() << std::endl;
} else {
std::cout << "Error: there is no value" << std::endl;
}
// или с операторами
if (div_result) { // operator bool
std::cout << "Result: " << *div_result << std::endl; // operator*
} else {
std::cout << "Error: there is no value" << std::endl;
}


Для того, чтобы вернуть пустой optional, используется константа std::nullopt. А в остальном интерфейс очень похож на std::expected за исключением доступа к ошибке.

Но на мой взгляд, std::optional не очень подходит для обработки ошибок.

👉🏿 Он имеет семантику наличия или отсутствия значения. Отсутствие значения - это в принципе нормальная ситуация в программировании. Вы сделали Select к базе и получили пустоту, запросили что-то по апи и получили пустоту - вот самое место для std::optional.

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

👉🏿 Если вам нужно специфицировать, какая конкретно ошибка произошла, то std::optional умывает руки. Нужно либо output параметры использовать, либо в принципе другой класс.

Если есть 23-й стандарт или доступ к бусту, то лучше использовать std::expected или boost::outcome.

Use the right tool. Stay cool.

#cpp17
15🔥8👍6😁2👎1
Обработка ошибок Шердингера
#опытным

Мы уже поговорили о том, что есть 2 подхода к обработке ошибок - исключения и возврат кода ошибки(std::expected или output параметры).

И хоть стандартная библиотека насквозь пропитана исключениями, она все-таки иногда, очень редко предоставляет альтернативные варианты. Например std::from_chars или std::to_chars.

Интересно, что в библиотеке std::filesystem очень многие функции и методы имеют две перегрузки работы с данными: одна с исключениями, другая - без. Например:

bool exists( const std::filesystem::path& p );
bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;

// or

bool remove( const std::filesystem::path& p );
bool remove( const std::filesystem::path& p, std::error_code& ec ) noexcept;


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

Однако выше приведены "образцово показательные" перегрузки. Посмотрите вот на это:

directory_iterator& operator++();
directory_iterator& increment( std::error_code& ec );


Есть класс std::filesystem::directory_iterator и эти итераторы нужно уметь инкрементировать, чтобы двигаться по элементам директории. Так как сигнатура операторов в С++ не поддерживает лишние параметры, то для варианта с кодом ошибок приходится определять именованный метод.

Обратите внимание, что increment не объявлен как noexcept!

То есть используя increment, вы не можете гарантировать отсутствие исключений. Да, ошибки при работе с файловой системой ОС передаются в качестве кодов ошибок. Но тот же std::bad_alloc increment кинуть может.

По всей видимости, мотивация не выбрасывать исключения связана с тем, что вызывающие стороны, использующие версию с исключениями, часто замусорены локальными блоками try/catch для обработки «рутинных» событий. Условно: при работе с файлами может оказаться, что у программы нет прав доступа для них. Это в целом нормальная ситуация в файловой системе, но в первой перегрузке эти ситуации репортятся через исключения, как исключительные ситуации.

Дизайн странный и путает людей. Поэтому будьте аккуратны с std::filesystem, если реально хотите убрать исключения с глаз долой.

Don't be confused. Stay cool.

#cpp17
👍1810🔥6😁2