Что будет если бросить исключение в деструкторе? Ходим по тонкому льду.
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "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👍31🔥13❤8
Что будет если бросить исключение в деструкторе? Уверенно ходим по тонкому льду.
#опытным
std::uncaught_exception() в С++17 заменилась на std::uncaught_exceptions(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.
Есть уже знакомый нам класс логгера. В деструкторе калькулятора логируем какую-то информацию(не забываем про try-catch). В функции Process создаем калькулятор и вызываем его функцию Calc, которая вдруг кинула исключение. Давайте проследим цепочку событий и поймем в чем здесь загвоздка.
Из Calc вылетает исключение -> видит блок catch -> запускается раскрутка стека и деструктор калькулятора -> в деструкторе калькулятора создается логгер и записывает сообщение в файл -> при выходе из скоупа вызывается деструктор логгера -> std::uncaught_exception возвращает true и создание симлинка не происходит.
Однако в этом случае вы можете попробовать создать символическую ссылку! Дело в том, что деструктор Logger не будет вызываться непосредственно в результате размотки стека — он будет вызван после создания нового объекта из деструктора калькулятора. Таким образом, вы можете выбросить исключение из деструктора Logger'- вам нужно только поймать это исключение, прежде чем оно выйдет из деструктора калькулятора.
Если вы создали объект уже в процессе раскрутки стека, то вы можете кинуть исключение из деструктора. Надо только его поймать.
Чтобы исправить подобное поведение придумали std::uncaught_exceptions(), которая возвращает количество активных исключений на данный момент. И вот как выглядит логгер с использованием std::uncaught_exceptions():
Вот в чем фокус. Если мы создаем объект уже во время размотки стека и количество исключений на момент создания объекта равно количеству исключений при его разрушении, то мы можем вызывать потенциально бросающие функции.
Кейсы применения этой функции ужасно узкие, чуть более подробно можете почитать ее предложение в стандарт от Саттера. Поэтому вы должны очень хорошо понимать, чего вы такими неочевидными приемами хотите достичь. В современной разработке на С++ даже определение кастомного деструктора - вещь редкая, не то, что выкидывать отткуда исключения.
Помните, что такая возможность есть, но используйте ее в самом крайнем случае.
Walk on a thin ice. Stay cool.
#cpp17 #cppcore
#опытным
std::uncaught_exception() в С++17 заменилась на std::uncaught_exceptions(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.
constexpr std::string_view defaultSymlinkPath = "system/logs/log.txt";
struct Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName } {}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
if (!std::uncaught_exception()) {
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
struct Calculator
{
int64_t Calc(const std::vector<std::string> ¶ms);
// ....
~Calculator()
{
try {
Logger logger("log.txt");
Logger.Log("Calculator destroyed");
}
catch (...) {
// ....
}
}
};
int64_t Process(const std::vector<std::string> ¶ms) {
try {
Calculator calculator;
return Calculator.Calc(params);
}
catch (...) {
// ....
}
}
Есть уже знакомый нам класс логгера. В деструкторе калькулятора логируем какую-то информацию(не забываем про try-catch). В функции Process создаем калькулятор и вызываем его функцию Calc, которая вдруг кинула исключение. Давайте проследим цепочку событий и поймем в чем здесь загвоздка.
Из Calc вылетает исключение -> видит блок catch -> запускается раскрутка стека и деструктор калькулятора -> в деструкторе калькулятора создается логгер и записывает сообщение в файл -> при выходе из скоупа вызывается деструктор логгера -> std::uncaught_exception возвращает true и создание симлинка не происходит.
Однако в этом случае вы можете попробовать создать символическую ссылку! Дело в том, что деструктор Logger не будет вызываться непосредственно в результате размотки стека — он будет вызван после создания нового объекта из деструктора калькулятора. Таким образом, вы можете выбросить исключение из деструктора Logger'- вам нужно только поймать это исключение, прежде чем оно выйдет из деструктора калькулятора.
Если вы создали объект уже в процессе раскрутки стека, то вы можете кинуть исключение из деструктора. Надо только его поймать.
Чтобы исправить подобное поведение придумали std::uncaught_exceptions(), которая возвращает количество активных исключений на данный момент. И вот как выглядит логгер с использованием std::uncaught_exceptions():
struct Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
int m_exceptions = std::uncaught_exceptions(); // <=
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName } {}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
if (m_exceptions == std::uncaught_exceptions())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
Вот в чем фокус. Если мы создаем объект уже во время размотки стека и количество исключений на момент создания объекта равно количеству исключений при его разрушении, то мы можем вызывать потенциально бросающие функции.
Кейсы применения этой функции ужасно узкие, чуть более подробно можете почитать ее предложение в стандарт от Саттера. Поэтому вы должны очень хорошо понимать, чего вы такими неочевидными приемами хотите достичь. В современной разработке на С++ даже определение кастомного деструктора - вещь редкая, не то, что выкидывать отткуда исключения.
Помните, что такая возможность есть, но используйте ее в самом крайнем случае.
Walk on a thin ice. Stay cool.
#cpp17 #cppcore
👍29🔥11❤7
Ответ
Поговорим о том, что не так в коде из предыдущего поста:
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
Поговорим о том, что не так в коде из предыдущего поста:
#include <cstdio>
void bar(char * s) {
printf("%s", s);
}
void foo() {
char s[] =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В bar() принимает указатель на неконстантные данные и никак их не изменяет. Стандартные правила хорошего тона - это помечать константностью параметры функции, в которой данные остаются нетронутыми.
🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.
🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.
Как мог бы выглядеть бы код на современных плюсах?
#include <print>
#include <string_view>
void bar(std::string_view s) {
std::println("{}", s);
}
void foo() {
constexpr std::string_view s =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}
int main() {
foo();
}
Всего 2 простых улучшения:
✅ Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.
✅ Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.
Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.
Believe in magic. Stay cool.
#cppcore #cpp23 #cpp17
❤27👍17🔥7👎4🤷♂1
std::from_chars
#новичкам
С++17 нам принес новую прекрасную функцию парсинга строк в числа - std::from_char.
На самом деле это два семейства перегрузок функций для целых чисел и чисел с плавающей точкой.
Задача функции - максимально быстро, безо всяких накладных расходов на выделение памяти и поддержку исключений, распарсить строку в число арифметического типа.
Функция возвращает структуру
Если парсинг удался и какая-то часть строки конвертировалась в число, то в ptr находится указатель на первый символ, на котором парсинг завершился. Если вся строка была интерпретирована, как число, то в ptr находится last указатель.
Если парсинг неудался, то ptr равен first, а код ошибки ec выставляется в std::errc::invalid_argument.
Примеры работы:
К тому же функция может детектировать переполнение:
В чем главный прикол этой функции?
Помимо отсутствия накладных расходов, это последовательный парсинг. Если у вас есть строка с последовательностью чисел, разделенных запятой, то вы просто в цикле можете передвигать нужные указатели и парсить числа одно за другим. Тот же std::stoi выкинул бы исключение и пошел пиво пить:
К тому же ее целочисленный вариант с С++23 constexpr, что позволить вам парсить строку в числа даже во время компиляции.
Если вы не любите исключения - std::from_char ваш выбор.
Be efficient. Stay cool.
#cpp17 #cpp23
#новичкам
С++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🔥42❤18👍15
std::to_chars
#новичкам
В C++17 появилась не только функция для парсинга (std::from_chars), но и её обратная версия — std::to_chars, которая позволяет конвертировать числа (int, float, double и др.) в строки без дополнительных затрат на выделение памяти и поддержку исключений.
Единственный случай, когда эта функция может зафейлиться - если вы передали слишком маленький буффер. Тогда ec выставляется в std::errc::value_too_large, а ptr в last.
В чем преимущество функции по сравнению со старой-доброй std::to_string?
💥 Можно задать точность и основание системы счисления.
💥 Отсутствуют динамические аллокации внутри функции.
💥 Возможность использовать любые char буферы, а не только строки.
Вот вам пример работы:
Не всегда проблема точности и аллокаций - это реальная проблема. Но если вы работаете с ограниченной кучей и хотите стандартное средство сериализации числа в буфер - std::to_char как раз для вас.
Be efficient. Stay cool.
#cpp17
#новичкам
В 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
#опытным
В 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🔥22❤13👍10😱1
constexpr функции сквозь года
#новичкам
constexpr бывают не только переменные, но и функции. Такие функции могут быть выполнены, как в compile-time, так и в runtime, в зависимости от контекста вызова:
- Если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции.
- Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime.
- Если вы попытаетесь присвоить возвращаемое значение функции с runtime аргументом constexpr переменной, то получите ошибку компиляции.
Пройдемся по стандартам языка и посмотрим, как в них изменялись constexpr функции.
В С++11 можно было использовать только однострочные constexpr функции с сильными ограничениями.
В С++14 уже можно писать многострочные функции, использовать в них локальные переменные и базовые конструкции языка, кроме try-catch(там динамические аллокации, с которыми трудно в compile-time), goto(открестились и правильно сделали) и еще пары менее значимых моментов:
C++17 - constexpr лямбды. За это отдельный лайк 17-му стандарту, но помимо этого ничего существенного не привнеслось:
C++20 и выше - constexpr почти везде. Уже есть и виртуальные constexpr функции, и исключения можно в compile-time отлавливать, и большинство простых контейнеров могут быть созданы и препарированы в compile-time, и все больше алгоритмов и стандартных функции получают пометки constexpr.
constexpr функции помогают иметь единый интерфейс для runtime и compile-time вычислений. Поэтому использование таких функций может приводить к неожиданному переносу некоторых вычислений в compile-time.
В сферах, где важны ультра-мега-милипизерные задержки очень важно как можно больше действий перевести в compile-time + сильно разгрузить "разогрев" программы , чтобы оставить время выполнения только для самых важных вещей(лутание бабок).
Free up time for important things. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#новичкам
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👍12❤6👎2
История capture this
#опытным
Проследим историю захвата this в лямбду сквозь стандарты С++, там есть на что посмотреть.
С++11
Появились лямбды и в них можно захватывать this как явно, так и неявно через параметры захвата по-умолчанию. Во всех случаях захватывается
Однако не было адекватного способа захватить объект по значению aka скопировать его в лямбду.
С++14
Появилась инициализация в захвате, поэтому стал реальным захват объекта по значению:
В остальном все осталось также.
С++17
Появился захват объекта по значению! Захват по значению может быть очень важно для асинхронных операций, которые откладывают выполнение лямбд:
В этом коде UB, потому что возвращаем лямбду со ссылкой на несуществующий объект. Поменять это можно захватом объекта по значению:
В этом случае в лямбде будет храниться копия объекта и все методы будут обращаться к этому скопированному объекту.
С++20
С появлением захвата this по значению стала очень путающей семантика неявного захвата. Что reference capture &, что value capture =, по факту захватывали текущий объект по ссылке. И ничто неявно не захватывало this по значению.
Изначально в 20-м стандарте эту проблему хотели решить, просто запретив неявный захват this в любом случае. Но посидели и поняли, что для ссылочного захвата по умолчанию семантика неявного захвата ссылки на объект(чем отдаленно явняется this) корректна. А вот для захвата по значению - нет.
Поэтому начиная с С++20 мы не можем неявно захватывать this в default capture by value:
Оставлю в картинке под постом инфографику по изменениям в стандартах относительно захвата this.
Know the history. Stay cool.
#cpp11 #cpp14 #cpp17 #cpp20
#опытным
Проследим историю захвата 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
🔥16👍11❤10🤯6
Динамический полиморфизм: std::variant + std::visit
#опытным
Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же std::function - сочетание виртуальных функций и шаблонов. Но о подробностях реализации в другом посте.
Другой пример - std::variant + std::visit. std::variant - шаблонный класс, который может хранить в себе объект любого типа, который есть среди его шаблонных параметров. Эдакий типобезопасный union, без UB и прочей грязи.
Вариант позволяет складывать фиксированный набор типов в один контейнер:
Но после помещения задач в контейнер мы уже точно не можем сказать, какой конкретно тип содержит каждый элемент. Как ими тогда оперировать?
Через std::visit, конечно. Эта функция, которая принимает функциональный объект, который можно вызвать для любого типа, потенциально хранящегося в варианте, к самому объекту варианта. Объект std::variant на самом деле знает, какой тип в нем хранится, просто нам он об этом не рассказывает. А std::visit'у рассказывает:
По-настоящему мощным это сочетание ставится при применении паттерна overload:
Опять же, на этапе компиляции воркер понятия не имеет, какой тип реально хранится в варианте. Решение, какой обработчик вызвать, принимается в рантайме. Поэтому пара variant+visit реализует динамический полиморфизм, хоть и не без шаблонной магии.
Visit your closest. Stay cool.
#cpp17 #template
#опытным
Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же 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🔥19❤13❤🔥2🤯2
Одно значимое улучшение С++17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
В каком порядке вызываются expr1, expr2, expr3, g, h и f?
Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.
Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.
Что было до С++17?
Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!
То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.
Полный бардак! И это приводило на самом деле к неприятным последствиям.
Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.
Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен.
Теперь такой код не будет проблемой:
потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp17
#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
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
👍37❤15🔥10❤🔥4
Идиома IILE
#опытным
Неплохой практикой написания кода является определение переменных, которые не изменяются, как const. Это позволяет коду был более экспрессивным, явным, а также компилятор в этом случае может чуть лучше рассуждать о коде и оптимизировать. И это не требует ничего сложного:
Но что делать, если переменная по сути своей константа, но у нее громоздская инициализация на несколько строк?
По-хорошему это уносится в какую-нибудь отдельную функцию. Но тогда теряется контекст и нужно будет прыгать по коду.
Хочется и const сделать, и в отдельную функцию не выносить. Кажется, что на двух стульях не усидишь, но благодаря лямбдам мы можем это сделать!
Есть такая идиома IILE(Immediately Invoked Lambda Expression). Вы определяете лямбду и тут же ее вызываете. И контекст сохраняется, и единовременность инициализации присутствует:
Пара лишних символов, зато проблема решена.
Тут есть одно "но". Вроде все хорошо, но немного напрягает, что все это можно спутать с простым определением лямбды, если не увидеть скобки вызова в конце.
Тоже не беда! Используем std::invoke:
Теперь мы четко и ясно видим, что лямбда вызывается. В таком виде прям кайф.
Эту же технику можно использовать например в списке инициализации конструктора, например, если нужно константное поле определить(его нельзя определять в теле конструктора).
Be expressive. Stay cool.
#cpp11 #cpp17 #goodpractice
#опытным
Неплохой практикой написания кода является определение переменных, которые не изменяются, как 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
🔥51👍22❤15👎4🤷♂3❤🔥2
Возврат ошибки. std::variant
#новичкам
Если у вас есть С++17, то поздравляю, у вас есть std::variant, который решает проблему суперпозиции полей из прошлого поста.
По сути, std::variant - это типобезопасный юнион, который хранит только один тип из списка шаблонных параметров. Объект варианта можно проверить на наличие нужного типа и есть способы no-exceptions сообщения об ошибке, если вы хотите получить доступ не к тому типу. Обычный std::get кидает исключение при неправильном доступе, но std::holds_alternative или std::get_if предоставляют небросающий апи:
Библиотечный код стал ощутимо короче и не перестал быть таким же читаемым. Но вот клиентский код стал очевидно менее читаемым, по сравнению с предыдущим постом.
К этому привело то, что семантика "результат или ошибка" не заложена в самой идее std::variant. Для него все типы изначально равнозначны и равноожидаемы. Нет плохого и хорошего типа.
Чисто технически, вы можете сделать простую обертку из варианта, чтобы такая семантика появилась, но оставим это в качестве домашнего задания читателям.
Вариант не требует динамических аллокаций и занимает столько памяти, сколько нужно для самого большого типа(помимо небольшой метаинформации). Так что по части эффективности здесь все хорошо.
Еще можно легко возвращать фиксированный набор из нескольких типов ошибки или значений, просто добавив нужный тип в список шаблонных параметров.
В небросающем коде нужно обязательно проверять каждый доступ к объекту варианта, потому что std::get кидает исключение(вообще говоря, в любом коде). Ну или сразу используйте std::get_if, если точно знаете, какой должен быть тип, но нужно подстраховаться от ошибок.
Это хороший вариант, но больно пользоваться. Хочется более элегантного решения решения для этой проблемы. И оно есть! О этом в следующем посте.
Use a right semantic. Stay cool.
#cpp17
#новичкам
Если у вас есть С++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
❤21👍11🔥7
Возврат ошибки. std::optional
#опытным
У std::variant довольно громоздкий интерфейс при возврате ошибки вместе с результатом работы функции. Но в С++17 появился еще один класс, который имеет семантику "Или" для типов + более дружелюбный интерфейс.
Это std::optional. Этот шаблонный класс либо содержит нужный тип, либо не содержит его. Вот так может выглядеть код:
Для того, чтобы вернуть пустой 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
#опытным
У 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
❤19🔥8👍6👎2😁2
Обработка ошибок Шердингера
#опытным
Мы уже поговорили о том, что есть 2 подхода к обработке ошибок - исключения и возврат кода ошибки(std::expected или output параметры).
И хоть стандартная библиотека насквозь пропитана исключениями, она все-таки иногда, очень редко предоставляет альтернативные варианты. Например std::from_chars или std::to_chars.
Интересно, что в библиотеке std::filesystem очень многие функции и методы имеют две перегрузки работы с данными: одна с исключениями, другая - без. Например:
std::filesystem завезли в стандарт относительно поздно, поэтому было время задуматься о людях, пишущих небросающий код.
Однако выше приведены "образцово показательные" перегрузки. Посмотрите вот на это:
Есть класс std::filesystem::directory_iterator и эти итераторы нужно уметь инкрементировать, чтобы двигаться по элементам директории. Так как сигнатура операторов в С++ не поддерживает лишние параметры, то для варианта с кодом ошибок приходится определять именованный метод.
Обратите внимание, что increment не объявлен как noexcept!
То есть используя increment, вы не можете гарантировать отсутствие исключений. Да, ошибки при работе с файловой системой ОС передаются в качестве кодов ошибок. Но тот же std::bad_alloc increment кинуть может.
По всей видимости, мотивация не выбрасывать исключения связана с тем, что вызывающие стороны, использующие версию с исключениями, часто замусорены локальными блоками
Дизайн странный и путает людей. Поэтому будьте аккуратны с std::filesystem, если реально хотите убрать исключения с глаз долой.
Don't be confused. Stay cool.
#cpp17
#опытным
Мы уже поговорили о том, что есть 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
👍20❤11🔥6😁2
Множество атрибутов
#опытным
Если вы хотите указать несколько атрибутов для вашей функции, вы можете использовать следующий синтаксис:
1️⃣ Списочный. Внутри одних скобок перечисляете все атрибуты:
2️⃣ Многоскобочный. Для больших любителей распиленных квадратов. Очень больших:
Больше квадратных скобок!
Также если вы используете несколько атрибутов из какого-то одного неймспейса, то можете использовать директиву using:
Но тогда котлеты отдельно, мухи отдельно. Все атрибуты одного неймспейса нужно уносить в отдельные скобки. Это фича С++17.
Что интересно, вы можете написать полную чупуху:
И это скомпилируется! Стандарт поддерживает любые implementation-defined атрибуты. Причем неизвестные атрибуты просто игнорируются. Правда игнор спровождается варнингами, которые тем не менее можно скрыть опциями, подобным -Wno-attributes.
Таким образом, если ваш код компилируется под разные системы, то вы можете не стесняясь использовать дублирующие атрибуты, предоставляемые разными компиляторами. Так на любой платформе можно получить одинаковое поведение.
Love squares. Stay cool.
#cppcore #cpp17
#опытным
Если вы хотите указать несколько атрибутов для вашей функции, вы можете использовать следующий синтаксис:
1️⃣ Списочный. Внутри одних скобок перечисляете все атрибуты:
[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]] int f();
2️⃣ Многоскобочный. Для больших любителей распиленных квадратов. Очень больших:
[[gnu::always_inline]] [[gnu::hot]] [[gnu::const]] [[nodiscard]] int f();
Больше квадратных скобок!
Также если вы используете несколько атрибутов из какого-то одного неймспейса, то можете использовать директиву using:
[[using gnu : always_inline, const, hot]] [[nodiscard]] int f();
Но тогда котлеты отдельно, мухи отдельно. Все атрибуты одного неймспейса нужно уносить в отдельные скобки. Это фича С++17.
Что интересно, вы можете написать полную чупуху:
[[rust, will, replace, cpp]] int f();
И это скомпилируется! Стандарт поддерживает любые implementation-defined атрибуты. Причем неизвестные атрибуты просто игнорируются. Правда игнор спровождается варнингами, которые тем не менее можно скрыть опциями, подобным -Wno-attributes.
Таким образом, если ваш код компилируется под разные системы, то вы можете не стесняясь использовать дублирующие атрибуты, предоставляемые разными компиляторами. Так на любой платформе можно получить одинаковое поведение.
Love squares. Stay cool.
#cppcore #cpp17
🔥22❤13👍10👎1
WAT
#опытным
Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.
Делаем вот так:
Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.
Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.
После получаем значение этой переменной и сравниваем ее с оригиналом.
Ну и в конце ищем среди массива строку эту runtime_str.
Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.
Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."
WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?
Дьявол кроется в деталях.
Вспоминаемшкольную математеку CTAD. Какой тип элементов массива выведется?
Правильно, const char *.
А как std::ranges::find сравнивает такие элементы?
Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.
Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.
Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.
А вот последний ассерт просто говорит о том, что указатель
И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.
В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.
Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.
Express your wishes precisely. Stay cool.
#cppcore #cpp17
#опытным
Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.
Делаем вот так:
constexpr std::array array = {"I", "love", "C++"};
int main() {
if (auto iter = std::ranges::find(array, "C++"); iter == std::end(array)) {
assert(false && "comptime arg");
}
// let's go with runtime now
if (setenv("RUNTIME", array[2], 0) != 0) {
assert(false && "setenv");
}
char *runtime_str = getenv("RUNTIME");
assert(strcmp(runtime_str, array[2]) == 0 && "equal strings");
if (auto magick_iter = std::ranges::find(array, runtime_str);
magick_iter == std::end(array)) {
assert(false && "runtime arg");
}
}Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.
Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.
После получаем значение этой переменной и сравниваем ее с оригиналом.
Ну и в конце ищем среди массива строку эту runtime_str.
Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.
Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."
WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?
Дьявол кроется в деталях.
Вспоминаем
Правильно, const char *.
А как std::ranges::find сравнивает такие элементы?
Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.
Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.
Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.
А вот последний ассерт просто говорит о том, что указатель
runtime_str не был найден в массиве, потому что там нет такого адреса.И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.
В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.
Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.
Express your wishes precisely. Stay cool.
#cppcore #cpp17
❤21👍12😁7🔥5👎1
Как получить длину строкового литерала?
#опытным
Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод size:
Ну или на худой конец вызовем strlen:
Но я считаю, это не по-современному.
С++ давно идет в сторону расширения возможностей вычислений в compile-time. Поэтому если что-то можно вычислить во время компиляции, то это нужно сделать именно там! Ни грамма лишнего времени вычислений не потратим.
Давайте посмотрим, как можно найти длину строкового литерала во время компиляции.
1️⃣ Кастомщина. Хочешь что-то сделать хорошо, сделай это сам. Не факт, что получится хорошо, но ты старался:
Реальный тип строкового литерала не const char *, а константный массив символов. Поэтому через шаблон мы можем подтянуть размер массива через NTTP-параметр шаблона и вернуть его наружу.
2️⃣ Используем sizeof. Этот оператор возвращает длину массива во время компиляции. Единственное, что он считает терминирующий символ, поэтому все равно вокруг него надо обертку писать, чтобы единичку нигде не потерять:
Эх, а так хотелось готового get-to-go решения. Погодите...
3️⃣ Обернуть не в строку, а в string_view и вызвать метод size(). Конструкторы вьюхи изначально с С++17 были constexpr, как и сам метод size(), поэтому просто берем и пишем:
Просто, работает из коробки и знакомо всем.
4️⃣ Да зачем что-то менять в коде, это для слабаков. Поменяем стандарт и все заработает в compile-time! Ну точнее конструктор std::string и метод size() в С++20 теперь тоже constexpr:
Пысы: я не просто вызываю какие-то функции в надежде, что они выполнятся в compile-time. Тот факт, что len - constexpr переменная, требует, чтобы компилятор вычислил выражение справа во время компиляции.
5️⃣ Тот пункт, который и вдохновил на написание этого поста. Все пункты выше либо надо было самим реализовывать, либо вот какие-то обертки, чтобы хакнуть систему и на самом деле не работать с литералами.
Но не так плюсы бедны на стандартные решения. Есть стандартная С++17 функция std::char_traits<char>::length. Она может работать в compile-time, имеет явную семантику вычисления длины и работает чисто с c-style строками:
Красиво? Ну а что вы от плюсов хотели?) Зато из коробки работает.
6️⃣ Пользовательские литералы. Еще один неординарный способ. С С++11 мы имеем возможность превращать численные и строковые литералы в пользовательские объекты с помощью дописывания суффикса. Прикольно же писать:
Коротко и понятно. Для этого нужно лишь определить оператор преобразования:
и теперь вы свободны от угнетения оберток.
Если есть еще идеи, кидайте в комменты, будет интересно.
Don't be oppressed. Stay cool.
#cpp11 #cpp17 #cpp20
#опытным
Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод size:
size_t length = std::string("Hello, subscribers!").size();Ну или на худой конец вызовем strlen:
size_t length = strlen("Hello, subscribers!");Но я считаю, это не по-современному.
С++ давно идет в сторону расширения возможностей вычислений в compile-time. Поэтому если что-то можно вычислить во время компиляции, то это нужно сделать именно там! Ни грамма лишнего времени вычислений не потратим.
Давайте посмотрим, как можно найти длину строкового литерала во время компиляции.
1️⃣ Кастомщина. Хочешь что-то сделать хорошо, сделай это сам. Не факт, что получится хорошо, но ты старался:
template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return N - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");
Реальный тип строкового литерала не const char *, а константный массив символов. Поэтому через шаблон мы можем подтянуть размер массива через NTTP-параметр шаблона и вернуть его наружу.
2️⃣ Используем sizeof. Этот оператор возвращает длину массива во время компиляции. Единственное, что он считает терминирующий символ, поэтому все равно вокруг него надо обертку писать, чтобы единичку нигде не потерять:
template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return sizeof(str) - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");
Эх, а так хотелось готового get-to-go решения. Погодите...
3️⃣ Обернуть не в строку, а в string_view и вызвать метод size(). Конструкторы вьюхи изначально с С++17 были constexpr, как и сам метод size(), поэтому просто берем и пишем:
constexpr size_t len = std::string_view("Hello, subscribers!").size();Просто, работает из коробки и знакомо всем.
4️⃣ Да зачем что-то менять в коде, это для слабаков. Поменяем стандарт и все заработает в compile-time! Ну точнее конструктор std::string и метод size() в С++20 теперь тоже constexpr:
constexpr size_t len = std::string("Hello, subscribers!").size();Пысы: я не просто вызываю какие-то функции в надежде, что они выполнятся в compile-time. Тот факт, что len - constexpr переменная, требует, чтобы компилятор вычислил выражение справа во время компиляции.
5️⃣ Тот пункт, который и вдохновил на написание этого поста. Все пункты выше либо надо было самим реализовывать, либо вот какие-то обертки, чтобы хакнуть систему и на самом деле не работать с литералами.
Но не так плюсы бедны на стандартные решения. Есть стандартная С++17 функция std::char_traits<char>::length. Она может работать в compile-time, имеет явную семантику вычисления длины и работает чисто с c-style строками:
constexpr size_t len = std::char_traits<char>::length("Hello, subscribers!");Красиво? Ну а что вы от плюсов хотели?) Зато из коробки работает.
6️⃣ Пользовательские литералы. Еще один неординарный способ. С С++11 мы имеем возможность превращать численные и строковые литералы в пользовательские объекты с помощью дописывания суффикса. Прикольно же писать:
constexpr auto length = "Hello, subscribers!"_len;
Коротко и понятно. Для этого нужно лишь определить оператор преобразования:
constexpr size_t operator"" _len(const char* str, size_t n) {
return n;
}и теперь вы свободны от угнетения оберток.
Если есть еще идеи, кидайте в комменты, будет интересно.
Don't be oppressed. Stay cool.
#cpp11 #cpp17 #cpp20
🔥44👍18❤8🤯1
Стандартные пользовательские литералы. Строковые
#новичкам
Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.
Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без underscore'а приятнее визуально.
Вторая особенность - нужно обязательно указывать
В остальном, это те же кастомные литералы, только для стандартных типов. Подразделяются они по базовому типу литерала, к которому приписывается суффикс.
Строковые кастомные литералы
Интересно, что для них операторы принимают 2 параметра: указатель и длину:
Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.
Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:
1️⃣ std::string:
2️⃣ std::string_view:
Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.
У них у обоих есть одна особенность. Так как размер строки передается в оператор и этот размер потом используется для создания объекта, то есть некоторые отличия при создании объектов через конструктор и через оператор:
Во втором случае получилась строка длиннее, чем в первом. Почему?
Для
Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки.
Для
Теперь конструктор знает реальную длину строки и аллоцирует столько памяти, сколько нужно, чтобы поместить весь литерал в строку.
Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.
Остальные стандартные литералы не уместились в ограничения телеги, поэтому будет вторая часть.
See the difference. Stay cool.
#cpp11 #cpp17
#новичкам
Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.
Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без underscore'а приятнее визуально.
Вторая особенность - нужно обязательно указывать
using namespace std::literals помимо включения нужных хэдэров. Кастомный оператор - это по сути обычная функция. И при вызове функции из какого-то пространства имен(а все стандартное лежит как минимум в неймспейсе std) мы должны перед именем функции указать это пространство. Но как вы это сделаете с оператором? Да никак. Поэтому явно нужно использовать в своем коде неймспейс. Он общий для всех стандартных операторов, но есть еще и подпространства под конкретные их группы.В остальном, это те же кастомные литералы, только для стандартных типов. Подразделяются они по базовому типу литерала, к которому приписывается суффикс.
Строковые кастомные литералы
Интересно, что для них операторы принимают 2 параметра: указатель и длину:
( const char*, std::size_t )
Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.
Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:
1️⃣ std::string:
constexpr std::string operator""s(const char* str, std::size_t len);
using namespace std::literals;
auto str = "Hello, World!"s;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string>);
2️⃣ std::string_view:
constexpr std::string_view
operator ""sv(const char* str, std::size_t len) noexcept;
using namespace std::literals;
auto str = "Hello, World!"sv;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string_view>);
Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.
У них у обоих есть одна особенность. Так как размер строки передается в оператор и этот размер потом используется для создания объекта, то есть некоторые отличия при создании объектов через конструктор и через оператор:
void print_with_zeros(const auto note, const std::string& s) {
std::cout << note;
for (const char c : s)
c ? std::cout << c : std::cout << "₀";
std::cout << " (size = " << s.size() << ")\n";
}
int main() {
using namespace std::string_literals;
std::string s1 = "abc\0\0def";
std::string s2 = "abc\0\0def"s;
print_with_zeros("s1: ", s1);
print_with_zeros("s2: ", s2);
}
// OUTPUT:
// s1: abc (size = 3)
// s2: abc₀₀def (size = 8)Во втором случае получилась строка длиннее, чем в первом. Почему?
Для
s1 вызывается конструктор от одного аргумента:basic_string( const CharT* s, const Allocator& alloc = Allocator() );
Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки.
Для
s2 вызывается конструктор от двух аргументов:basic_string( const CharT* s, size_type count,
const Allocator& alloc = Allocator() );
Теперь конструктор знает реальную длину строки и аллоцирует столько памяти, сколько нужно, чтобы поместить весь литерал в строку.
Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.
Остальные стандартные литералы не уместились в ограничения телеги, поэтому будет вторая часть.
See the difference. Stay cool.
#cpp11 #cpp17
❤30🔥12👍8😁7🤯2🤔1💯1
WAT. История скобок, изменивших все
#опытным
Спасибо, @d7d1cd, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Посмотрите еще раз на этот пример:
Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:
Но у вашего компилятора на это другое мнение. Кланг, например, выводит:
WAT? А где 2 и 4? И почему вообще 1 элемент?
С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.
Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:
Конструируя вектор с помощью универсальной инициализации, вы уже внутри самых внешних скобок должны перечислять элементы.
Вот и получается, что строка выше парсится компилятором, как одна пара.
Тогда получается, что вью на строку можно создать с помощью
Без проблем. Вот вам подходящий конструктор:
У нас же строковые литералы неявно приводятся к указателям. Очень уж похоже на то, что мы хотим создать вью на непрерывный поток байтов. Компилятор именно это и предполагает. Жаль, что только:
оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.
Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.
А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.
Пофиксить эту неприятную неожиданность можно либо убрав лишнюю пару скобок, либо явно сказав, где вы хотите видеть пары:
Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного
Avoid ambiguity. Stay cool.
#STL #cpp17 #cpp23
#опытным
Спасибо, @d7d1cd, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Посмотрите еще раз на этот пример:
int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};
for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}
Вроде все тривиально, проще только 2+2. Все более менее с первого взгляда ожидают такой вывод:
one and two
three and four
Но у вашего компилятора на это другое мнение. Кланг, например, выводит:
one and three
WAT? А где 2 и 4? И почему вообще 1 элемент?
С виду вектор должен инициализироваться от std::initializer_list, в котором будут лежать 2 пары.
Но, судя по выводу, пара вообще одна. И здесь подсказка. Собака зарыта в одной лишней паре фигурных скобок:
/->/{/<-/{"one", "two"}, {"three", "four"}/->/}/<-/Конструируя вектор с помощью универсальной инициализации, вы уже внутри самых внешних скобок должны перечислять элементы.
Вот и получается, что строка выше парсится компилятором, как одна пара.
Тогда получается, что вью на строку можно создать с помощью
{"one", "two"}?!? Без проблем. Вот вам подходящий конструктор:
template< class It, class End >
constexpr basic_string_view( It first, End last );
У нас же строковые литералы неявно приводятся к указателям. Очень уж похоже на то, что мы хотим создать вью на непрерывный поток байтов. Компилятор именно это и предполагает. Жаль, что только:
The behavior is undefined if [first, last) is not a valid range
оба указателя не относятся к одной и той же последовательности, поэтому получили ub в наказание.
Кланг видимо идет от first либо до last, либо до символа конца строки. Поэтому 2 вьюхи содержат полные первые строки.
А вот gcc похоже идет до конца, пока не встретит last. Поэтому в его выводе куча мусора.
Пофиксить эту неприятную неожиданность можно либо убрав лишнюю пару скобок, либо явно сказав, где вы хотите видеть пары:
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{std::pair{"one", "two"}, std::pair{"three", "four"}}
};
Ну и да. Можно просто использовать С++17 и никакого уб не будет! В c++17 у std::string_view нет конструктора от двух итераторов, поэтому список {{"one","two"}, {"three","four"}} не мог быть использован для инициализации одного
pair. Компилятор, следуя правилам инициализации из списка, развернул вложенный список и интерпретировал содержимое как два отдельных элемента для вектора. Можно убедиться тут. Спасибо @Shuomi за комментарий по поводу различного поведения при разных стандартах)Avoid ambiguity. Stay cool.
#STL #cpp17 #cpp23
❤26👍14🤯9🔥6