std::forward_like
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
template< class T >
constexpr T&& forward(std::remove_reference_t<T>& t ) noexcept;
template< class T >
constexpr T&& forward(std::remove_reference_t<T>&& t ) noexcept;
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
return forward<decltype(obj)>(obj).member;
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
struct adapter {
std::deque<std::string> container;
auto&& operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>(self.container[i]);
} };Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
4🔥22👍7❤4😁2
Идеальная передача из лямбды
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
Ну а передача копии вообще никогда не была проблемой:
Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таковзакон стандарт.
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
auto callback = [message=get_message(), &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}Ну а передача копии вообще никогда не была проблемой:
auto callback = [message=get_message(), &scheduler]() {
// some preparetions
scheduler.submit(message);
}Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
auto callback = [message=get_message(), &scheduler](this auto &&self) {
return scheduler.submit(std::forward_like<decltype(self)>(message));
};Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таков
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
2🔥21👍8❤5😁1
Ответ
Поговорим о том, что не так в коде из предыдущего поста:
🔞 Вопрос был про плюсовый код, но он как будто бы здесь даже не проходил. Пользоваться С++ и использовать только сишный инструментарий - идея, мягко говоря, не очень.
🔞 В 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::expected
#опытным
В С++23 появился практически идеальный класс для работы с объектами ошибки - std::expected.
Это та самая обертка над вариантом с приятным интерфейсом, о котором говорилось в прошлом посте.
По сути у std::expected в базовом интерефейсе 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator-> для ленивых со сточенными пальцами;
Преимущества нового типа
✅ Хранит только два типа: значение и ошибка.
✅ Делает код интуитивно понятнее, поскольку для создания ошибки нужно использовать
✅ Предоставляет простой и лаконичный базовый интерфейс: 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator->, кому лень писать названия методов.
С std::expeсted удобно работать, если есть всего один тип результата и один тип ошибки. Работать с std::expected<std::variant<Type1, Type2>, Error> или std::expected<Type, std::variant<Error1, Error2>> не так удобно, как просто с вариантом из трех типов. Если нужно возвращать больше ошибок, то можно пользоваться разными вариантами кодов ошибки от enuma'а до std::error_code или даже просто строкой.
Must have при работе без исключений.
Use a right semantic. Stay cool.
#cpp23
#опытным
В С++23 появился практически идеальный класс для работы с объектами ошибки - std::expected.
Это та самая обертка над вариантом с приятным интерфейсом, о котором говорилось в прошлом посте.
struct Error {
std::string message;
};
std::expected<double, Error> safe_divide(double a, double b) {
if (b == 0.0) { // здесь нужна нормальная проверка на равенство с epsilon
return std::unexpected(Error{"Division by zero"});
}
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: " << div_result.error().message << std::endl;
}
// или с операторами
if (div_result) { // operator bool
std::cout << "Result: " << *div_result << std::endl; // operator*
} else {
std::cout << "Error: " << div_result.error().message << std::endl;
}По сути у std::expected в базовом интерефейсе 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator-> для ленивых со сточенными пальцами;
Преимущества нового типа
std::expected по сравнению с std::variant:✅ Хранит только два типа: значение и ошибка.
✅ Делает код интуитивно понятнее, поскольку для создания ошибки нужно использовать
std::unexpected. Это особенно удобно, когда тип ошибки std::string. В этом случае использование std::unexpected{ "Something bad happens" } позволяет явно обозначить в коде, что мы не просто строку возвращаем, а сообщение об ошибке.✅ Предоставляет простой и лаконичный базовый интерфейс: 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator->, кому лень писать названия методов.
С std::expeсted удобно работать, если есть всего один тип результата и один тип ошибки. Работать с std::expected<std::variant<Type1, Type2>, Error> или std::expected<Type, std::variant<Error1, Error2>> не так удобно, как просто с вариантом из трех типов. Если нужно возвращать больше ошибок, то можно пользоваться разными вариантами кодов ошибки от enuma'а до std::error_code или даже просто строкой.
Must have при работе без исключений.
Use a right semantic. Stay cool.
#cpp23
🔥26❤14👍11
Уплощаем многомерный массив
#опытным
Иногда у вас есть коллекция элементов, для каждого из которых вы выполняете операцию, возвращающую вектор значений:
Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?
С помощью С++20 отображения std::views::join:
Это все сработает и на экране появлятся заветные чиселки.
Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте
Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:
Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.
Don't stuck in a loop. Stay cool.
#cpp20 #cpp23
#опытным
Иногда у вас есть коллекция элементов, для каждого из которых вы выполняете операцию, возвращающую вектор значений:
std::vector<int> Process(const std::string& str);
std::vector<std::string> elems = ...;
auto result_view = elems | std::views::transform([](const std::string& str) {
return Process(str);
})
Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?
С помощью С++20 отображения std::views::join:
std::vector<int> Process(const std::string& str);
std::vector<std::string> elems = ...;
auto result = elems | std::views::transform([](const std::string &str) {
return Process(str);
}) |
std::views::join | std::ranges::to<std::vector>();
std::print("{}", result);
Это все сработает и на экране появлятся заветные чиселки.
Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте
Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:
auto result = elems | std::views::transform([](const auto & elem) {
return Process(elem);
}) |
std::views::join | std::views::as_rvalue |
std::ranges::to<std::vector>();Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.
Don't stuck in a loop. Stay cool.
#cpp20 #cpp23
❤22👍13🔥7
join
#опытным
Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:
И как же сложно того же результата достичь в плюсах!
То делают через потоки:
то через std::accumulate:
Ну вы что! Стандартная строка же себе не может позволить иметь метод join, принимающий коллекцию строк и возвращающий объединенную строку с разделителями. Это же не универсально и никому не надо...
Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:
Можете обмазать все это шаблонами с головы до пят, чтобы получить универсальное решение, либо использовать этот код прям inplace, он и так довольно понятный.
И жизнь стала чуть-чуть счастливее...
Make thing simple. Stay cool.
#cpp23
#опытным
Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:
my_list = ["John", "Peter", "Vicky"]
x = " ".join(my_list)
print(x)
# OUTPUT
# John Peter Vicky
И как же сложно того же результата достичь в плюсах!
То делают через потоки:
std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";
std::ostringstream oss;
oss << vec[0];
for (size_t i = 1; i < vec.size(); ++i) {
oss << delimiter << vec[i];
}
return oss.str();
}то через std::accumulate:
std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";
return std::accumulate(
std::next(vec.begin()), vec.end(),
vec[0],
[&delimiter](const std::string& a, const std::string& b) {
return a + delimiter + b;
}
);
}Ну вы что! Стандартная строка же себе не может позволить иметь метод join, принимающий коллекцию строк и возвращающий объединенную строку с разделителями. Это же не универсально и никому не надо...
Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:
std::string join(const std::vector<std::string> &vec,
const std::string &delimiter) {
return vec | std::views::join_with(delimiter) |
std::ranges::to<std::string>();
}
Можете обмазать все это шаблонами с головы до пят, чтобы получить универсальное решение, либо использовать этот код прям inplace, он и так довольно понятный.
И жизнь стала чуть-чуть счастливее...
Make thing simple. Stay cool.
#cpp23
❤27👍12🔥9😁5
Удобно превращаем enum в число
#опытным
В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.
Но вот представьте себе, что вам где-то нужно получить числовое представление одного из перечислителя. К какому типу кастовать?
Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:
Если вам просто нужно вывести число в поток, то кастуйте к инту, ничего страшного не будет. Однако математические операции над полученным числом могут доставить неприятности, если тип будет не тот и будут использоваться сужающие-расширяющие преобразования.
Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.
С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:
Соответственно, для каста нужно сделать такую штуку:
Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:
Красота!
Know your type. Stay cool.
#cpp11 #cpp23
#опытным
В прошлом посте мы выяснили, что с С++11 можно самостоятельно указывать нижележащий тип, который и хранит все элементы enum'а.
Но вот представьте себе, что вам где-то нужно получить числовое представление одного из перечислителя. К какому типу кастовать?
Это важно, потому что scoped enum неявно не приводится к числам. Нам нужно явно указывать тип:
enum class ColorMask : std::uint32_t
{
red = 0xFF,
green = (red << 8),
blue = (green << 8),
alpha = (blue << 8)
};
// std::cout << ColorMask::red << std::endl; // ERROR
std::cout << static_cast<int>(ColorMask::red) << std::endl;
Если вам просто нужно вывести число в поток, то кастуйте к инту, ничего страшного не будет. Однако математические операции над полученным числом могут доставить неприятности, если тип будет не тот и будут использоваться сужающие-расширяющие преобразования.
Современные IDE-шки возможно будут вам показывать нужный тип, а возможно и нет. Если тип enum'а явно указан, то можно взять его. Но если нет, то гадать не хочется. Хочется стандартного решения.
С++11 также вводит тип шаблонный тип std::underlying_type, который предоставляет зависимый тип type, содержащий подкапотный тип enum'a:
enum e1 {};
enum class e2 {};
enum class e3 : unsigned {};
enum class e4 : int {};
constexpr bool e1_t = std::is_same_v<std::underlying_type_t<e1>, int>;
constexpr bool e2_t = std::is_same_v<std::underlying_type_t<e2>, int>;
constexpr bool e3_t = std::is_same_v<std::underlying_type_t<e3>, int>;
constexpr bool e4_t = std::is_same_v<std::underlying_type_t<e4>, int>;
std::cout
<< "underlying type for 'e1' is " << (e1_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e2' is " << (e2_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e3' is " << (e3_t ? "int" : "non-int") << '\n'
<< "underlying type for 'e4' is " << (e4_t ? "int" : "non-int") << '\n';
// OUTPUT
// underlying type for 'e1' is non-int
// underlying type for 'e2' is int
// underlying type for 'e3' is non-int
// underlying type for 'e4' is intСоответственно, для каста нужно сделать такую штуку:
auto num = static_cast<std::underlying_type_t<ColorMask>>(ColorMask::red);
Плохо, что это очень громоздкая конструкция, где к тому же типы повторяются. Поэтому в С++23 ввели хэлпер-сахарок std::to_underlying, который за нас все это делает:
auto num = std::to_underlying(ColorMask::red);
Красота!
Know your type. Stay cool.
#cpp11 #cpp23
👍21🔥16❤9🥱1
Оборачиваем вспять байты
#новичкам
Когда мы низкоуровнево работаем с сетью, то надо понимать, что в данных, полученных по сети, нужно реверсировать порядок байтов, чтобы правильно интерпретировать значения. Также реверсировать порядок нужно при отправке данных по сети. Это происходит из-за того, что в стеке протоколов TCP/IP принят порядок Big-endian - старший байт хранится по младшему адресу. А на большинстве хостов(десктопов и серверов) - Little-endian: младший байт хранится по младшему адресу.
Соответственно нужны функции для реверсирования байтов. Обычно для этого используют либо компиляторные интринсики:
Либо системное апи:
Либо какое-нибудь библиотечное решение:
Но в С++23 появилась стандартная функция для разворачивания порядка байтов!
Работает она только для интегральных типов и вот ее возможная реализация:
Результат у нее собственно ровно тот, который и ожидается:
Как всегда стандарт запаздывает лет на 10-15-20, но хорошо, что все-таки завезли эту полезную функцию, которую можно кроссплатформенно использовать.
Use standard solutions. Stay cool.
#cpp23
#новичкам
Когда мы низкоуровнево работаем с сетью, то надо понимать, что в данных, полученных по сети, нужно реверсировать порядок байтов, чтобы правильно интерпретировать значения. Также реверсировать порядок нужно при отправке данных по сети. Это происходит из-за того, что в стеке протоколов TCP/IP принят порядок Big-endian - старший байт хранится по младшему адресу. А на большинстве хостов(десктопов и серверов) - Little-endian: младший байт хранится по младшему адресу.
Соответственно нужны функции для реверсирования байтов. Обычно для этого используют либо компиляторные интринсики:
### GCC/Clang
uint16_t swapped16 = __builtin_bswap16(value);
uint32_t swapped32 = __builtin_bswap32(value);
uint64_t swapped64 = __builtin_bswap64(value);
### MSVC:
uint16_t swapped16 = _byteswap_ushort(value);
uint32_t swapped32 = _byteswap_ulong(value);
uint64_t swapped64 = _byteswap_uint64(value);
Либо системное апи:
#include <arpa/inet.h> // Linux/macOS
// или
#include <winsock2.h> // Windows
uint16_t network_to_host16 = ntohs(value);
uint16_t host_to_network16 = htons(value);
uint32_t network_to_host32 = ntohl(value);
uint32_t host_to_network32 = htonl(value);
uint64_t network_to_host64 = ntohll(value);
uint64_t host_to_network64 = htonll(value);
Либо какое-нибудь библиотечное решение:
#include <boost/endian/conversion.hpp>
uint32_t value = 0x12345678;
uint32_t swapped = boost::endian::endian_reverse(value);
uint32_t to_big = boost::endian::native_to_big(value);
uint32_t to_little = boost::endian::native_to_little(value);
Но в С++23 появилась стандартная функция для разворачивания порядка байтов!
template< class T >
constexpr T byteswap( T n ) noexcept;
Работает она только для интегральных типов и вот ее возможная реализация:
template<std::integral T>
constexpr T byteswap(T value) noexcept
{
static_assert(std::has_unique_object_representations_v<T>,
"T may not have padding bits");
auto value_representation = std::bit_cast<std::array<std::byte, sizeof(T)>>(value);
std::ranges::reverse(value_representation);
return std::bit_cast<T>(value_representation);
}
Результат у нее собственно ровно тот, который и ожидается:
template<std::integral T>
void dump(T v, char term = '\n')
{
std::cout << std::hex << std::uppercase << std::setfill('0')
<< std::setw(sizeof(T) * 2) << v << " : ";
for (std::size_t i{}; i != sizeof(T); ++i, v >>= 8)
std::cout << std::setw(2) << static_cast<unsigned>(T(0xFF) & v) << ' ';
std::cout << std::dec << term;
}
int main()
{
static_assert(std::byteswap('a') == 'a');
std::cout << "byteswap for U16:\n";
constexpr auto x = std::uint16_t(0xCAFE);
dump(x);
dump(std::byteswap(x));
std::cout << "\nbyteswap for U32:\n";
constexpr auto y = std::uint32_t(0xDEADBEEFu);
dump(y);
dump(std::byteswap(y));
std::cout << "\nbyteswap for U64:\n";
constexpr auto z = std::uint64_t{0x0123456789ABCDEFull};
dump(z);
dump(std::byteswap(z));
}
// OUTPUT
// byteswap for U16:
// CAFE : FE CA
// FECA : CA FE
// byteswap for U32:
// DEADBEEF : EF BE AD DE
// EFBEADDE : DE AD BE EF
// byteswap for U64:
// 0123456789ABCDEF : EF CD AB 89 67 45 23 01
// EFCDAB8967452301 : 01 23 45 67 89 AB CD EF
Как всегда стандарт запаздывает лет на 10-15-20, но хорошо, что все-таки завезли эту полезную функцию, которую можно кроссплатформенно использовать.
Use standard solutions. Stay cool.
#cpp23
❤28👍13😁8🔥5
Атрибуты лямбды
#опытным
В прошлом посте код с картинки реально компилируется и, если вы не поняли, что это за чертовщина, то следующие несколько постов будут для вас.
В С++11 у нас появилась возможность указывать атрибуты для функции. Например:
Вы можете, например, пометить возвращаемое значение функции, как то, которое нельзя игнорировать, и компилятор даст вам по сопатке, если вы его все же заигнорите.
Ну это функции. А как же лямбды? Хочется и для них указывать атрибуты.
И атрибуты для возвращаемого значения лямбды завезли в С++23. Выглядит это так:
После скобок для захвата вы указываете список атрибутов в квадратных скобках. Выглядит интересно. Не очень элегантно, но интересно.
Одни скажут: "усложнение синтаксиса!". Другие скажут, что давно пора лямбды подтягивать ко всем возможностям обычных функций.
Тут как бы все просто: не хотите - не используйте. У лямбды и так полно опциональных обвесок, одним больше, одним меньше. Можно определить шаблонную лямбду и обвесить ее всякими концептами с trailing return type. И это будет страшный зверь. Можно сделать отдельный пост, как может выглядеть ультимативная лямбда.
Ну а если вы хотите немного больше синтаксически говорить кодом, то теперь можете использовать атрибуты для лямбд.
Don't ignore. Stay cool.
#cpp23
#опытным
В прошлом посте код с картинки реально компилируется и, если вы не поняли, что это за чертовщина, то следующие несколько постов будут для вас.
В С++11 у нас появилась возможность указывать атрибуты для функции. Например:
[[nodiscard]] int ComplicatedCompute() {
return 2*2;
}
ComplicatedCompute();
// warning: ignoring return value of 'int ComplicatedCompute()',
// declared with attribute nodiscardВы можете, например, пометить возвращаемое значение функции, как то, которое нельзя игнорировать, и компилятор даст вам по сопатке, если вы его все же заигнорите.
Ну это функции. А как же лямбды? Хочется и для них указывать атрибуты.
И атрибуты для возвращаемого значения лямбды завезли в С++23. Выглядит это так:
auto complicated_compute = [] [[nodiscard]] () { return 2 * 2; };
complicated_compute();
// warning: ignoring return value of 'main()::<lambda()>',
// declared with attribute 'nodiscard'После скобок для захвата вы указываете список атрибутов в квадратных скобках. Выглядит интересно. Не очень элегантно, но интересно.
Одни скажут: "усложнение синтаксиса!". Другие скажут, что давно пора лямбды подтягивать ко всем возможностям обычных функций.
Тут как бы все просто: не хотите - не используйте. У лямбды и так полно опциональных обвесок, одним больше, одним меньше. Можно определить шаблонную лямбду и обвесить ее всякими концептами с trailing return type. И это будет страшный зверь. Можно сделать отдельный пост, как может выглядеть ультимативная лямбда.
Ну а если вы хотите немного больше синтаксически говорить кодом, то теперь можете использовать атрибуты для лямбд.
Don't ignore. Stay cool.
#cpp23
❤22👍10🔥8😁3
Продлеваем жизнь временного объекта range based for
#опытным
На самом деле у проблемы в этом коде:
есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
#опытным
На самом деле у проблемы в этом коде:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t1) { return t1; } // всегда возвращает висячую ссылку
T g();
void foo()
{
for (auto e : f1(g())) {} // OK: время жизни возвращаемого значения g() продлено
for (auto e : f2(g())) {} // UB: локальный объект t1 функции f2 все равно разрушается при выходе из скоупа f2
}
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
👍36☃14❤10🔥5
Предотвращаем висячие ссылки
#опытным
Давайте снова взглянем на этот пример:
Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
#опытным
Давайте снова взглянем на этот пример:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
auto& vec = generateData().items();
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
🔥21❤14👍9🤯3
split
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
А как сделатьпростейшую вещь разделить строку на С++?
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']
А как сделать
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
auto range = text | std::views::split(' ');
for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three fourЕсли вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
std::string text = "one two three four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
🔥38👍12❤9🤯3👎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