enum class
#новичкам
Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы
В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:
Он решает две большие проблемы обычных перечислений:
👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.
Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:
Что значит прибавить красному цвету единицу - решительно непонятно.
Неявные преобразования enum class'ов же запрещено:
Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:
👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:
И все прекрасно компилируется.
С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.
Protect your scope. Stay cool.
#cppcore #cpp11
#новичкам
Перечисления пришли в С++ еще из С и отлично живут. Однако плюсовикам не очень с ними комфортно работать с силу наследования слабой типизации и неявных преобразований enum'ов в числовые типы и в другие enum'ы
В С++11 появился новый тип перечислений - scoped enumerations. Или ограниченные областью видимости перечисления. Определяются они так:
enum class Enumeration {CATEGORY1, CATEGORY2, CATEGORY3};Он решает две большие проблемы обычных перечислений:
👉🏿 Обычные перечисления неявно преобразуются в int и обратно, что вызывает ошибки, когда не предполагается использование перечисления в качестве целого числа.
Можно например попробовать получить следующее значение перечисления, просто прибавив единицу:
cpp
enum Color { RED, GREEN, BLUE };
Color c = RED;
Color next = c + 1; // Implicit conversion to int and visa versa!
Что значит прибавить красному цвету единицу - решительно непонятно.
Неявные преобразования enum class'ов же запрещено:
enum class Color { RED, GREEN, BLUE };
Color c = Color::RED;
Color next = c + 1; // ERROR!Если вам сильно нужно преобразовать перечислитель к числу, то вы это должны сделать явно:
Color c = Color::RED;
Color next = static_cast<Color>(static_cast<int>(c) + 1);
👉🏿 Обычные перечисления экспортируют свои перечислители в окружающую область видимости, вызывая конфликты имён с другими сущностями в этой окружающей области:
enum Color { RED, GREEN, BLUE };
enum TrafficLight { RED, YELLOW, GREEN }; // ERROR!
void graphics_library() {
Color c = RED;
}У scoped enum'ов такой проблемы нет. Имена перечислителей находятся в скоупе своего перечисления:
enum class Color1 { RED, GREEN, BLUE };
enum class Color2 { RED, GREEN, BLUE };
void graphics_library() {
Color1 c1 = Color1::RED;
Color2 c2 = Color2::RED;
}И все прекрасно компилируется.
С учетом неймспейсов и любви к явным кастам в коммьюнити, в С++ лучше использовать enum class'ы вместо обычных перечислений.
Protect your scope. Stay cool.
#cppcore #cpp11
👍44❤17🔥16
enum struct
#опытным
В прошлом посте мы рассказали про enum class. И в 99.999% случаев эту сущность будут писать в коде именно, как enum class.
Но можно написать enum struct и это тоже будет работать!
Немного кто знает вообще о существовании такой конструкции. Свойства enum struct абсолютно аналогичны enum class и структуры перечислений были введены просто для консистентности и поддержания паритета в возможности использования двух ключевых слов.
Вот такой короткий и бесполезный факт из мира плюсов)
Be useful. Stay cool.
#fun #cpp11
#опытным
В прошлом посте мы рассказали про enum class. И в 99.999% случаев эту сущность будут писать в коде именно, как enum class.
Но можно написать enum struct и это тоже будет работать!
enum class FileMode { Read, Write, Append };
enum struct LogLevel { Debug, Info, Warning, Error };
int main() {
FileMode mode = FileMode::Read;
LogLevel level = LogLevel::Info;
std::cout << (mode == FileMode::Read) << std::endl;
std::cout << (level == LogLevel::Info) << std::endl;
}Немного кто знает вообще о существовании такой конструкции. Свойства enum struct абсолютно аналогичны enum class и структуры перечислений были введены просто для консистентности и поддержания паритета в возможности использования двух ключевых слов.
Вот такой короткий и бесполезный факт из мира плюсов)
Be useful. Stay cool.
#fun #cpp11
2👍38😁24❤10🔥7🤣5
Размер enum'а
#опытным
Перечисления - это по факту именованные числа. Каждому перечислителю ставится в соответствие число, к которому перечислитель может приводиться. Оно либо указывается явно, либо проставляется компилятором.
Но тогда встает вопрос: а сколько весит enum? Мы же про эффективность и хотим, чтобы данные занимали минимально возможное пространство.
Мы можем явно написать:
Мы как бы явно говорим, что ограничиваем размер enum'а 8-ью битами. Но будет ли его размер реально 8 бит?
Не факт. Компилятор может выбрать любой подходящий по размеру тип, главное, чтобы он мог вместить все элементы перечисления. Это может быть char, short или int. И все это разного размера.
Неприятно, что на это нельзя было влиять.
Но прочь неприятности, потому что в С++11, помимо enum class появилась возможность указания размера scoped и unscoped enum'ов:
И теперь вы может контролировать и сами задавать размер перечисления.
Control your size. Stay cool.
#cpp11
#опытным
Перечисления - это по факту именованные числа. Каждому перечислителю ставится в соответствие число, к которому перечислитель может приводиться. Оно либо указывается явно, либо проставляется компилятором.
Но тогда встает вопрос: а сколько весит enum? Мы же про эффективность и хотим, чтобы данные занимали минимально возможное пространство.
Мы можем явно написать:
enum MY_FAVOURITE_FRUITS
{
E_APPLE = 0x01,
E_WATERMELON = 0x02,
E_COCONUT = 0x04,
E_STRAWBERRY = 0x08,
E_CHERRY = 0x10,
E_PINEAPPLE = 0x20,
E_BANANA = 0x40,
E_MANGO = 0x80,
E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};
Мы как бы явно говорим, что ограничиваем размер enum'а 8-ью битами. Но будет ли его размер реально 8 бит?
Не факт. Компилятор может выбрать любой подходящий по размеру тип, главное, чтобы он мог вместить все элементы перечисления. Это может быть char, short или int. И все это разного размера.
Неприятно, что на это нельзя было влиять.
Но прочь неприятности, потому что в С++11, помимо enum class появилась возможность указания размера scoped и unscoped enum'ов:
enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
E_APPLE = 0x01,
E_WATERMELON = 0x02,
E_COCONUT = 0x04,
E_STRAWBERRY = 0x08,
E_CHERRY = 0x10,
E_PINEAPPLE = 0x20,
E_BANANA = 0x40,
E_MANGO = 0x80,
E_DEVIL_FRUIT = 0xFF
};
И теперь вы может контролировать и сами задавать размер перечисления.
Control your size. Stay cool.
#cpp11
❤26👍15🔥4😁4
Удобно превращаем 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❤10🥱1
Мувать не всегда дешево
#новичкам
С приходом мув семантики настали "прекрасные плюсы будущего". Нет никакого копирования, чудо-оптимизации бороздят просторы стека и кучи. Не жизнь, а сказка.
Но мир не такой уж солнечный и приветливый. Это очень опасное...
Если вы придерживаетесь RAII, пользуетесь контейнерами и умными указателями, то вы практически всегда пользуетесь правилом нуля и никогда не определяете самостоятельно специальные методы класса и, в частности, конструктор перемещения и оператор перемещающего присваивания. Компилятор сгенерирует их за вас, ленивых дядь.
Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.
И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:
Что выбрать?
"Конечно мувнуть, это же не долгое копирование, выполнится быстро" - вот к таким не совсем корректным мыслям может привести "оторванность от низов".
Кажется, что у некоторых людей есть ощущение, что данные из одного объекта как-то перетекают в другой объект и это происходит очень быстро.
Но это не так! Перемещение - это поверхностное копирование.
Возьмем простой пример:
Что будет происходить при перемещении
Чуть сложнее:
Что будет при перемещении
Можно еще занулить конечно, но это редко происходит из соображений перфоманса.
Получается, что реально "переместить" вы можете только данные, выделенные на куче. И то они никуда не перемещаются. Вы просто копируете указатель из одного объекта в другой, при этом сами данные никак не затрагиваются.
Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.
Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.
Теперь снова актуализируем вопрос: мувать или копировать?
И ответ уже не плоскости оптимизации, а в плоскости логики кода. Перемещайте, когда вам в текущем скоупе объект больше не нужен и копируйте, если нужен. Тогда вы не пытаетесь оптимизировать код, а передаете владение объектом другому коду. Редко, когда вы на авито продаете вещи, чтобы заработать. Вы их продаете, чтобы от лишнего избавиться и дать их тем, кому они нужны, особой выгоды не ожидая. Вот здесь примерно это и должно происходить.
В реальности все немного сложнее и всегда будут исключения, но просто хочу обратить внимание, что мув семантика - это в первую очередь про передачу владения объектом и только потом уже оптимизация.
Think logically. Stay cool.
#cppcore #cpp11
#новичкам
С приходом мув семантики настали "прекрасные плюсы будущего". Нет никакого копирования, чудо-оптимизации бороздят просторы стека и кучи. Не жизнь, а сказка.
Но мир не такой уж солнечный и приветливый. Это очень опасное...
Если вы придерживаетесь RAII, пользуетесь контейнерами и умными указателями, то вы практически всегда пользуетесь правилом нуля и никогда не определяете самостоятельно специальные методы класса и, в частности, конструктор перемещения и оператор перемещающего присваивания. Компилятор сгенерирует их за вас, ленивых дядь.
Рано или поздно вы немного отрываетесь от "низов": вас уже не интересует КАК конкретно эти методы реализованы. Вы оперируете более высокоуровневыми сущностями и полагаетесь на компилятор.
И вот вы в ситуации, когда у вас есть данные, обернутые в класс, которые легально по контексту кода можно мувнуть или скопировать. Условно говоря, у вас есть функция Process, которая принимает данные по значению, чтобы поддержать оба варианта передачи: копирование и мув:
void Process(Data data);
Что выбрать?
"Конечно мувнуть, это же не долгое копирование, выполнится быстро" - вот к таким не совсем корректным мыслям может привести "оторванность от низов".
Кажется, что у некоторых людей есть ощущение, что данные из одного объекта как-то перетекают в другой объект и это происходит очень быстро.
Но это не так! Перемещение - это поверхностное копирование.
Возьмем простой пример:
struct Data {
int a;
double b;
};
Data obj1{3, 3.14};
Data obj2 = std::move(obj1);Что будет происходить при перемещении
obj1? Копирование a и b.Чуть сложнее:
struct Data {
std::array<int, 5> arr;
};
Data obj1{.arr = {1, 2, 3, 4, 5}};
Data obj2 = std::move(obj1);Что будет при перемещении
obj1, а значит и arr? Тоже копирование! std::array - это массив, фиксированного размера, расположенный на стеке. Как вы собираетесь его перемещать в другой объект? Под другой объект уже выделена своя память на стеке, вы не можете один кусок стека переместить в другой. Вы можете только скопировать значения.Можно еще занулить конечно, но это редко происходит из соображений перфоманса.
Получается, что реально "переместить" вы можете только данные, выделенные на куче. И то они никуда не перемещаются. Вы просто копируете указатель из одного объекта в другой, при этом сами данные никак не затрагиваются.
struct Data {
std::string * str;
// member functions for making it work properly
};
Data obj1{.str = new std::string("Hello, World!")};
Data obj2 = std::move(obj1);obj2 теперь имеет такое же значение указателя str, как и obj1, но сама строка оказалась нетронутой.Более того. Даже если вы используете std::string, то не всегда мув будет быстрее копирования! Thanks to SSO.
Получается, что никто никуда не течет. Все так же пресловуто копируется, кроме динамических данных под указателями.
Теперь снова актуализируем вопрос: мувать или копировать?
И ответ уже не плоскости оптимизации, а в плоскости логики кода. Перемещайте, когда вам в текущем скоупе объект больше не нужен и копируйте, если нужен. Тогда вы не пытаетесь оптимизировать код, а передаете владение объектом другому коду. Редко, когда вы на авито продаете вещи, чтобы заработать. Вы их продаете, чтобы от лишнего избавиться и дать их тем, кому они нужны, особой выгоды не ожидая. Вот здесь примерно это и должно происходить.
В реальности все немного сложнее и всегда будут исключения, но просто хочу обратить внимание, что мув семантика - это в первую очередь про передачу владения объектом и только потом уже оптимизация.
Think logically. Stay cool.
#cppcore #cpp11
3👍41❤17🔥7😎5
Передача владения
#новичкам
Захотелось совсем немного развить тему предыдущего поста.
В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.
Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.
Другой пример:
Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.
Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.
Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.
Поэтому можно принять за правило, что, передав владение, вы больше физически не имеете права пользоваться объектом. Это как продав компанию, вы бы продолжили иметь то же влияние на нее. Нетушки. Либо крестик снимите, либо трусы наденьте. Либо передали владение и забыли, либо скопировали и дальше попользовались.
Give away what you don't need. Stay cool.
#cppcore #cpp11
#новичкам
Захотелось совсем немного развить тему предыдущего поста.
В целом, мув семантика она не столько про оптимизацию(для этого есть например rvo/nrvo), сколько про передачу владения объектами. И то, что std::move ничего не мувает(а пытается сделать каст к rvalue reference) хорошо укладывается в эту концепцию. Данные не перемещаются, но вы говорите, что передаете владение этими данными.
void bar(std::vector<int>&& vec) {
// do nothing
}
void foo() {
std::vector<int> vec = {1, 2, 3};
bar(std::move(vec));
}Здесь мы передаем владение вектором из foo в bar. Заметьте, что bar оперирует правой ссылкой, то есть никакие перемещающие конструкторы не вызывались. Но такая сигнатура говорит о главном: bar ожидает эксклюзивного права владения над этим вектором. Вы должны явно мувнуть объект, чтобы вызвать bar. И не важно, что он дальше bar с этим вектором делает. Может ничего не сделает, а может и использует как-то данные. Но так решил автор кода: вызов bar предполагает передачу ему владения вектором.
Другой пример:
std::vector<int> double_elements(std::vector<int> vec) {
for (auto& elem: vec) {
elem *= 2;
}
return vec;
}
void foo() {
std::vector<int> vec = {1, 2, 3};
{
auto doubled = double_elements(vec);
std::println("{}", doubled);
}
vec.push_back(4);
{
auto doubled = double_elements(std::move(vec));
std::println("{}", doubled);
}
}
Функция double_elements принимает вектор по значению и возвращает набор из удвоенных элементов.
Функция foo 2 раза вызывает удвоение значений элементов. По логике функции foo, ей еще нужен vec в целости и сохранности(нужно доложить в него элемент). Поэтому она и передает в первый раз vec в double_elements по значению. Но после второго вызова вектор ей больше не нужен. Поэтому можно передать владение им в double_elements: возможно он им распорядится лучше.
Еще одна вещь, которая подчеркивает передачу владения: moved-from объект практически никак в общем случае нельзя безопасно использовать, кроме как безопасно разрушить или переприсвоить(в комментах под прошлым постом более конкретно обсуждали этот момент). Даже если функция принимает rvalue reference, это не значит, что она не изменяет объект: возможно внутренние вызовы это делают.
Поэтому можно принять за правило, что, передав владение, вы больше физически не имеете права пользоваться объектом. Это как продав компанию, вы бы продолжили иметь то же влияние на нее. Нетушки. Либо крестик снимите, либо трусы наденьте. Либо передали владение и забыли, либо скопировали и дальше попользовались.
Give away what you don't need. Stay cool.
#cppcore #cpp11
❤19👍13🔥6
Время жизни и range-based for
#новичкам
Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:
Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?
Перед ответом экскурс в стандарт. Есть у вас range-based for:
Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:
Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.
В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.
А вот во что преобразуется цикл из самого первого примера поста:
range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.
Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:
Другие решения рассмотрим в следующих постах.
Avoid dangling references. Stay cool.
#cppcore #cpp11
#новичкам
Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:
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);
}Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?
Перед ответом экскурс в стандарт. Есть у вас range-based for:
for(const auto& item: <range>) {
process(item);
}Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:
auto&& range = <range>;
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
process(item);
}
Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.
std::vector<int> generate() {
return {1, 2, 3, 4, 5};
}
for (const auto& item: generate()) {
...
}
// range-based for transforms into
auto&& range = generate();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
...
}В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.
А вот во что преобразуется цикл из самого первого примера поста:
auto&& range = generateData().items();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
int x = *it;
process(x);
}
range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.
Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
auto data = generateData();
for (int x : data.items()) {
process(x);
}Другие решения рассмотрим в следующих постах.
Avoid dangling references. Stay cool.
#cppcore #cpp11
❤34🔥23👍8😁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❤16👍9🤯3
Как получить длину строкового литерала?
#опытным
Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод 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
🔥45👍18❤9🤯1
Пользовательские литералы
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.
В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:
👉🏿 Целочисленные:
👉🏿 С плавающей точкой:
👉🏿 Символьные: '
👉🏿 Строковые:
👉🏿 Логические:
👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя -
Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.
Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.
А мы живем все-таки в мире объектов. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.
Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.
То есть в С++ давно был механизм, с помощью которого можно было изменять тип литерала через суффикс. Стоит лишь дать возможность программистам самостоятельно определять свои суффиксы, чтобы по-своему интерпретировать литерал.
Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!
Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:
Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.
Просто посмотрите, насколько сократился код, уменьшилось количество скобок и увеличилась читаемость. Выглядит круто.
Это было небольшое интро, в следующий раз рассмотрим кейсы, когда пользовательские литералы могут принести реальную пользу.
Extend your capabilities. Stay cool.
#cppcore #cpp11
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.
В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:
👉🏿 Целочисленные:
5, 42, 0xFF.👉🏿 С плавающей точкой:
3.14, 6.02e23.👉🏿 Символьные: '
a', '\n'.👉🏿 Строковые:
"Hello, world!".👉🏿 Логические:
true, false.👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя -
nullptr.Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.
Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.
А мы живем все-таки в мире объектов. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.
Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.
То есть в С++ давно был механизм, с помощью которого можно было изменять тип литерала через суффикс. Стоит лишь дать возможность программистам самостоятельно определять свои суффиксы, чтобы по-своему интерпретировать литерал.
Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!
Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:
class Temperature {
private:
double kelvin; // for precicion and consistency
explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};
Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}
{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}
Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.
Просто посмотрите, насколько сократился код, уменьшилось количество скобок и увеличилась читаемость. Выглядит круто.
Это было небольшое интро, в следующий раз рассмотрим кейсы, когда пользовательские литералы могут принести реальную пользу.
Extend your capabilities. Stay cool.
#cppcore #cpp11
❤25👍14🔥11🤯2🤷♂1🤪1