Новый год
Вот и подходит к своему завершению 2025 год.
Самое время подвести его итоги. Но я не буду перечислять список полезных или популярных постов. Кто захочет, то найдет. Я хочу поделиться состоянием(вайбом для зумеров) от завершающего свой последний аккорд года.
⚡️ Меня радует, что качество постов наконец-то меня удовлетворяет. Не по обертке(всегда бывают косяки), а по сути, структурированности и целостности. Можете ради интереса вернуться к первым постам и проследить эволюцию стиля. Количество юморесок и степень свободы изложения заметно сократились в угоду системности. Это логичная проф деформация, когда каждый раз при написании постов делаешь инъекцию 3-х кубиков стандарта внутривенно. Это несомненно позитивные изменения. Но теперь хочется, чтобы маятник вновь качнулся в сторону развлекательного аспекта. Все-таки мы здесь покекать пришли, а не плюсы учить. Или у вас не так?)
⚡️ Канал свел меня с прекрасными людьми, с которыми у нас началось сотрудничество по части плюсов и не только. Я очень вырос благодаря этим связям и не могу наудивляться, насколько у нас здесь клевые ребята собрались.
⚡️ Очень радует, что подписчики все чаще предлагают какие-то идеи для постов, особенно лично. Благодаря некоторым появились даже целые рубрики. Видно, что наше коммьюнити живое и активно развивается. У нас можно и поболтать за жизнь, и похоливарить на тему языков, и узнать что-то новенькое от крутых спецов. Есть более профессиональные чаты для гурманов и заядлых сомелье С++, но мне кажется, что определенную нишу мы заняли.
У меня это был эпический год по всем направлениям в жизни. Накопилась большая усталось, но при этом на лице улыбка.
И хочу вам сказать большое спасибо. Всем и каждому. Именно благодаря вам я расту и развиваюсь. Благодаря вам куча людей в принципе может регулярно получать небольшую порцию любимого С++ к обеду. Надеюсь, что канал вам нравится и вы находите его полезным. Без вас ничего этого не было бы.
Желаю вам в будущем году плотно и потно трудиться. Легко не будет, особенно начинающим, но у вас все получится. Идите к своим целям на всех парах. Верю в вас всех и каждого.
С Новым годом, друзья! С новым счастьем!☃️🎊🎉🎄
Вот и подходит к своему завершению 2025 год.
Самое время подвести его итоги. Но я не буду перечислять список полезных или популярных постов. Кто захочет, то найдет. Я хочу поделиться состоянием(вайбом для зумеров) от завершающего свой последний аккорд года.
⚡️ Меня радует, что качество постов наконец-то меня удовлетворяет. Не по обертке(всегда бывают косяки), а по сути, структурированности и целостности. Можете ради интереса вернуться к первым постам и проследить эволюцию стиля. Количество юморесок и степень свободы изложения заметно сократились в угоду системности. Это логичная проф деформация, когда каждый раз при написании постов делаешь инъекцию 3-х кубиков стандарта внутривенно. Это несомненно позитивные изменения. Но теперь хочется, чтобы маятник вновь качнулся в сторону развлекательного аспекта. Все-таки мы здесь покекать пришли, а не плюсы учить. Или у вас не так?)
⚡️ Канал свел меня с прекрасными людьми, с которыми у нас началось сотрудничество по части плюсов и не только. Я очень вырос благодаря этим связям и не могу наудивляться, насколько у нас здесь клевые ребята собрались.
⚡️ Очень радует, что подписчики все чаще предлагают какие-то идеи для постов, особенно лично. Благодаря некоторым появились даже целые рубрики. Видно, что наше коммьюнити живое и активно развивается. У нас можно и поболтать за жизнь, и похоливарить на тему языков, и узнать что-то новенькое от крутых спецов. Есть более профессиональные чаты для гурманов и заядлых сомелье С++, но мне кажется, что определенную нишу мы заняли.
У меня это был эпический год по всем направлениям в жизни. Накопилась большая усталось, но при этом на лице улыбка.
И хочу вам сказать большое спасибо. Всем и каждому. Именно благодаря вам я расту и развиваюсь. Благодаря вам куча людей в принципе может регулярно получать небольшую порцию любимого С++ к обеду. Надеюсь, что канал вам нравится и вы находите его полезным. Без вас ничего этого не было бы.
Желаю вам в будущем году плотно и потно трудиться. Легко не будет, особенно начинающим, но у вас все получится. Идите к своим целям на всех парах. Верю в вас всех и каждого.
С Новым годом, друзья! С новым счастьем!☃️🎊🎉🎄
23❤92🎄22👍16☃13❤🔥3👎1🗿1
Праздник Баг к нам приходит
Врываемся в новый год с поучительных историй.
На дворе 31 декабря 2к21 года. В России все нарезают салаты, готовятся веселиться и еще не знают, какой год их ждет впереди. На западе тоже все еще празднуют, хотя Рождество уже позади.
В первые минуты января сверкает салют, хлопаются хлопушки, шампанское переливается через края бокалов и перестает доставляться вся электронная почта, за обработку и доставку которой отвечал Microsoft Exchange Server.
Вот так в одну секунду по всему миру куча организаций перестала получать свою почту. В журнале событий сервера можно было найти такую запись: «Процесс сканирования FIP-FS Scan Process не прошел инициализацию. Ошибка: 0x8004005. Подробности ошибки: «Неопределенная ошибка» или «Код ошибки: 0x80004005». Описание ошибки: «Не удается преобразовать "2201010001" в длинное число».
Оказывается, что Exchange сохранял в журнале проверок антивируса даты в виде int32. Максимальным значением может быть 2147483647. У числа может быть всего 10 десятичных разрядов: первые 2 числа - последние 2 цифры года, дальше month/day/time. Для 2021 все было ок. Но чтобы представить первую секунду 2022 года нужно число 2201010001. А это уже перебор. Конвертация навернулась и полетели ошибки.
Эту проблему тогда обозвали Y2K22 bug, по аналогии с багом 2000 года(Y2K). В 20-м веке люди тоже часто обрабатывали только последние 2 цифры даты и в момент миллениума года сбрасывались просто до нуля.
Стоит ли говорить, что малварьный модель FIP-FS был скорее всего написан на С/C++?
Но не в этом дело. Если какой-то программист решил применить свою уникальную оптимизацию и не подумал чуть наперед, то здесь любым языком можно пакостей.
Вообще, ошибки связаные с переходом на новый год, не так уж и редки. У меня на проекте тоже был такой баг. Так что будьте внимательны при работе со временем. Хардкод и обрезание дат играют злую шутку.
А вы сталкивались с подобными ошибками? Расскажите в комментах.
Celebrate the holiday. Stay cool.
#fun
Врываемся в новый год с поучительных историй.
На дворе 31 декабря 2к21 года. В России все нарезают салаты, готовятся веселиться и еще не знают, какой год их ждет впереди. На западе тоже все еще празднуют, хотя Рождество уже позади.
В первые минуты января сверкает салют, хлопаются хлопушки, шампанское переливается через края бокалов и перестает доставляться вся электронная почта, за обработку и доставку которой отвечал Microsoft Exchange Server.
Вот так в одну секунду по всему миру куча организаций перестала получать свою почту. В журнале событий сервера можно было найти такую запись: «Процесс сканирования FIP-FS Scan Process не прошел инициализацию. Ошибка: 0x8004005. Подробности ошибки: «Неопределенная ошибка» или «Код ошибки: 0x80004005». Описание ошибки: «Не удается преобразовать "2201010001" в длинное число».
Оказывается, что Exchange сохранял в журнале проверок антивируса даты в виде int32. Максимальным значением может быть 2147483647. У числа может быть всего 10 десятичных разрядов: первые 2 числа - последние 2 цифры года, дальше month/day/time. Для 2021 все было ок. Но чтобы представить первую секунду 2022 года нужно число 2201010001. А это уже перебор. Конвертация навернулась и полетели ошибки.
Эту проблему тогда обозвали Y2K22 bug, по аналогии с багом 2000 года(Y2K). В 20-м веке люди тоже часто обрабатывали только последние 2 цифры даты и в момент миллениума года сбрасывались просто до нуля.
Стоит ли говорить, что малварьный модель FIP-FS был скорее всего написан на С/C++?
Но не в этом дело. Если какой-то программист решил применить свою уникальную оптимизацию и не подумал чуть наперед, то здесь любым языком можно пакостей.
Вообще, ошибки связаные с переходом на новый год, не так уж и редки. У меня на проекте тоже был такой баг. Так что будьте внимательны при работе со временем. Хардкод и обрезание дат играют злую шутку.
А вы сталкивались с подобными ошибками? Расскажите в комментах.
Celebrate the holiday. Stay cool.
#fun
👍23❤10🔥6😁4🫡3
Наследие Cfront. Компоновка
#новичкам
Продолжаем серию постов с прошлого года. Для тех, кто не в теме: пост про Cfront тут, начало серии тут.
С++ оказывал влияние не только на компиляторы, но и на линковщики.
Если в первых версиях языка и компилятора Cfront Бьерн сфокусировался на ООП и полиморфизме, то дальше были введены шаблоны и inline функции.
Шаблоны и inline функции просто исходя из своей механики работы предполагают то, что конкретные инстанциации и определения inline функций могут находиться в нескольких единицах трансляции в рамках одной программы.
Но в С был и есть One Definition Rule, который запрещал иметь более одного определения сущности в рамках одной программы.
Поначалу для решения этой проблемы использовались разные хаки: от макросов и магии с именами до ручной инстанциации шаблонов в одном cpp файле.
Это конечно было неудобно, но благо С++ становился все более популярным и влиятельным. Поэтому команда Cfront начала активно взаимодействовать с разработчиками линковщиков для того, чтобы ввести так поддержку слабых символов. Их в программе может быть сколько угодно, линковщик выберет один любой из них и будет ссылать на этот символ все заглушки. Главное, чтобы все определения символа были одинаковыми, иначе UB.
Также в С++ появились глобальные объекты. А глобальные объекты требуют своей инициализации (то есть выполнения кода) до main. В сижке такого нет, там исполнение пользовательского кода начинается с вызова main. Кстати поэтому в С нет SIOF.
Поэтому приходилось извращаться. Формировать массив конструкторов глобальных объектов и вызывать его первой инструкцией main. Разрушение происходило в конце main с помощью массива деструкторов.
Но это костыль и нужно было нормальное решение. Результатом совместной работы Cfront и линковщиков стали секции .ctors/.dtors в объектных и бинарных файлах. Там находятся информация о том, какие глобальные пользовательские объекты есть в коде. Код конструкторов и деструкторов объектов из этих секций выполняется до и после main соответственно.
Таким было наследие Cfront. The end.
Have a legacy. Stay cool.
#compiler
#новичкам
Продолжаем серию постов с прошлого года. Для тех, кто не в теме: пост про Cfront тут, начало серии тут.
С++ оказывал влияние не только на компиляторы, но и на линковщики.
Если в первых версиях языка и компилятора Cfront Бьерн сфокусировался на ООП и полиморфизме, то дальше были введены шаблоны и inline функции.
Шаблоны и inline функции просто исходя из своей механики работы предполагают то, что конкретные инстанциации и определения inline функций могут находиться в нескольких единицах трансляции в рамках одной программы.
Но в С был и есть One Definition Rule, который запрещал иметь более одного определения сущности в рамках одной программы.
Поначалу для решения этой проблемы использовались разные хаки: от макросов и магии с именами до ручной инстанциации шаблонов в одном cpp файле.
Это конечно было неудобно, но благо С++ становился все более популярным и влиятельным. Поэтому команда Cfront начала активно взаимодействовать с разработчиками линковщиков для того, чтобы ввести так поддержку слабых символов. Их в программе может быть сколько угодно, линковщик выберет один любой из них и будет ссылать на этот символ все заглушки. Главное, чтобы все определения символа были одинаковыми, иначе UB.
Также в С++ появились глобальные объекты. А глобальные объекты требуют своей инициализации (то есть выполнения кода) до main. В сижке такого нет, там исполнение пользовательского кода начинается с вызова main. Кстати поэтому в С нет SIOF.
Поэтому приходилось извращаться. Формировать массив конструкторов глобальных объектов и вызывать его первой инструкцией main. Разрушение происходило в конце main с помощью массива деструкторов.
Но это костыль и нужно было нормальное решение. Результатом совместной работы Cfront и линковщиков стали секции .ctors/.dtors в объектных и бинарных файлах. Там находятся информация о том, какие глобальные пользовательские объекты есть в коде. Код конструкторов и деструкторов объектов из этих секций выполняется до и после main соответственно.
Таким было наследие Cfront. The end.
Have a legacy. Stay cool.
#compiler
👍27🔥11❤8
Cppfront
#опытным
Есть интересный проект у Герба Саттера - компилятор Cppfront. По аналогии с Cfront, который компилировал С++ в С, Cppfront компилирует экспериментальный синтаксис Cpp2 в наш привычный C++.
Cpp2 - это можно сказать очень сахарный и слегка преобразованный С++. Герб говорит, что это не наследник и не соперник С++. Он помогает С++ эволюционировать, проверяя на нем функциональность, которую предлагают внести в С++.
Если просто: Герб и другие активные деятели С++ из комитета придумали Cpp2 и написали препроцессор, который преобразует новый синтаксис в наш привычный С++.
Как обычно завозятся фичи в стандарт: пишется огромный документ, где словами объясняется как должна работать фича. Но проверить на практике механизмы ее работы работы нельзя, можно лишь мысленные эксперименты проводить.
С появлением Cppfront, если кто-то хочет завести в стандарт С++ какую-то новую функциональность(например pattern matching), то ее можно относительно быстро реализовать в Сpp2 и поиграться с ней на практике. Коммьюнити может почелленджить решение и его использование, прочекать граничные и сложные случаи и тд.
То есть не нужно трогать существующие компиляторы, стандартные библиотеки, разбираться в синтаксическом анализе, копаться в кишках реализации и тд. Просто пишешь парсер и фичу уже можно потрогать руками.
И это будет работать на любой платформе, где есть С++ компилятор!
Простой пример кода на Cpp2:
Видно, что добавлены всякие аннотации, универсальные инициализации переменных и функций. Внутреннее наполнение функций примерно такое же, но под другой оберткой.
И очень прикольно, что вы сами можете поиграться с Cpp2 в годболте и посмотреть какой будет итоговый С++ код.
Вот ссылочка на гитхаб и на доку.
Help to evolve. Stay cool.
#compiler
#опытным
Есть интересный проект у Герба Саттера - компилятор Cppfront. По аналогии с Cfront, который компилировал С++ в С, Cppfront компилирует экспериментальный синтаксис Cpp2 в наш привычный C++.
Cpp2 - это можно сказать очень сахарный и слегка преобразованный С++. Герб говорит, что это не наследник и не соперник С++. Он помогает С++ эволюционировать, проверяя на нем функциональность, которую предлагают внести в С++.
Если просто: Герб и другие активные деятели С++ из комитета придумали Cpp2 и написали препроцессор, который преобразует новый синтаксис в наш привычный С++.
Как обычно завозятся фичи в стандарт: пишется огромный документ, где словами объясняется как должна работать фича. Но проверить на практике механизмы ее работы работы нельзя, можно лишь мысленные эксперименты проводить.
С появлением Cppfront, если кто-то хочет завести в стандарт С++ какую-то новую функциональность(например pattern matching), то ее можно относительно быстро реализовать в Сpp2 и поиграться с ней на практике. Коммьюнити может почелленджить решение и его использование, прочекать граничные и сложные случаи и тд.
То есть не нужно трогать существующие компиляторы, стандартные библиотеки, разбираться в синтаксическом анализе, копаться в кишках реализации и тд. Просто пишешь парсер и фичу уже можно потрогать руками.
И это будет работать на любой платформе, где есть С++ компилятор!
Простой пример кода на Cpp2:
#include <iostream>
#include <string>
name: () -> std::string = {
s: std::string = "world";
decorate(s);
return s;
}
decorate: (inout s: std::string) = {
s = "[" + s + "]";
}
auto main() -> int {
std::cout << "Hello " << name() << "\n";
}
Видно, что добавлены всякие аннотации, универсальные инициализации переменных и функций. Внутреннее наполнение функций примерно такое же, но под другой оберткой.
И очень прикольно, что вы сами можете поиграться с Cpp2 в годболте и посмотреть какой будет итоговый С++ код.
Вот ссылочка на гитхаб и на доку.
Help to evolve. Stay cool.
#compiler
🔥31👍15❤11
Время жизни и 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
❤33🔥23👍8😁5
Инициализация внутри range-based for
#новичкам
Решение из предыдущего поста не совсем идеальное. Посмотрите сами:
Переменная data больше нигде кроме цикла не нужна. Но она засоряет собой и своим именем скоуп, требует отдельной инструкции, да и хотелось бы сразу избавляться от ненужного.
Было бы идеально инициализировать data прям внутри скоупа цикла, как это делается в условных операторах. И с С++20 это стало возможным:
Вот и все. Мы также создаем lvalue объект, но теперь его время жизни ограничено телом цикла. При выходе из цикла он уничтожится и мы можем забыть про этот объект.
Кстати. А задавались вы вопросом "нужны ли инструкции инициализации в цикле while?"
В if есть, в во всех вариантах цикла for есть. Для единообразия нужно и в while.
Как думаете?
На самом деле это будет избыточно. while+инициализация выглядела бы так:
Это легко заменяется на классический for без указания инструкций, которые после каждой итерации выполняются:
Даже короче получилось. Поэтому инициализация в while не нужна.
Forget about garbage. Stay cool.
#cpp20
#новичкам
Решение из предыдущего поста не совсем идеальное. Посмотрите сами:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
auto data = generateData();
for (int x : data.items()) {
process(x);
}Переменная data больше нигде кроме цикла не нужна. Но она засоряет собой и своим именем скоуп, требует отдельной инструкции, да и хотелось бы сразу избавляться от ненужного.
Было бы идеально инициализировать data прям внутри скоупа цикла, как это делается в условных операторах. И с С++20 это стало возможным:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (auto data = generateData(); int x : data.items()) {
process(x);
}Вот и все. Мы также создаем lvalue объект, но теперь его время жизни ограничено телом цикла. При выходе из цикла он уничтожится и мы можем забыть про этот объект.
Кстати. А задавались вы вопросом "нужны ли инструкции инициализации в цикле while?"
В if есть, в во всех вариантах цикла for есть. Для единообразия нужно и в while.
Как думаете?
На самом деле это будет избыточно. while+инициализация выглядела бы так:
while(size_t i = 0; some_condition) {...}Это легко заменяется на классический for без указания инструкций, которые после каждой итерации выполняются:
for(size_t i = 0; some_condition;) {...}Даже короче получилось. Поэтому инициализация в while не нужна.
Forget about garbage. Stay cool.
#cpp20
❤25👍14🔥6
Продлеваем жизнь временного объекта range based for
#опытным
На самом деле у проблемы в этом коде:
есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
#опытным
На самом деле у проблемы в этом коде:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t1) { return t1; } // всегда возвращает висячую ссылку
T g();
void foo()
{
for (auto e : f1(g())) {} // OK: время жизни возвращаемого значения g() продлено
for (auto e : f2(g())) {} // UB: локальный объект t1 функции f2 все равно разрушается при выходе из скоупа f2
}
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
👍35☃14❤10🔥5
Предотвращаем висячие ссылки
#опытным
Давайте снова взглянем на этот пример:
Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
#опытным
Давайте снова взглянем на этот пример:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
auto& vec = generateData().items();
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice
🔥20❤13👍9🤯3
split
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
А как сделатьпростейшую вещь разделить строку на С++?
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']
А как сделать
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
auto range = text | std::views::split(' ');
for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three fourЕсли вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
std::string text = "one two three four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp23
🔥38👍11❤9🤯2👎1
Прелести рэнджей
#опытным
Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.
Возьмем пример из прошлого поста:
Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.
И на самом деле мы получаем кучу аллокаций.
Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.
Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.
Но можно поступить проще и воспользоваться ренджами!
Рэнджи ленивые. Прям как студенты: всем на последнем курсе заранее дают задачу написать диплом, но обычно его начинают писать, когда научрук уже дает последнее китайское предупреждение вслед за пендалем, что нужно сделать работу уже вчера.
Но у рэнджей это скорее преимущество.
Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.
Мы просто пишем:
И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.
С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:
Отбрасываем первые два элемента и берем только первый оставшийся. И опять же, ничего не происходит.
Вычисления происходят, когда мы пытаемся что-то узнать о результирующем диапазоне, например, пустой ли он:
Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:
И здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.
Возможно find работал бы и быстрее(а это еще проверить надо), но рэнджи предлагают более понятную альтернативу самописному коду.
Be expressive. Stay cool.
#cpp20
#опытным
Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.
Возьмем пример из прошлого поста:
std::string text = "one two three four";
std::vector<std::string_view> strs;
boost::split(strs, text, boost::is_any_of(" "));
Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.
И на самом деле мы получаем кучу аллокаций.
Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.
Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.
Но можно поступить проще и воспользоваться ренджами!
Рэнджи ленивые. Прям как студенты: всем на последнем курсе заранее дают задачу написать диплом, но обычно его начинают писать, когда научрук уже дает последнее китайское предупреждение вслед за пендалем, что нужно сделать работу уже вчера.
Но у рэнджей это скорее преимущество.
Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.
Мы просто пишем:
auto range = text | std::views::split(' ');И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.
С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:
auto range = text | std::views::split(' ') | std::views::drop(2) | std::views::take(1);Отбрасываем первые два элемента и берем только первый оставшийся. И опять же, ничего не происходит.
Вычисления происходят, когда мы пытаемся что-то узнать о результирующем диапазоне, например, пустой ли он:
if (range.empty()) {
std::cerr << "There are less than three words in text";
}Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:
std::string_view str{*range.begin()};
std::cout << str << std::endl;
// OUTPUT: threeИ здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.
Возможно find работал бы и быстрее(а это еще проверить надо), но рэнджи предлагают более понятную альтернативу самописному коду.
Be expressive. Stay cool.
#cpp20
👍27🔥14❤9😁3
std::to_array
#опытным
std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):
либо надеяться на CTAD и не передавать никаких параметров:
Если так сложились звезды, что у вас тип инициализатора совпадает с желаемым типом элементов массива, то вам прекрасно подойдет последний вариант.
И вот здесь всплавает тот самый нюанс.
Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:
и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.
Мне конечно не жалко написать эту жалкую тройку, пальцы не сотрутся. Но зачем? Компилятор же может определить размер и это уменьшит количество потенциальных ошибок.
У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:
Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.
Применяется std::to_array так:
Да, пишем на пару символов больше, зато размер будет четко соблюдаться.
Automate your tools. Stay cool.
#cpp20
#опытным
std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):
std::array<int, 4> arr = {1, 2, 3, 4};либо надеяться на CTAD и не передавать никаких параметров:
std::array arr = {1, 2, 3, 4};Если так сложились звезды, что у вас тип инициализатора совпадает с желаемым типом элементов массива, то вам прекрасно подойдет последний вариант.
И вот здесь всплавает тот самый нюанс.
Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:
std::array<std::string_view> arr = {"La", "Bu", "Bu"};и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.
Мне конечно не жалко написать эту жалкую тройку, пальцы не сотрутся. Но зачем? Компилятор же может определить размер и это уменьшит количество потенциальных ошибок.
У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:
namespace detail {
template <class T, std::size_t N, std::size_t... I>
constexpr std::array<std::remove_cv_t<T>, N>
to_array_impl(T (&a)[N], std::index_sequence<I...>) {
return {{a[I]...}};
}
} // namespace detail
template <class T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T (&a)[N]) {
return detail::to_array_impl(a, std::make_index_sequence<N>{});
}Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.
Применяется std::to_array так:
constexpr auto names = std::to_array<std::string_view>({"Goku", "Luffy", "Ichigo", "Gojo", "Joseph", "L"});Да, пишем на пару символов больше, зато размер будет четко соблюдаться.
Automate your tools. Stay cool.
#cpp20
❤26👍15🔥9🥱2☃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👍10😁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
🔥43👍16❤8
Пользовательские литералы
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому 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
❤24👍12🔥11🤯2🤷♂1🤪1
Пользовательские литералы. А зачем?
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.
Поехали:
🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:
Меньше деталей, больше фокуса на происходящем.
🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании
Получится неожиданный результат, даже если функция работает верно.
Вот шобы такого не было, можно использовать соответствующие литералы:
🥨 Автоматический вывод типов может легкосломаться вывести не тот тип, который вы ожидаете, если вы работаете с сырыми литералами. Пользовательский литерал же сразу на месте конструирует нужный объект и компилятор будет правильно интерпретировать его тип.
Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:
Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.
🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:
И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.
В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.
Be useful. Stay cool.
#cppcore #cpp11 #cpp20
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.
Поехали:
🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:
auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;Меньше деталей, больше фокуса на происходящем.
🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании
auto. Поэтому легко может произойти такая ситуация, что вы возьмете и будете совместно оперировать синтаксически одинаковыми типами, но на деле они будут обозначать разные вещи. Условно, будем складывать градусы и радианы:double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree
Получится неожиданный результат, даже если функция работает верно.
Вот шобы такого не было, можно использовать соответствующие литералы:
class Radian {...};
Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}
SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error
🥨 Автоматический вывод типов может легко
Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:
using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char *>);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);
Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.
🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:
template<size_t N>
struct FixedString {
char data[N];
constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}
constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};
template <FixedString str>
class Class {};
Class<"Hello World!"> cl;
И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.
В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.
Be useful. Stay cool.
#cppcore #cpp11 #cpp20
👍14❤9🔥7😁1
Привычно возводим в степень
Спасибо, @Ivaneo, за любезно предоставленную идею для поста.
Как же бесило в универе, что в С/С++ нет нормального оператора возведения в степень, приходится использовать библиотечную std::pow. В других же языках такое есть. Например в питоне и рубях это оператор
В С++ мы такого в общем виде получить, к сожалению, не можем. Но можем сделать даже лучше в очень определенном сценарии.
И в этом нам помогут пользовательские литералы. Смотрите сами:
Берем уникод символ двойки верхнего регистра и делаем его суффикстом пользовательского литерала. И получаем почти привычную работающую версию возведения в квадрат! Это конечно не совсем стандарт, но на основных компиляторах работает.
А если еще и суффикс убрать:
То будет вообще огонь! Прям как в школе учили.
Да, суффиксы без андерскора запрещено использовать, так как они зарезервированы для стандарта. Но тем не менее это работает с варнингами на gcc и msvc, но уже не собирается на кланге.
Забавный примерчик. Жаль, что это может работать только для литералов и не распространяется на все переменные.
Provide better solutions. Stay cool.
#fun
Спасибо, @Ivaneo, за любезно предоставленную идею для поста.
Как же бесило в универе, что в С/С++ нет нормального оператора возведения в степень, приходится использовать библиотечную std::pow. В других же языках такое есть. Например в питоне и рубях это оператор
**(2 ** 3), а в Lua и Julia - оператор ^(2 ^ 3).В С++ мы такого в общем виде получить, к сожалению, не можем. Но можем сделать даже лучше в очень определенном сценарии.
И в этом нам помогут пользовательские литералы. Смотрите сами:
long double operator ""_²(long double d)
{
return d * d;
}
int main()
{
auto d = 2.0_²;
std::cout << d << "\n";
}
// OUTPUT:
// 4
Берем уникод символ двойки верхнего регистра и делаем его суффикстом пользовательского литерала. И получаем почти привычную работающую версию возведения в квадрат! Это конечно не совсем стандарт, но на основных компиляторах работает.
А если еще и суффикс убрать:
long double operator ""²(long double d)
{
return d * d;
}
int main()
{
auto d = 2.0²;
std::cout << d << "\n";
}
То будет вообще огонь! Прям как в школе учили.
Да, суффиксы без андерскора запрещено использовать, так как они зарезервированы для стандарта. Но тем не менее это работает с варнингами на gcc и msvc, но уже не собирается на кланге.
Забавный примерчик. Жаль, что это может работать только для литералов и не распространяется на все переменные.
Provide better solutions. Stay cool.
#fun
❤24🤣22👍11🔥6😁3🗿1
Забавный факт про std::unordered_map
#опытным
std::unoredered_map обязана работать на базе хэш-таблицы, чтобы удовлетворить требованиям по асимптотической сложности ее операций.
А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранит связный список элементов, у которых одинаковый ключ.
При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):
Но это не единственный способ итерироваться по мапе!
У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:
Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!
Вывод:
Пользы в этом немного, но может помочь, например, в отладке своей кастомном хэш-функции, чтобы добиться равномерного распределения.
Inspect your solutions. Stay cool.
#cpp11
#опытным
std::unoredered_map обязана работать на базе хэш-таблицы, чтобы удовлетворить требованиям по асимптотической сложности ее операций.
А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранит связный список элементов, у которых одинаковый ключ.
При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):
std::unoredered_map<std::string, int> map = ...;
for (const auto& [key, value]: map) {
...
}
Но это не единственный способ итерироваться по мапе!
У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:
local_iterator begin( size_type n );
local_iterator end( size_type n );
Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!
std::unordered_map<std::string, int> word_count = {
{"AI", 5}, {"evil", 7}, {"banana", 3},
{"date", 2}, {"elderberry", 4}
};
// Iterate over backets
for (size_t i = 0; i < word_count.bucket_count(); ++i) {
std::cout << "Bucket " << i << " ("
<< word_count.bucket_size(i) << " elements): ";
// Iterate inside certain backet
for (auto it = word_count.begin(i); it != word_count.end(i); ++it) {
std::cout << "[" << it->first << ":" << it->second << "] ";
}
std::cout << std::endl;
}Вывод:
Bucket 0 (0 elements):
Bucket 1 (0 elements):
Bucket 2 (2 elements): [date:2] [evil:7]
Bucket 3 (0 elements):
Bucket 4 (0 elements):
Bucket 5 (2 elements): [elderberry:4] [banana:3]
Bucket 6 (0 elements):
Bucket 7 (0 elements):
Bucket 8 (0 elements):
Bucket 9 (0 elements):
Bucket 10 (0 elements):
Bucket 11 (1 elements): [AI:5]
Bucket 12 (0 elements):
Пользы в этом немного, но может помочь, например, в отладке своей кастомном хэш-функции, чтобы добиться равномерного распределения.
Inspect your solutions. Stay cool.
#cpp11
🔥40😁15❤8👍6🤯2❤🔥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
❤28🔥12👍8😁7🤔1
Стандартные пользовательские литералы. Числовые
#новичкам
Числа подходят для инициализации многих сущностей. Метры, килограммы, градусы и тд. В стандартной библиотеке не так уж и много классов, значения которых можно представить числами. Но тем не менее они есть и литералы становятся довольно полезными при использовании.
Наибольшее распространение числовые пользовательские литералы получили при работе со временем.
Мы все привыкли писать:
Вы также можете верхнюю шестерку операторов использовать совместно в операциях:
Они совместимы и в результате получается объект общего типа(какой конкретно зависит от реализации, но скорее всего std::chrono::seconds в данном случае)
В крутящемся на проде коде нечасто можно увидеть использование какого-то захардкоженного промежутка времени. Обычно такие штуки уносят в конфигурацию, чтобы иметь возможность подкрутить эти параметры без изменения кода.
Однако литералы времени отлично можно применить в тестах. Например, хотите вы протестировать свой многопоточный шедулер. В качестве простого теста можно запихать в него лямбду, в которой установить значение промиса. А снаружи явно ждать установки значения:
если по истечению 100ms у фьючи не будет статуса "готово", то тест падает.
Интересный факт: оператор суффикс
Ну и еще есть литералы для комплексных чисел:
Работает там примерно так же, как в математике.
Не уверен, что кто-то этим пользуется. Но если пользуетесь, расскажите над каким проектом работаете, интересно же.
Be useful. Stay cool.
#cpp14
#новичкам
Числа подходят для инициализации многих сущностей. Метры, килограммы, градусы и тд. В стандартной библиотеке не так уж и много классов, значения которых можно представить числами. Но тем не менее они есть и литералы становятся довольно полезными при использовании.
Наибольшее распространение числовые пользовательские литералы получили при работе со временем.
Мы все привыкли писать:
5ч или 34мин. И начиная с С++14 мы примерно так и можем оперировать временем. Есть операторы преобразования целых и дробных чисел в годы, дни, часы, минуты, секунды, миллисекунды, микросекунды и наносекунды:using namespace std::literals;
auto ns = 100ns; // наносекунды
auto us = 100us; // микросекунды
auto ms = 100ms; // миллисекунды
auto s = 100s; // секунды
auto min = 100min; // минуты
auto h = 24h; // часы
auto d = 42d; // дни
auto y = 12y; // года
Вы также можете верхнюю шестерку операторов использовать совместно в операциях:
auto time = 1h + 30min + 90s;
Они совместимы и в результате получается объект общего типа(какой конкретно зависит от реализации, но скорее всего std::chrono::seconds в данном случае)
В крутящемся на проде коде нечасто можно увидеть использование какого-то захардкоженного промежутка времени. Обычно такие штуки уносят в конфигурацию, чтобы иметь возможность подкрутить эти параметры без изменения кода.
Однако литералы времени отлично можно применить в тестах. Например, хотите вы протестировать свой многопоточный шедулер. В качестве простого теста можно запихать в него лямбду, в которой установить значение промиса. А снаружи явно ждать установки значения:
std::promise<void> promise;
auto future = promise.get_future();
dispatcher_->schedule([&] { promise.set_value(); });
EXPECT_EQ(future.wait_for(100ms), std::future_status::ready);
если по истечению 100ms у фьючи не будет статуса "готово", то тест падает.
Интересный факт: оператор суффикс
s конфликтует своим именем с оператором преобразования к строке. Но проблема решается автоматически разным типом аргументов. Никаких реальных конфликтов, обычная перегрузка:std::string_literals::operator"" s(const char*, size_t)
std::chrono_literals::operator"" s(unsigned long long)
Ну и еще есть литералы для комплексных чисел:
using namespace std::literals;
auto c1 = 1.0 + 2.0i; // std::complex<double>(1.0, 2.0)
auto c2 = 3.0i; // std::complex<double>(0.0, 3.0)
auto c3 = 4.0if; // std::complex<float>(0.0f, 4.0f)
auto c4 = 5.0il; // std::complex<long double>(0.0L, 5.0L)
Работает там примерно так же, как в математике.
Не уверен, что кто-то этим пользуется. Но если пользуетесь, расскажите над каким проектом работаете, интересно же.
Be useful. Stay cool.
#cpp14
❤25😁17🔥12👍3⚡1
Доступ к приватным членам. Макросы
#новичкам
Доступ к приватным членам? Фуфуфу, это грязь! Да как вы смеете?! Хорошие люди старались, инкапсуляцию изобретали, а вы надругаться над ними хотите? Не по-славянски это, не по-православному...
Не далеки от правды слова выше. Если у вас уже есть какой-то работающий класс и вы хотите ужом извернуться, чтобы вытащить его кишки наружу - надо задуматься. О степени своей маниакальности, но главное - над архитектурой вашего кода. Потому что в большинстве случаев вы будете делать какое-то безобразие и разного рода хаки, чтобы подсмотреть в приватные поля. Лучше чуть подольше подумать и переработать целиком решение с учетом новых вводных.
Но!
Врага надо знать в лицо!
Поэтому в течение нескольких следующих постов мы будем обсуждать варианты инвазивного и неинвазивного доступа к приватным членам класса. Как говорится: не повторяйте в проде, чревато говнокодом по теории разбитых окон.
И на завтрак мы разберем самый простой способ. Макросы.
Есть у нас хэдэр:
Подключаем его в цппшник, но перед этим делаем грязь:
Строчкой
И это работает! Да, стандартом запрещено заменять макросами ключевые слова. Но это вообще не волнует компиляторы. gcc даже c флагами -pedantic -Wall не выдает никаких предупреждений. clang только с флагом -pedantic генерирует варнинг.
Конечно же за такой макрос надо не то что по рукам бить. Надо их из жопы вырывать без анастезии и вставлять в нормальное место.
Пожалуй, это самый дурнопахнущих из всех способов, потому что ломает инкапсуляцию прям везде. Так сказать начали с вкуснятины. Но оставайтесь на свзяи, продолжение тоже будет вкусным.
Be legal. Stay cool.
#NONSTANDARD #badpractice
#новичкам
Доступ к приватным членам? Фуфуфу, это грязь! Да как вы смеете?! Хорошие люди старались, инкапсуляцию изобретали, а вы надругаться над ними хотите? Не по-славянски это, не по-православному...
Не далеки от правды слова выше. Если у вас уже есть какой-то работающий класс и вы хотите ужом извернуться, чтобы вытащить его кишки наружу - надо задуматься. О степени своей маниакальности, но главное - над архитектурой вашего кода. Потому что в большинстве случаев вы будете делать какое-то безобразие и разного рода хаки, чтобы подсмотреть в приватные поля. Лучше чуть подольше подумать и переработать целиком решение с учетом новых вводных.
Но!
Врага надо знать в лицо!
Поэтому в течение нескольких следующих постов мы будем обсуждать варианты инвазивного и неинвазивного доступа к приватным членам класса. Как говорится: не повторяйте в проде, чревато говнокодом по теории разбитых окон.
И на завтрак мы разберем самый простой способ. Макросы.
Есть у нас хэдэр:
// header.cpp
#pragma once
class X {
public:
X() : private_(1) { /.../
}
template <class T>
void f(const T &t) { /.../
}
int Value() { return private_; }
// ...
private:
int private_;
};
Подключаем его в цппшник, но перед этим делаем грязь:
#define private public
#include "source.h"
#include <iostream>
void Hijack( X& x )
{
x.private_ = 2;
}
int main() {
X x;
Hijack(x);
std::cout << "Hi, there! Hack has performed successfully" << std::endl;
}
Строчкой
#define private public вы заменяете все нижележащие по коду вхождения слова private на public. Таким образом вы не трогаете хэдэр, но все равно имеете доступ к абсолютно всем его полям и методам.И это работает! Да, стандартом запрещено заменять макросами ключевые слова. Но это вообще не волнует компиляторы. gcc даже c флагами -pedantic -Wall не выдает никаких предупреждений. clang только с флагом -pedantic генерирует варнинг.
Конечно же за такой макрос надо не то что по рукам бить. Надо их из жопы вырывать без анастезии и вставлять в нормальное место.
Пожалуй, это самый дурнопахнущих из всех способов, потому что ломает инкапсуляцию прям везде. Так сказать начали с вкуснятины. Но оставайтесь на свзяи, продолжение тоже будет вкусным.
Be legal. Stay cool.
#NONSTANDARD #badpractice
❤35👍11😁10😭6🔥5🤯5😱4🤣1
Доступ к приватным членам. Указатали
#новичкам
Если по-честному, то все эти спецификаторы доступа к членам класса, это чисто синтаксическое ограничение на непреднамеренное использование в коде имен непубличных членов. Ну и способ выделение в классе интерфейса, чтобы сказать другим программистам, какими легальными способами можно оперировать объектом.
Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.
То есть банально
Как бы логично предположить, что если у класса только одно поле, то сам объект будет состоять только из этого поля. И можно спокойно привести указатель на объект к указателю на поле.
Но здесь есть целых 2 проблемы.
1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.
2️⃣ Вторая еще серьезнее. Цитата из стандарта:
Если ваша кодовая конструкция интерпретируется, как использование недоступных вам мемберов, то конструкция ill-formed. Не сказано, что сама программа ifndr, но это все равно значит, что код выше не соответствует правилам языка.
Первую проблему можно обойти с помощьюдыры в стандарте memcpy:
Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.
Если мемберов много, то нужно будет учитывать выравнивание полей в объекте.
В общем, это все может работать на конкретной архитектуре и компиляторе, если вы сами все руками в каждом конкретном случае проверяете. Но стандарт вас осуждает и ничего не обещает.
Пусть в конце каждого поста из серии будет эпилог: получать доступ к приватным полям - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.
Be legal. Stay cool.
#cppcore #badpractice
#новичкам
Если по-честному, то все эти спецификаторы доступа к членам класса, это чисто синтаксическое ограничение на непреднамеренное использование в коде имен непубличных членов. Ну и способ выделение в классе интерфейса, чтобы сказать другим программистам, какими легальными способами можно оперировать объектом.
Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.
То есть банально
class MyClass {
private:
int secret = 42;
};
void illegalAccess(MyClass &obj) {
int *ptr = (int *)&obj; // assume that secret is first member
std::cout << "Illegal: " << *ptr << std::endl;
}Как бы логично предположить, что если у класса только одно поле, то сам объект будет состоять только из этого поля. И можно спокойно привести указатель на объект к указателю на поле.
Но здесь есть целых 2 проблемы.
1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.
2️⃣ Вторая еще серьезнее. Цитата из стандарта:
The interpretation of a given construct is established without regard to access control. If the interpretation established makes use of inaccessible members or base classes, the construct is ill-formed. Если ваша кодовая конструкция интерпретируется, как использование недоступных вам мемберов, то конструкция ill-formed. Не сказано, что сама программа ifndr, но это все равно значит, что код выше не соответствует правилам языка.
Первую проблему можно обойти с помощью
void AccessWithMemcpy(MyClass& obj) {
int value;
std::memcpy(&value, &obj, sizeof(int));
std::cout << value << std::endl;
}Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.
Если мемберов много, то нужно будет учитывать выравнивание полей в объекте.
В общем, это все может работать на конкретной архитектуре и компиляторе, если вы сами все руками в каждом конкретном случае проверяете. Но стандарт вас осуждает и ничего не обещает.
Пусть в конце каждого поста из серии будет эпилог: получать доступ к приватным полям - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.
Be legal. Stay cool.
#cppcore #badpractice
❤21👍17😁9🔥6❤🔥1😭1