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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Инициализация внутри range-based for
#новичкам

Решение из предыдущего поста не совсем идеальное. Посмотрите сами:

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

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

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

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

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:

auto& vec = generateData().items();


Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.

Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.

Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.

Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.

Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.

Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?

И это действительно решает проблему.

Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}

~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};


std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.

Это также прекрасно решает проблему.

Prevent misuse. Stay cool.

#cpp11 #cpp20 #cpp23 #goodpractice
🔥2013👍9🤯3
​​split
#опытным

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

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

text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']


А как сделать простейшую вещь разделить строку на С++?

Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:

std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four


Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.

Поэтому люди городили свои огороды через find, стримы и прочее.

Но хочется чего-то родного.. Чего-то стандартного...

И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:

auto range = text | std::views::split(' ');

for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three four


Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:

auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]


Правда здесь уже нужен С++23 с его std::ranges::to.

У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:

std::string text = "one two  three   four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]


Все примеры можете найти здесь.

И жить стала еще чуть прекрасней)

Never too late. Stay cool.

#cpp20 #cpp23
🔥38👍119🤯2👎1
​​Прелести рэнджей
#опытным

Рэнджи - это не просто сахар и "уродливые" палки 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🔥149😁3
std::to_array
#опытным

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🥱21
​​WAT
#опытным

Спасибо, ₿ 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?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?

Дьявол кроется в деталях.

Вспоминаем школьную математеку CTAD. Какой тип элементов массива выведется?

Правильно, 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:

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👍168
Пользовательские литералы
#новичкам

Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому 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.

Поехали:

🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:

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
👍149🔥7😁1
Привычно возводим в степень

Спасибо, @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 и обычные итераторы(под капотом этого форика):

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😁158👍6🤯2❤‍🔥1
​​Стандартные пользовательские литералы. Строковые
#новичкам

Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.

Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без 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
​​Стандартные пользовательские литералы. Числовые
#новичкам

Числа подходят для инициализации многих сущностей. Метры, килограммы, градусы и тд. В стандартной библиотеке не так уж и много классов, значения которых можно представить числами. Но тем не менее они есть и литералы становятся довольно полезными при использовании.

Наибольшее распространение числовые пользовательские литералы получили при работе со временем.

Мы все привыкли писать: или 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👍31
​​Доступ к приватным членам. Макросы
#новичкам

Доступ к приватным членам? Фуфуфу, это грязь! Да как вы смеете?! Хорошие люди старались, инкапсуляцию изобретали, а вы надругаться над ними хотите? Не по-славянски это, не по-православному...

Не далеки от правды слова выше. Если у вас уже есть какой-то работающий класс и вы хотите ужом извернуться, чтобы вытащить его кишки наружу - надо задуматься. О степени своей маниакальности, но главное - над архитектурой вашего кода. Потому что в большинстве случаев вы будете делать какое-то безобразие и разного рода хаки, чтобы подсмотреть в приватные поля. Лучше чуть подольше подумать и переработать целиком решение с учетом новых вводных.

Но!

Врага надо знать в лицо!

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

И на завтрак мы разберем самый простой способ. Макросы.

Есть у нас хэдэр:

// 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
​​Доступ к приватным членам. Указатали
#новичкам

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

Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.

То есть банально

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

Первую проблему можно обойти с помощью дыры в стандарте memcpy:

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
Квиз
#новичкам

Буквально на секундочку вернемся к теме пользовательских литералов.

Со строковыми литералами всегда какая-то беда происходит. То тип путает карты, то этот null-terminator комом в горле встает, то чтобы вычислить длину надо быть кмс по приседаниям.

Но в комбинации с пользовательскими операторами может получиться такая кракозябра, что фиг разберешь.

А разбирать надо для понимания процессов. Поэтому сегодня проверим ваши интуицию/знания в рамках небольшого #quiz 'а.

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


#include <iostream>
#include <iomanip>
#include <type_traits>
#include <cmath>

int operator ""_length(const char*, std::size_t length) { return length; }

int main()
{
auto s = "A" "B"_length "C"
"D"
"E"
"FGH";

std::cout << s << "\n";
}
🤔10👍74🔥2🤯1
👨‍💻Хотите начать карьеру в разработке? Обратите внимание на Rust и познакомьтесь с ним за один вечер!

📆На открытом уроке 25 февраля в 20:00 МСК вы установите инструменты, разберётесь с rustc и Cargo и создадите своё первое приложение. Пошагово, с объяснением каждой команды и структуры проекта.

Вы увидите, как Rust решает реальные проблемы C++, Python и других языков, где ошибки часто проявляются слишком поздно. Поймёте философию языка и получите готовую среду для дальнейшего развития. Если вы рассматриваете Rust как следующий шаг в карьере, этот урок — эффективная точка входа.

👉Встречаемся в преддверии старта курса «Rust Developer. Basic». Зарегистрируйтесь и начните системно разбираться в языке, который уже меняет индустрию: https://otus.pw/3b4Q/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
🤣213👍2🔥2😁2😢1
​​WAT
#новичкам

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Ответ на квиз из поста выше - на экран выведется 8.

WAT? Строковые литералы конкатенируются? Да еще и пользовательский суффикс между двух литералов применяется к конкатенации?

Вообще, да. Сейчас во всем разберемся.

Для начала. Да, c-style строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.

С длинными строками, которые целиком не влезают на экран, неудобно работать: читать и редактировать. А если у вас настроены линтеры на ограничение длины строки, то все равно придется как-то разбивать на части эту длинную строку.

Можно это делать с помощью символов экранирования, например так:

auto str = "Suuuuuuuuuuuuuuupppeeeeeeeeeeeeeeeeeeeeeeeeeeerrrr
loooooooooooooooooooooooooooooong \ striiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";


Но здесь будут проблемы с тем, что вторая часть должна начинаться с самого начала следующей строки, иначе пробелы будут включены в сам литерал. Представьте, что будет с кодом, в котором внутри функций(например для репорта ошибок) будут так разделяться литералы.

Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:

auto str = "Hello "
// void
"World!";
std::cout << str << std::endl;

// OUTPUT
// Hello World!


Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.

Ну и теперь понятно, почему пользовательский суффикс применяется к полной конкатенации c-style строки. Фаза конкатенации строковых литералов идет раньше этапа компиляции, на котором определяется значение аргументов оператора. Поэтому аргументом и является уже склеенная строка.

Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.

Кстати, такая склейка есть только у строковых литералов. Цифры в числовых литералах обязательно должны идти подряд:

int num1 = 123; // OK
int num2 = 12 23 // ERROR
int num3 = 1'234; // if you want to logicaly devide large number


Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).

Don't break into pieces. Be whole. Stay cool.

#cppcore #cpp11
🔥14👍54🤯4❤‍🔥1