Неочевидное преимущество шаблонов
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
Как видите, здесь много
Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
void qsort( void *ptr, std::size_t count, std::size_t size, /* c-compare-pred */* comp );
extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);
Как видите, здесь много
void * указателей на void. В том числе с помощью него достигается полиморфизм в С(есть еще макросы, но не будем о них).Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
[](const void* x, const void* y)
{
const int arg1 = *static_cast<const int*>(x);
const int arg2 = *static_cast<const int*>(y);
const auto cmp = arg1 <=> arg2;
if (cmp < 0)
return -1;
if (cmp > 0)
return 1;
return 0;
}
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
50🔥35👍9❤5⚡2👎1
Рекурсивные лямбды. Невозможно?
#новичкам
Лямбды по сути - функциональные объекты. Можем ли мы вызвать лямбду внутри самой себя? То есть существуют ли рекурсивные лямбды?
Вот мы пытаемся с помощью лямбды посчитать факториал числа. В чем здесь проблема?
Фактически данный код значит, что компилятор должен сгенерировать замыкание и в поля этого замыкания поместить ссылку на само замыкание:
В этом случае нужно указать тип factorial, но он еще не известен. Он будет известен только после генерации замыкания. А при попытке сгенерировать замыкание... Ну вы уже знаете, что будет.
В общем влипли мы в то, что рекурсивные лямбды невозможны из-за рекурсии.
Однако если в таком виде мы не можем писать рекурсивные лямбды, это не значит, что ни в каком другом виде мы это делать не сможем. Об этом следующие посты.
Don't close on yourself. Stay cool.
#cppcore
#новичкам
Лямбды по сути - функциональные объекты. Можем ли мы вызвать лямбду внутри самой себя? То есть существуют ли рекурсивные лямбды?
int main() {
auto factorial = [&factorial](int n) {
return n > 1 ? n * factorial(n - 1) : 1;
};
return factorial(5);
}Вот мы пытаемся с помощью лямбды посчитать факториал числа. В чем здесь проблема?
Фактически данный код значит, что компилятор должен сгенерировать замыкание и в поля этого замыкания поместить ссылку на само замыкание:
class lkdlkhbahbahkl_danfaksdf_lamba
{
public:
int operator()(int n) const
{
return n > 1 ? n * factorial(n - 1) : 1;
}
private:
???? factorial;
};
В этом случае нужно указать тип factorial, но он еще не известен. Он будет известен только после генерации замыкания. А при попытке сгенерировать замыкание... Ну вы уже знаете, что будет.
В общем влипли мы в то, что рекурсивные лямбды невозможны из-за рекурсии.
Однако если в таком виде мы не можем писать рекурсивные лямбды, это не значит, что ни в каком другом виде мы это делать не сможем. Об этом следующие посты.
Don't close on yourself. Stay cool.
#cppcore
🔥26👍13❤5😁3
Больше нет сил
Ребят, плохие новости. У нас больше нет возможности вести канал. Денис лидит 2 команды и строит себе сам дачу, а я пилю курс на Яндекс Практикум и подрабатываю курьером в самокате. В одного уже все руки в дырках от гвоздей, у второго ноги сточились. * Если у кого есть хорошая бригада строителей в Нижнем Новгороде или остеопат - пишите в личку.
В общем, на канал времени не остается совсем. Чем дольше мы его пилим, тем больше понимаем, что не вывозим. Это нормально, это жизнь. Я вон 20 лет лет трехметровым был, но плита жизни придавила и уполовинила. А теперь я маленький и очень толстый.
Поэтому мы заканчиваем с каналом. Спасибо всем, что читали нас и давали свою обратную связь. Мы очень сильно выросли благодаря вам.
Но есть и радостные новости - тому, кто наберет больше всех сердечек на любом своем комментрии под этим постом, мы передадим права на владение каналом. Уверен, что у вам небезразлично будущее Грокаем С++, поэтому лайкайте достойных людей.
На этом все. Спасибо за этот путь, он был бесценен....
Stay alert. Stay cool.
Ребят, плохие новости. У нас больше нет возможности вести канал. Денис лидит 2 команды и строит себе сам дачу, а я пилю курс на Яндекс Практикум и подрабатываю курьером в самокате. В одного уже все руки в дырках от гвоздей, у второго ноги сточились. * Если у кого есть хорошая бригада строителей в Нижнем Новгороде или остеопат - пишите в личку.
В общем, на канал времени не остается совсем. Чем дольше мы его пилим, тем больше понимаем, что не вывозим. Это нормально, это жизнь. Я вон 20 лет лет трехметровым был, но плита жизни придавила и уполовинила. А теперь я маленький и очень толстый.
Поэтому мы заканчиваем с каналом. Спасибо всем, что читали нас и давали свою обратную связь. Мы очень сильно выросли благодаря вам.
Но есть и радостные новости - тому, кто наберет больше всех сердечек на любом своем комментрии под этим постом, мы передадим права на владение каналом. Уверен, что у вам небезразлично будущее Грокаем С++, поэтому лайкайте достойных людей.
На этом все. Спасибо за этот путь, он был бесценен....
Stay alert. Stay cool.
😭205😁95🤣33🐳16👍13😢11
Рекурсивные лямбды. Хакаем систему
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
std::function<int(int)> factorial = [&factorial](int n) -> int {
return (n) ? n * factorial(n-1) : 1;
};Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
auto factorial = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto i = factorial(7, factorial);Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
class __lambda_24_20
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(int n, type_parameter_0_0 && factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial(n - 1, factorial);
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<__lambda_24_20 &>(int n, __lambda_24_20 & factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial.operator()(n - 1, factorial);
}
#endif
};
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
auto factorial_impl = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto factorial = [&](int n) { return factorial_impl(n, factorial_impl); };
auto i = factorial(7);Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
using factorial_t = int(*)(int);
static factorial_t factorial = [](int n) {
if (n <= 1) return n;
return n * factorial(n - 1);
};
auto i = factorial(7);
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
❤38🔥17👍12
Рекурсивные лямбды. Идеал.
#опытным
Все предыдущие примеры были воркэраундами вокруг неспособности лямбды обращаться к самой себе. Какие-то из них имеют ограничения в использовании, какие-то - накладные расходы.
Но по-настоящему рекурсивные лямбды появились только в С++23 с введением deducing this.
Если лямбда - это класс с методом operator(), значит мы внутрь этого метода можем передать явный this и тогда лямбда сможет вызвать сама себя!
У нас конечно в С++20 есть шаблонные лямбды, но здесь это немножко оверкилл. Поэтому используем автоматический вывод типа с помощью auto aka дженерик лямбду.
У нас была цель, мы к ней шли и, наконец, пришли. Ура, товарищи, ура!
Однако как будто бы слишком много разговоров о сущности, которой пользовались полтора дровосека.
Да, рекурсивные лямбды - это скорее экзотика. Но и у них есть свои юзкейсы. Поговорим о них в следующем посте.
Find true yourself. Stay cool.
#cppcore #cpp23
#опытным
Все предыдущие примеры были воркэраундами вокруг неспособности лямбды обращаться к самой себе. Какие-то из них имеют ограничения в использовании, какие-то - накладные расходы.
Но по-настоящему рекурсивные лямбды появились только в С++23 с введением deducing this.
Если лямбда - это класс с методом operator(), значит мы внутрь этого метода можем передать явный this и тогда лямбда сможет вызвать сама себя!
auto factorial = [](this auto&& self, int n) {
if (n <= 1) return 1;
return n * self(n - 1);
};У нас конечно в С++20 есть шаблонные лямбды, но здесь это немножко оверкилл. Поэтому используем автоматический вывод типа с помощью auto aka дженерик лямбду.
У нас была цель, мы к ней шли и, наконец, пришли. Ура, товарищи, ура!
Однако как будто бы слишком много разговоров о сущности, которой пользовались полтора дровосека.
Да, рекурсивные лямбды - это скорее экзотика. Но и у них есть свои юзкейсы. Поговорим о них в следующем посте.
Find true yourself. Stay cool.
#cppcore #cpp23
👍26😁17🔥11❤7
Рекурсивные лямбды. Кейсы
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
Тут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
struct Leaf { };
struct Node;
using Tree = std::variant<Leaf, Node*>;
struct Node {
Tree left;
Tree right;
};
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{}
using Lambdas::operator()...;
};
int main()
{
Leaf l1;
Leaf l2;
Node nd{l1, l2};
Tree tree = &nd;
int num_leaves = std::visit(Visitor(
[](Leaf const&) { return 1; },
[](this const auto& self, Node* n) -> int {
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
auto printTuple = [](const auto& tuple) constexpr {
auto impl = []<size_t idx>(this const auto& self, const auto& t) constexpr {
if constexpr (idx < std::tuple_size_v<std::decay_t<decltype(t)>>) {
std::cout << std::get<idx>(t) << " ";
self.template operator()<idx+1>(t); // Рекурсивный вызов
}
};
impl.template operator()<0>(tuple);
};
std::tuple<int, double, std::string> tp{1, 2.0, "qwe"};
printTuple(tp);
// Output:
// 1 2 qweТут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
auto listFiles = [](const std::filesystem::path& dir) {
std::vector<std::string> files;
auto traverse = [&](this const auto& self, const auto& path) {
for (const auto& entry : std::filesystem::directory_iterator(path)) {
if (entry.is_directory()) {
self(entry.path());
} else {
files.push_back(entry.path().string());
}
}
};
traverse(dir);
return files;
};Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
👍28🔥14❤6⚡1😁1
Mutable
#новичкам
Это ключевое слово - один из самых темных уголков С++. И не то, чтобы очень важный уголок. Вы вполне ни разу могли с ним не сталкиваться. Но тем не менее по какой-то причине интервьютеры часто задают вопрос: "для чего предназначен mutable?". Ответит человек или нет особо никак не показывает его навыки программиста, лишь знание узких мест языка. Но раз такие вопросы задают, то вы должны быть готовы к ответу на них. Поэтому и родился этот пост.
Проблема вот в чем. Есть константный объект. Как вы знаете, поля константного объекта запрещено изменять. Но это довольно сильное ограничение. Да, не хотелось бы, чтобы инвариаты класса менялись. Однако помимо комплекса полей класса, представляющих собой инвариант класса, в объекте могут храниться другие поля, которые не входят в этот инвариант.
И вот мы имеем дело с тем, что нам хочется иметь семантическую константность, когда защищаем от изменения только те поля, которые должны быть неизменными в константном объекте. Но по дефолту нам дана синтаксическая константность, которая запрещает изменения любых нестатических полей.
В этом логгере мы хотим подсчитать количество логирований на протяжении времени жизни объекта. Одна мы не можем этого сделать, потому что нам запрещено изменять поля в константных методах.
Что же делать?
Вот тут как раз, mutableВалера, настало твое время.
Помечая ключевым словом mutable поле класса вы разрешаете менять его в константных методах:
Теперь мы можем изменять счетчик даже в константном методе.
В целом, на это все о функциональности этого ключевого слова.
В каких кейсах его можно применять?
✅ Сбор статистики вычислений в объекте. Пример выше как раз об этом. Для сбора статистики могут использоваться и более сложные сущности, типа оберток над известными системами мониторинга(аля prometheus).
✅ Если вы хотите потокобезопасные константные методы. Вам могут понадобиться мьютексы и/или кондвары, которые придется пометить mutable, чтобы их можно было использовать в константных методах.
✅ Кэш. Результаты предыдущих вычислений никак не влияют на инвариант класса, поэтому внутренний кэш класса можно пометить mutable, чтобы кэш можно было использовать в константных методах.
Из популярного все. Если кто знает узкий кейсы применения mutable, просим пройти в чат.
Ну все, никакой гадкий интервьюер вас не завалит. Ваше кунг-фу теперь сильнее его кунг-фу.
Surprise your enemy. Stay cool.
#cppcore #interview
#новичкам
Это ключевое слово - один из самых темных уголков С++. И не то, чтобы очень важный уголок. Вы вполне ни разу могли с ним не сталкиваться. Но тем не менее по какой-то причине интервьютеры часто задают вопрос: "для чего предназначен mutable?". Ответит человек или нет особо никак не показывает его навыки программиста, лишь знание узких мест языка. Но раз такие вопросы задают, то вы должны быть готовы к ответу на них. Поэтому и родился этот пост.
Проблема вот в чем. Есть константный объект. Как вы знаете, поля константного объекта запрещено изменять. Но это довольно сильное ограничение. Да, не хотелось бы, чтобы инвариаты класса менялись. Однако помимо комплекса полей класса, представляющих собой инвариант класса, в объекте могут храниться другие поля, которые не входят в этот инвариант.
И вот мы имеем дело с тем, что нам хочется иметь семантическую константность, когда защищаем от изменения только те поля, которые должны быть неизменными в константном объекте. Но по дефолту нам дана синтаксическая константность, которая запрещает изменения любых нестатических полей.
class ThreadSafeLogger {
std::atomic<int> call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Error! Changing class field in const member-function
// logging
}
};В этом логгере мы хотим подсчитать количество логирований на протяжении времени жизни объекта. Одна мы не можем этого сделать, потому что нам запрещено изменять поля в константных методах.
Что же делать?
Вот тут как раз, mutable
Помечая ключевым словом mutable поле класса вы разрешаете менять его в константных методах:
class ThreadSafeLogger {
mutable std::atomic<int> call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Works fine
// logging
}
};Теперь мы можем изменять счетчик даже в константном методе.
В целом, на это все о функциональности этого ключевого слова.
В каких кейсах его можно применять?
✅ Сбор статистики вычислений в объекте. Пример выше как раз об этом. Для сбора статистики могут использоваться и более сложные сущности, типа оберток над известными системами мониторинга(аля prometheus).
✅ Если вы хотите потокобезопасные константные методы. Вам могут понадобиться мьютексы и/или кондвары, которые придется пометить mutable, чтобы их можно было использовать в константных методах.
✅ Кэш. Результаты предыдущих вычислений никак не влияют на инвариант класса, поэтому внутренний кэш класса можно пометить mutable, чтобы кэш можно было использовать в константных методах.
class SomeComputingClass {
mutable std::unordered_map<Key, Result> cache;
public:
Result compute(const Key& key) const {
if (!cache.contains(key)) {
cache[key] = /* actual computing */;
}
return cache[key];
}
};Из популярного все. Если кто знает узкий кейсы применения mutable, просим пройти в чат.
Ну все, никакой гадкий интервьюер вас не завалит. Ваше кунг-фу теперь сильнее его кунг-фу.
Surprise your enemy. Stay cool.
#cppcore #interview
2❤29🔥17👍13❤🔥1
Mutable. А зачем?
#опытным
В прошлом посте мы рассказали, для чего используется ключевое слово mutable. Однако все же этот инструмент нарушает привычное поведение полей константных объектов. Да, есть семантическая и синтаксическая константность. Но вот проблема: когда у вас в арсенале есть инструмент, который позволяет обходить ограничения, то высока вероятность появления соблазна использовать этот хак не по назначению.
Поля классов в константных методах не должны меняться! Не просто так это правило придумано. В неумелых руках mutable может использоваться, как сглажевание косяков дизайна. В принципе классика: в начале пишется говнокод, потом пишется другой говнокод, чтобы исправить косяки изначального говнокода. Зато быстро задачи закрываются и KPI растет!
Чтобы предотвратить круговорот говнокода в природе, старайтесь минимизировать использование mutable. Проектируйте свои модули с умом, чтобы не приходилось их фиксить грязными хаками.
Тем более, что есть отличный способ, как вы можете заменить использование mutable.
Используйте умные указатели!
Дело в том, что на самом деле при работе с умными указателями вы меняете не сам объект указателя, а объект, на который он указывает. В этом случае вы спокойно можете проводить операции над нижележащим объектом в константном методе и при этом синтаксическая константность будет сохраняться!
Если вам нужен какой-то счетчик определенных событий? Передайте его шаренным указателем в конструктор и инкрементируйте его, сколько вам влезет в константных методах:
Единственное, что будет странно оборачивать мьютексы внутрь умного указателя. Кажется, это более страшная конструкция, чем mutable. Поэтому для мьютексов думаю можно сделать исключение.
В общем, смысл такой, что надо 100 раз подумать о целесообразности использования mutable в вашем конкретном случае. А потом все равно решить его не использовать.
Don't use dirty hacks. Stay cool.
#cppcore
#опытным
В прошлом посте мы рассказали, для чего используется ключевое слово mutable. Однако все же этот инструмент нарушает привычное поведение полей константных объектов. Да, есть семантическая и синтаксическая константность. Но вот проблема: когда у вас в арсенале есть инструмент, который позволяет обходить ограничения, то высока вероятность появления соблазна использовать этот хак не по назначению.
Поля классов в константных методах не должны меняться! Не просто так это правило придумано. В неумелых руках mutable может использоваться, как сглажевание косяков дизайна. В принципе классика: в начале пишется говнокод, потом пишется другой говнокод, чтобы исправить косяки изначального говнокода. Зато быстро задачи закрываются и KPI растет!
Чтобы предотвратить круговорот говнокода в природе, старайтесь минимизировать использование mutable. Проектируйте свои модули с умом, чтобы не приходилось их фиксить грязными хаками.
Тем более, что есть отличный способ, как вы можете заменить использование mutable.
Используйте умные указатели!
Дело в том, что на самом деле при работе с умными указателями вы меняете не сам объект указателя, а объект, на который он указывает. В этом случае вы спокойно можете проводить операции над нижележащим объектом в константном методе и при этом синтаксическая константность будет сохраняться!
Если вам нужен какой-то счетчик определенных событий? Передайте его шаренным указателем в конструктор и инкрементируйте его, сколько вам влезет в константных методах:
class ThreadSafeLogger {
explicit ThreadSafeLogger(std::shared_ptr<CallCountMetric> metric) : call_count{metric} {}
std::shared_ptr<CallCountMetric> call_count;
public:
void log(const std::string& msg) const {
call_count->Increment(); // Works fine
// logging
}
};Единственное, что будет странно оборачивать мьютексы внутрь умного указателя. Кажется, это более страшная конструкция, чем mutable. Поэтому для мьютексов думаю можно сделать исключение.
В общем, смысл такой, что надо 100 раз подумать о целесообразности использования mutable в вашем конкретном случае. А потом все равно решить его не использовать.
Don't use dirty hacks. Stay cool.
#cppcore
🔥26👍13❤6⚡1😁1
Mutable lambdas
#опытным
Лямбда выражения имеют одну интересную особенность. И эта особенность аффектит то, что можно делать внутри лямбды.
Простой пример:
Определяем 2 лямбды: в одну захватываем
В чем здесь проблема?
А в том, что во втором случае мы получим ошибку компиляции.
На самом деле operator() у замыкания по умолчанию помечен как const метод, видимо чтобы его можно было вызывать на константных объектах замыкания. То есть это значит, что мы не можем изменять поля замыкания при вызове лямбды.
Ссылки интересным образом это ограничение обходят. Так как ссылки сами по себе неизменяемы(так как по факту это обертка над константным указателем), то формально требования выполняются. А то, что мы изменяем объект, на который указывает ссылка - "вы не понимаете, это другое".
Под одним из прошлых постов разгорелась дискуссия по этому моменту. @KpacHoe_ympo в этом комменте упомянул, что в константных методах можно менять объекты, на которые ссылаются ссылки. Однако на мой вгляд(и подтверждений в стандарте я не нашел), что это не уб. Иначе в лямбду нельзя было бы захватывать ссылки вообще. Вряд ли весь захват по ссылке в лямбду держится на уб.
А вот объекты, захваченные по значению, не умеют обходить ограничения константности. В замыкании они превращаются в обычные поля класса, которые нельзя изменять внутри константных методов.
Но если нам очень нужно изменять захваченные по значению поля? На помощь приходит уже полюбившийся нам mutable. Лямбду можно пометить этим ключевым словом и тогда ее константный оператор() перестанет быть константным! Тогда мы можем как угодно изменять любые захваченные значения:
Теперь все работает отлично.
То есть в лямбда выражениях mutable используется в случаях, когда необходима модификация объектов, захваченных по значению.
Это может использоваться, например, для перемещения захваченных объектов в one-shot коллбэках:
По завершению таски, коллюэк кладет в шедулер мувнутое сообщения без накладных расходов на копирование.
Мутабельные лямбды - не такая популярная фича, еще менее известная, чем обычный mutable, но о их существовании нужно знать.
Break the rules. Stay cool.
#cppcore
#опытным
Лямбда выражения имеют одну интересную особенность. И эта особенность аффектит то, что можно делать внутри лямбды.
Простой пример:
int val = 0;
auto lambda1 = [&val]() { std::cout << ++val << std::endl; };
auto lambda2 = [val]() { std::cout << ++val << std::endl; };
Определяем 2 лямбды: в одну захватываем
val по ссылке, во второй - по значению.В чем здесь проблема?
А в том, что во втором случае мы получим ошибку компиляции.
На самом деле operator() у замыкания по умолчанию помечен как const метод, видимо чтобы его можно было вызывать на константных объектах замыкания. То есть это значит, что мы не можем изменять поля замыкания при вызове лямбды.
Ссылки интересным образом это ограничение обходят. Так как ссылки сами по себе неизменяемы(так как по факту это обертка над константным указателем), то формально требования выполняются. А то, что мы изменяем объект, на который указывает ссылка - "вы не понимаете, это другое".
Под одним из прошлых постов разгорелась дискуссия по этому моменту. @KpacHoe_ympo в этом комменте упомянул, что в константных методах можно менять объекты, на которые ссылаются ссылки. Однако на мой вгляд(и подтверждений в стандарте я не нашел), что это не уб. Иначе в лямбду нельзя было бы захватывать ссылки вообще. Вряд ли весь захват по ссылке в лямбду держится на уб.
А вот объекты, захваченные по значению, не умеют обходить ограничения константности. В замыкании они превращаются в обычные поля класса, которые нельзя изменять внутри константных методов.
Но если нам очень нужно изменять захваченные по значению поля? На помощь приходит уже полюбившийся нам mutable. Лямбду можно пометить этим ключевым словом и тогда ее константный оператор() перестанет быть константным! Тогда мы можем как угодно изменять любые захваченные значения:
int val = 0;
auto lambda2 = [val]() mutable { std::cout << ++val << std::endl; };
Теперь все работает отлично.
То есть в лямбда выражениях mutable используется в случаях, когда необходима модификация объектов, захваченных по значению.
Это может использоваться, например, для перемещения захваченных объектов в one-shot коллбэках:
auto callback = [message=get_message, &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}
SomeTask task{callback};
task.run();По завершению таски, коллюэк кладет в шедулер мувнутое сообщения без накладных расходов на копирование.
Мутабельные лямбды - не такая популярная фича, еще менее известная, чем обычный mutable, но о их существовании нужно знать.
Break the rules. Stay cool.
#cppcore
❤23👍15🔥12💯2
Обзор книжки #2
Мы тут недавно провели опрос на канале и выяснилось, что треть наших читателей считают себя новичками, отважно сражающимися с С++, но пока перевес сил не на их стороне. Возможно некоторые из вас только написали знаменитый "hello, world!".
У таких людей особый запрос на хорошие книги, которые помогут им вкатиться в С++.
В первом обзоре на "Практику многопоточного программированния" мы совсем не охватили эту аудиторию, поэтому исправляемся.
Сегодня у нас на обзоре труд Герберта Шилдта "С++ для начинающих".
Все мы знаем, что плюсы - универсальный инструмент, который позволяет писать самое большое множество возможных программ. Но для достижения этого плюсам пришлось разрастись до каких-то монструозных размеров, куда больших, чем госдолг США. Именно поэтому С++ учить сложно. Нужно очень грамотно подбирать подрядок тем, чтобы сложность наращивалась линейно, а не сваливалась на голову неподьемным грузом.
Чудесно, что книга Шилдта реализует именно такой подход. Первое издание вышло в 2002 году, немного после начала эры стандартного С++. Поэтому там просто физически речь не идет о новых стандартах, а только о самой базе С++ и его синтаксических конструкциях: система типов, операции над ними, if'ы, циклы, функции, ООП, шаблоны и исключения. Даже стандартной библиотеки почти не касаются(за исключением iostream, чтобы можно было взаимодействовать с программой).
Как можно говорить в начале книги про std::string, когда вы еще не прошли классы и динамическое выделение памяти? Как можно полноценно рассказывать про new, не пройдя ООП и исключения? Не, ну можно, так многие делают. Только при таком подходе в голове появляется много "черных ящиков", которые работают, но нет понимания как работают. Благодаря намеренному опущению упоминания стандартной библиотеки, текст книги очень последовательный.
"С++ для начинающих" написана очень легким языком. Формат повествования ориентирован на прям "зеленых" человечков. Много пошаговых инструкций с подробными пояснениями, чтобы ваша голова не вспухла от вдруг появившихся 50 строчек кода. После каждой главы даны задания, чтобы закрепить полученные знания.
Единственное, что нужно сделать скидку на год выхода первого издания и не расчитывать на то, что инструменты компиляции, указанные в книге вам помогут написать и запустить вашу первую программу. Тогда мир был другой, динозавры и мамонты еще ходили по земле. Нужно будет искать гайд по запуску с++ кода на вашей ОС во всемирной паутине.
Итого. После прочтения книжки, вы не будете знать, как писать нормальный код на С++. Но это и не было целью. Цель книги - рассказать про базовые синтаксические конструкции языка. То есть по завершении книги у вас будет полноценный фундамент, чтобы изучать уже более продвинутый С++. Постепенность обучения - залог успеха.
Хотите быть успешным в своем пути обучения кунг-фу С++? У меня для вас хорошие новости. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу его разыграть среди подписчиков и остальных любителей понюхать переплёт.
Все, что нужно сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом(обязательно) слово "Конкурс". Повторные комментарии будут удаляться. Возможность влететь в розыгрыш будет еще ровно календарную неделю после публикации этого поста. На 8 день выйдет пост с результатами.
Победителя выберем рандомайзером.
Эта книга - прекрасный подарок себе, своему ребенку-старшекласснику или даже продвинутой бабуле, которая хочет хакнуть госуслуги, чтобы накрутить голосов за проведение газа к своей даче. Возможно даже коту. Пусть хоть делом займется, а то только жиреет и спит.
Be lucky. Stay cool
Мы тут недавно провели опрос на канале и выяснилось, что треть наших читателей считают себя новичками, отважно сражающимися с С++, но пока перевес сил не на их стороне. Возможно некоторые из вас только написали знаменитый "hello, world!".
У таких людей особый запрос на хорошие книги, которые помогут им вкатиться в С++.
В первом обзоре на "Практику многопоточного программированния" мы совсем не охватили эту аудиторию, поэтому исправляемся.
Сегодня у нас на обзоре труд Герберта Шилдта "С++ для начинающих".
Все мы знаем, что плюсы - универсальный инструмент, который позволяет писать самое большое множество возможных программ. Но для достижения этого плюсам пришлось разрастись до каких-то монструозных размеров, куда больших, чем госдолг США. Именно поэтому С++ учить сложно. Нужно очень грамотно подбирать подрядок тем, чтобы сложность наращивалась линейно, а не сваливалась на голову неподьемным грузом.
Чудесно, что книга Шилдта реализует именно такой подход. Первое издание вышло в 2002 году, немного после начала эры стандартного С++. Поэтому там просто физически речь не идет о новых стандартах, а только о самой базе С++ и его синтаксических конструкциях: система типов, операции над ними, if'ы, циклы, функции, ООП, шаблоны и исключения. Даже стандартной библиотеки почти не касаются(за исключением iostream, чтобы можно было взаимодействовать с программой).
Как можно говорить в начале книги про std::string, когда вы еще не прошли классы и динамическое выделение памяти? Как можно полноценно рассказывать про new, не пройдя ООП и исключения? Не, ну можно, так многие делают. Только при таком подходе в голове появляется много "черных ящиков", которые работают, но нет понимания как работают. Благодаря намеренному опущению упоминания стандартной библиотеки, текст книги очень последовательный.
"С++ для начинающих" написана очень легким языком. Формат повествования ориентирован на прям "зеленых" человечков. Много пошаговых инструкций с подробными пояснениями, чтобы ваша голова не вспухла от вдруг появившихся 50 строчек кода. После каждой главы даны задания, чтобы закрепить полученные знания.
Единственное, что нужно сделать скидку на год выхода первого издания и не расчитывать на то, что инструменты компиляции, указанные в книге вам помогут написать и запустить вашу первую программу. Тогда мир был другой, динозавры и мамонты еще ходили по земле. Нужно будет искать гайд по запуску с++ кода на вашей ОС во всемирной паутине.
Итого. После прочтения книжки, вы не будете знать, как писать нормальный код на С++. Но это и не было целью. Цель книги - рассказать про базовые синтаксические конструкции языка. То есть по завершении книги у вас будет полноценный фундамент, чтобы изучать уже более продвинутый С++. Постепенность обучения - залог успеха.
Хотите быть успешным в своем пути обучения кунг-фу С++? У меня для вас хорошие новости. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу его разыграть среди подписчиков и остальных любителей понюхать переплёт.
Все, что нужно сделать, чтобы поучаствовать в розыгрыше - написать один раз в комментариях под этим постом(обязательно) слово "Конкурс". Повторные комментарии будут удаляться. Возможность влететь в розыгрыш будет еще ровно календарную неделю после публикации этого поста. На 8 день выйдет пост с результатами.
Победителя выберем рандомайзером.
Эта книга - прекрасный подарок себе, своему ребенку-старшекласснику или даже продвинутой бабуле, которая хочет хакнуть госуслуги, чтобы накрутить голосов за проведение газа к своей даче. Возможно даже коту. Пусть хоть делом займется, а то только жиреет и спит.
Be lucky. Stay cool
3❤22👍14🔥6😁4👎2
Удобно сравниваем объекты
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
Выглядит уже довольно сложно. А если мы захотим уточнить класс дополнительным полем секунд? Условие будет просто нечитаемым.
Однако есть элегантное решение этой проблемы. Можно использовать оператор сравнения для тупла. Он работает ровно, как мы и ожидаем в нашем случае. Сравнивает первые поля тупла, если они равны, то сравнивает вторые поля и так далее. В общем, сравнивает свои поля по [короткой схеме](https://t.iss.one/grokaemcpp/187).
Чтобы из наших полей класса получился тупл, нужно использовать функцию std::tie, которая и крафтит кортеж из переданных аргументов. Получится примерно так:
Теперь при добавлении поля класса, мы всего лишь должны добавить аргумент к std::tie:
Фишка рабочая и удобная. Так что пользуйтесь.
Use lifehacks. Stay cool.
#goodpractice
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
struct Time {
int hours;
int minutes;
bool operator<(const Time& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes))
return true;
else
return false;
}
};Выглядит уже довольно сложно. А если мы захотим уточнить класс дополнительным полем секунд? Условие будет просто нечитаемым.
Однако есть элегантное решение этой проблемы. Можно использовать оператор сравнения для тупла. Он работает ровно, как мы и ожидаем в нашем случае. Сравнивает первые поля тупла, если они равны, то сравнивает вторые поля и так далее. В общем, сравнивает свои поля по [короткой схеме](https://t.iss.one/grokaemcpp/187).
Чтобы из наших полей класса получился тупл, нужно использовать функцию std::tie, которая и крафтит кортеж из переданных аргументов. Получится примерно так:
struct Time {
int hours;
int minutes;
bool operator<(const Time& other) {
return std::tie(hours, minutes) < std::tie(other.hours, other.minutes);
}
};Теперь при добавлении поля класса, мы всего лишь должны добавить аргумент к std::tie:
struct Time {
int hours;
int minutes;
int seconds;
bool operator<(const Time& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};Фишка рабочая и удобная. Так что пользуйтесь.
Use lifehacks. Stay cool.
#goodpractice
1👍95❤17🔥12⚡5❤🔥2🥱2
std::forward_like
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
template< class T >
constexpr T&& forward(std::remove_reference_t<T>& t ) noexcept;
template< class T >
constexpr T&& forward(std::remove_reference_t<T>&& t ) noexcept;
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
return forward<decltype(obj)>(obj).member;
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
struct adapter {
std::deque<std::string> container;
auto&& operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>(self.container[i]);
} };Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
4🔥22👍7❤4😁2
Идеальная передача из лямбды
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
Ну а передача копии вообще никогда не была проблемой:
Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таковзакон стандарт.
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
auto callback = [message=get_message(), &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}Ну а передача копии вообще никогда не была проблемой:
auto callback = [message=get_message(), &scheduler]() {
// some preparetions
scheduler.submit(message);
}Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
auto callback = [message=get_message(), &scheduler](this auto &&self) {
return scheduler.submit(std::forward_like<decltype(self)>(message));
};Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таков
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
2🔥21👍8❤5😁1
Макросы
#новичкам
Один из способов избежать дублирования кода — использовать макросы. Макросы — инструкции препроцессора, которые позволяют заменять одни строки на другие:
Теперь вы можете использовать этот макрос, возвращающий максимальное из 2-х значений, для любых типов данных:
Но использование макросов — это игра в русскую рулетку. Никогда не знаешь, когда в голове появится на 2 дырки больше. Они работают на уровне текстовой подстановки, и если что-то пойдет не так, компилятор вам не поможет. Как вы думаете, какие значения будут у переменных result, x и y после выполнения следующего кода?
Мы хотим сравнить две переменные после их инкрементов. Поэтому ожидаемые значения: 11, 6, 11. Однако у препроцессора и компилятора есть свое мнение на этот счет. Реальный вывод:
Переменная y имеет значение на один больше ожидаемого. Это перестает быть удивительным после того, как мы посмотрим на то, во что раскрывается макрос:
Мы просто подставили текст и в любых значениях x и y, одна из этих переменных претерпит лишний инкремент.
Более сложные ситуации генерируют все менее и менее тривиальные ошибки.
Недостатки макросов
🔞 Нетипобезопасность: Макросы не проверяют типы данных. Программа может скомпилироваться, но упасть в runtime.
🔞 Побочные эффекты: Макросы могут привести к неожиданным результатам, особенно если аргументы содержат побочные эффекты (например, инкременты).
🔞 Сложность отладки: Макросы могут раскрываться в причудливые строки, которые вы просто не увидите в своем коде. Придется отлаживаться по файлу с кодом после препроцессора, а это нетривиальная задача.
Макросы — это как использовать переводчик текста по изображению. В идеальных случаях работает хорошо, но иногда может "кабачок" интерпретировать как "маленький дешевый ресторан".
Поэтому CppCoreGuideLines говорят нам не использовать макросы при определении функций.
Вместо макросов в С++ есть шаблоны и вычисления времени компиляции, возможности которых с каждым стандартом все возрастают.
Don't be confusing. Stay cool.
#cppcore
#новичкам
Один из способов избежать дублирования кода — использовать макросы. Макросы — инструкции препроцессора, которые позволяют заменять одни строки на другие:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Теперь вы можете использовать этот макрос, возвращающий максимальное из 2-х значений, для любых типов данных:
int result1 = MAX(1, 2); // Работает
double result2 = MAX(1.5, 2.5); // Тоже работает
Но использование макросов — это игра в русскую рулетку. Никогда не знаешь, когда в голове появится на 2 дырки больше. Они работают на уровне текстовой подстановки, и если что-то пойдет не так, компилятор вам не поможет. Как вы думаете, какие значения будут у переменных result, x и y после выполнения следующего кода?
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = 10;
int result = MAX(++x, ++y);
Мы хотим сравнить две переменные после их инкрементов. Поэтому ожидаемые значения: 11, 6, 11. Однако у препроцессора и компилятора есть свое мнение на этот счет. Реальный вывод:
Result: 12
x: 6
y: 12
Переменная y имеет значение на один больше ожидаемого. Это перестает быть удивительным после того, как мы посмотрим на то, во что раскрывается макрос:
int result = ++x > ++y ? ++x : ++y;
Мы просто подставили текст и в любых значениях x и y, одна из этих переменных претерпит лишний инкремент.
Более сложные ситуации генерируют все менее и менее тривиальные ошибки.
Недостатки макросов
🔞 Нетипобезопасность: Макросы не проверяют типы данных. Программа может скомпилироваться, но упасть в runtime.
🔞 Побочные эффекты: Макросы могут привести к неожиданным результатам, особенно если аргументы содержат побочные эффекты (например, инкременты).
🔞 Сложность отладки: Макросы могут раскрываться в причудливые строки, которые вы просто не увидите в своем коде. Придется отлаживаться по файлу с кодом после препроцессора, а это нетривиальная задача.
Макросы — это как использовать переводчик текста по изображению. В идеальных случаях работает хорошо, но иногда может "кабачок" интерпретировать как "маленький дешевый ресторан".
Поэтому CppCoreGuideLines говорят нам не использовать макросы при определении функций.
Вместо макросов в С++ есть шаблоны и вычисления времени компиляции, возможности которых с каждым стандартом все возрастают.
Don't be confusing. Stay cool.
#cppcore
1👍33😁11❤4🔥3❤🔥1
Итоги конкурса
Мы долго ждали и, наконец, дождались. Вчера мы честно взяли генератор случайных чисел и нашли победителя и будущего счастливого обладателя книжки "С++ для начинающих" Герберта Шилдта. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете нашей непредвзятости)
Перед оглашением результатов хотим сказать спасибо всем участникам розыгрыша и людям, оставивших свое мнение о книге. Благодаря этому начинающие свой путь в плюсах подписчики смогут составить более объективную картину о контенте книги. Спасибо всем, что поддерживаете инициативу!
Ну а победителем стал Антон Конев давайте похлопаем ему👏👏👏. Антон, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.
Будем работать, чтобы таких розыгрышей было больше. Поэтому мягко напоминаю, что у Питера самый качественный перевод зарубежных книг. А там сами все найдете.
Be lucky. Stay cool.
Мы долго ждали и, наконец, дождались. Вчера мы честно взяли генератор случайных чисел и нашли победителя и будущего счастливого обладателя книжки "С++ для начинающих" Герберта Шилдта. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете нашей непредвзятости)
Перед оглашением результатов хотим сказать спасибо всем участникам розыгрыша и людям, оставивших свое мнение о книге. Благодаря этому начинающие свой путь в плюсах подписчики смогут составить более объективную картину о контенте книги. Спасибо всем, что поддерживаете инициативу!
Ну а победителем стал Антон Конев давайте похлопаем ему👏👏👏. Антон, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.
Будем работать, чтобы таких розыгрышей было больше. Поэтому мягко напоминаю, что у Питера самый качественный перевод зарубежных книг. А там сами все найдете.
Be lucky. Stay cool.
🎉28👍9👏9❤6🗿1
Сравниваем производительности оператора<
#опытным
В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .
Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.
Сравним 4 реализации operator<:
Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.
Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:
Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.
В конце мы запускаем бенчмарк над функцией с измерением времени выполнения, говорим ему, что мы сами будет мерять время(UseManualTime), и сколько итераций цикла нужно выполнить(Iterations(20)).
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Compare things. Stay cool.
#performance #cpp20
#опытным
В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .
Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.
Сравним 4 реализации operator<:
struct Time_comparison_unreadable {
int hours;
int minutes;
int seconds;
bool operator<(const Time_comparison_unreadable& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes) || (hours == other.hours && minutes == other.minutes && seconds < other.seconds))
return true;
else
return false;
}
};
struct Time_comparison_readable {
// fields
bool operator<(const Time_comparison_readable& other) {
if (hours < other.hours) return true;
if (hours > other.hours) return false;
if (minutes < other.minutes) return true;
if (minutes > other.minutes) return false;
if (seconds < other.seconds) return true;
return false;
}
};
struct Time_tie {
// fields
bool operator<(const Time_tie& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};
struct Time_spaceship {
// fields
auto operator<=>(const Time_spaceship &) const = default;
};Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.
Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(1,100);
template <typename TimeClass>
static void time_comparison_experiment(benchmark::State& state) {
std::vector<TimeClass> v(1'000'000);
std::generate(v.begin(), v.end(), [&] () -> TimeClass { return TimeClass{ dist(rng) % 24, dist(rng) % 60, dist(rng) % 60 }; });
while (state.KeepRunning()) {
auto start = std::chrono::high_resolution_clock::now();
std::sort(v.begin(), v.end());
auto end = std::chrono::high_resolution_clock::now();
auto elapsed_seconds =
std::chrono::duration_cast<std::chrono::duration<double>>(
end - start);
state.SetIterationTime(elapsed_seconds.count());
std::shuffle(v.begin(), v.end(), rng);
}
}
BENCHMARK(time_comparison_experiment<Time_comparison_unreadable>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_comparison_readable>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_tie>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_spaceship>)->UseManualTime()->Iterations(20);
Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.
В конце мы запускаем бенчмарк над функцией с измерением времени выполнения, говорим ему, что мы сами будет мерять время(UseManualTime), и сколько итераций цикла нужно выполнить(Iterations(20)).
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Compare things. Stay cool.
#performance #cpp20
Telegram
Грокаем C++
Удобно сравниваем объекты
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
struct Time {
int…
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
struct Time {
int…
👍13❤7🔥6⚡1
Виртуальные функции в compile-time
#опытным
Виртуальные функции являются средством реализации динамического полиморфизма в С++. Почему он вообще называется динамическим?
Да потому что выбор конкретной реализации происходит в рантайме, а не во время компиляции.
Но что, если я вам скажу, что мы можем реализовывать полиморфизм времени компиляции с помощью виртуальных функций?
Выглядит как оксюморон, но подождите кидаться грязными тряпками. Сейчас все разберем.
Начиная с С++11 у нас есть constexpr функции. Эти функции могут быть вычислены на этапе компиляции, если их аргументы также известны на этом этапе. Аргументы могут быть константами, литералами, constexpr переменными или результатом вычисления других constexpr функций.
В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.
Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.
Начиная с С++20 constexpr функции могут быть виртуальными!
Все как мы привыкли: делаем иерархию классов с виртуальной функцией, только везде на всех этапах приписываем constexpr. И это работает!
А где это может быть использовано, посмотрим в следующий раз.
Increase your usability. Stay cool.
#cpp11 #cpp20 #cppcore
#опытным
Виртуальные функции являются средством реализации динамического полиморфизма в С++. Почему он вообще называется динамическим?
Да потому что выбор конкретной реализации происходит в рантайме, а не во время компиляции.
Но что, если я вам скажу, что мы можем реализовывать полиморфизм времени компиляции с помощью виртуальных функций?
Выглядит как оксюморон, но подождите кидаться грязными тряпками. Сейчас все разберем.
Начиная с С++11 у нас есть constexpr функции. Эти функции могут быть вычислены на этапе компиляции, если их аргументы также известны на этом этапе. Аргументы могут быть константами, литералами, constexpr переменными или результатом вычисления других constexpr функций.
constexpr int double_me(int n)
{
return n * 2;
}
// условие верное и мы не падаем
static_assert(double_me(4) == 8);
// условие ложно и компиляция прервется на этой строчке
static_assert(double_me(4) == 7);
В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.
Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.
Начиная с С++20 constexpr функции могут быть виртуальными!
struct VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const = 0;
};
struct Impl: VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const override
{
return 2 * n;
}
};
constexpr auto impl = Impl{};
// для полиморфизма с виртуальными функциями нужна ссылка
constexpr const VeryComplicatedCaclulation& impl_ref = impl;
constexpr auto a = impl_ref.double_me(4);
static_assert(a == 8); // true
Все как мы привыкли: делаем иерархию классов с виртуальной функцией, только везде на всех этапах приписываем constexpr. И это работает!
А где это может быть использовано, посмотрим в следующий раз.
Increase your usability. Stay cool.
#cpp11 #cpp20 #cppcore
11❤26🔥12👍9🤯4🤔2🐳1
Виртуальные функции в compile-time Ч2
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
class error_code
{
private:
int val_;
const error_category* cat_;
public:
constexpr error_code() noexcept;
constexpr error_code(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code(ErrorCodeEnum e) noexcept;
constexpr void assign(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code& operator=(ErrorCodeEnum e) noexcept;
constexpr void clear() noexcept;
constexpr int value() const noexcept;
constexpr const error_category& category() const noexcept;
constexpr explicit operator bool() const noexcept;
error_condition default_error_condition() const noexcept;
string message() const;
};
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
error_category*, наследникам которого мы и можем делигировать принятие решения о том, является ли значение ошибкой или нет.class error_category
{
public:
// ...
virtual bool failed(int ev) const noexcept;
// ...
};
// И добавляем метод в класс error_code
class error_code
{
// ...
bool failed() const noexcept { return cat_->failed(val_); }
// ...
};
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
❤18👍13🔥10⚡1
Еще одно отличие С и С++
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
В плюсах такое можно сделать только с помощью нескольких инструкций.
Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
int array[100] = {[13] = 1, [45] = 1, [79] = 1};В плюсах такое можно сделать только с помощью нескольких инструкций.
int array[100] = {};
array[13] = array[45] = array[79] = 1;Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
int array[100] = {[13] = 1, [30 ... 40] = 1, [45] = 1, [79] = 1};Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
struct point { int x, y, z; };
struct point p1 = { .y = 2, .x = 3 };
struct point p2 = { y: 2, x: 3 };
struct point p3 = { x: 1};Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
1🔥29👍10❤6❤🔥1
Designated initialization
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
Хоть
Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
Несмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
struct Person {
std::string name;
std::string surname;
std::string id;
};
struct Item {
std::string name;
double price;
std::string id;
};
struct Order {
Person person;
Item purchase;
std::string pick_up_address;
};Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
Order order{.person = {.name = "Golum",
.surname = "Iz shira",
.id = "666"},
.purchase = {.name = "Precious",
.price = 9999999.9,
.id = "13"},
.pick_up_address = "Mordor"};То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
struct Person
{
std::string name;
std::string surname;
unsigned age;
};
struct Employee : Person
{
unsigned salary;
};
Employee e1{ { .name{"John"}, .surname{"Wick"}, .age{40} }, 50000 };
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
struct Point{
int x, y, z;
};
Point p{.x = 2, .z = 3}; // y is not mentioned, but it will have value of 0Хоть
y строит в середине, но это не мешает нам не указывать его при создании класса и это поле гарантированно будет равно 0.Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
struct Point{
int x, y;
};
Point p{.y = 2, .x = 3}; // not valid in C++!Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
struct Point{
int x, y;
};
Point p{2, .y = 3}; // Not allowedНесмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
2🔥34👍15❤10