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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​std::from_chars
#новичкам

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

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

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


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

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

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

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


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

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

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


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


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

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

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

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


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

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


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


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

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

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

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

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


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

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

Be efficient. Stay cool.

#cpp17 #cpp23
1🔥4218👍15
Возврат ошибки. std::expected
#опытным

В С++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
🔥2614👍11
​​Уплощаем многомерный массив
#опытным

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

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 у строки. Чтобы соединить список строк разделителем нужно просто написать:

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 неявно не приводится к числам. Нам нужно явно указывать тип:

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🔥169🥱1
​​Оборачиваем вспять байты
#новичкам

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

[[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
#опытным

На самом деле у проблемы в этом коде:

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
👍361410🔥5
​​Предотвращаем висячие ссылки
#опытным

Давайте снова взглянем на этот пример:

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
🔥2114👍9🤯3
​​split
#опытным

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

Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:

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👍129🤯3👎1
WAT. История скобок, изменивших все
#опытным

Спасибо, @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