История 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