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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
А вам когда-нибудь снился С++ в кошмарах?)
😁79🔥129😱1
Ревью

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

Для новичков, кто еще не видел эту рубрику. В рубрике #ревью мы выкладываем сюда отрывок кода, а вы в комментах по существу говорите, что в этом коде не так и как бы вы это исправили. Как бы вы это делали на реальном ревью в своем проекте. Того, кто найдет больше всего недостатков, я завтра упомяну в итоговом посте-компиляции из всех найденных проблем.

Вот собственно код:

struct Task {
void Execute() {
// pretend that this is very long calculations
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};

void WorkingThread(std::deque<Task> queue) {
std::mutex mtx;
mtx.lock();
while (!queue.empty()) {
auto elem = queue.front();
queue.pop_front();
elem.Execute();
lck.unlock();
// to get other threads opportunity to work
std::this_thread::sleep_for(std::chrono::milliseconds(1));
lck.lock();
}
}

int main() {
std::deque<Task> queue(10);
std::thread thr1{WorkingThread, queue}, thr2{WorkingThread, queue};
}


Чего ждем? Айда в комменты поливать г ревьюить код.

Ask for objective critique. Stay cool.
🔥20👍6❤‍🔥44
​​Результаты ревью

Всем участникам спасибо за развернутые ответы. Самый объемный комментарий с большим количеством правильных предположений относительно кода написал Alex D. Давайте ему все вместе похлопаем👏👏👏.

Также удивило, что некоторые предоставили прям свои исправленные версии кода. Спасибо большое за вовлеченность😌. Ваши примеры помогают новичкам по-другому посмотреть на задачу и увидеть альтернативные и более корректные версии ее решения.

Но мы не будем уж погружаться совсем далеко в дебри и не будет совсем переписывать кусок, чтобы в нем появлялись атомики, тредпулы, кондвары и бечконечный разбор очереди до graceful shutdown. Уже в одном из прошлых ревью разбирали, как может выглядеть более совершенная версия воркера для очереди. Все ваши комменты и примеры кода с более продвинутой обработкой в любом случае имеют смысл. Просто этот пример довольно учебный и планировал показывать вполне конкретные вещи, не претендуя на что-то сложное.

Но и на этом поле есть, где развернуться.

Итак, пойдем по в рандомном порядке:

💥 Очередь копируется в воркер потока, поэтому мы обрабатываем не просто копию очереди, а копии!. Каждый поток имеет свою копию. Передача по значению не была бы прям уж сильной проблемой, если бы был один воркер. А их тут 2. И вся работа будет делаться дважды. Это конечно не порядок. Меняем параметр функции на ссылку.

💥 Но если вы думаете, что вы так передадите потоку ссылку - вы ошибаетесь! На самом деле для всех объектов-аргументов потоковой функции во внутреннем сторадже нового потока создается их копия, чтобы не иметь дело с висячими ссылками. Если вы хотите передать истинную ссылку в поток, то надо воспользоваться std::ref.

💥 Нет join'ов у потоков. Надо их либо вызвать, либо использовать jthread из С++20.

💥 Одна из самых больших непоняток в коде для комментаторов - что за переменная lck? На самом деле автор кода немного с ним экспериментировал и просто допустил ошибку, не переименовав lck в mtx. Такое бывает, особенно с новичками. Особенно в проектах без CI.

💥 Если вы используете мьютекс, это не значит, что все автоматически становится потокобезопасным. В этом примере мьютекс никак не защищает критическую секцию! А как он может защитить, если он является локальной переменной. То есть каждый поток будет иметь свою копию этого мьютекса. Не порядок. Его надо сделать либо глобальным вообще, либо статической локальной переменной.

💥 Использование чистых вызовов lock и unlock на мьютексах является очень плохой практикой. В случае изначально пустой очереди или такого распределения расписания потоков, что очередь окажется пуста до входа в while, один из потоков никогда не отпустит лок.
Да и вот это засыпание после execute выглядит очень криво.
Нужно использовать RAII обертки, типа std::lock_guard.

💥 Но в нашем случае lock_guard будет плохим решением. Во-первых, не очень понятно, куда его вставлять. Изначальная проверка на пустоту тоже должна быть "обезопашена". Тогда надо ставить до while. Но в этом случае один из потоков будет выполнять всю работу целиком. А нам хотелось бы использовать преимущества многопоточки.

И еще один нюанс: у нас под локом выполняется довольно тяжелая функция, которая долго выполняется. И все это время другие потоки будут простаивать без дела. А если у нас со временем их станет больше? Еще больше простоя. А время - это деньги.

В общем, нехватает гибкости. Хочется иметь возможность отпускать замок на время, а потом опять его захватывать. Но и преимуществами RAII тоже хочется пользоваться.
Выход - std::unique_lock. Он позволяет делать и то, и то.

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

💥 Неплохо бы обернуть выполнение задачи в блок try-catch. Так если вылетит исключения из выполнения одной задачи, мы можем как-то обработать эту ситуацию и пойти работать дальше, а не просто свалиться в std::terminate.
Пусть этот пример останется "учебным", но теперь хотя бы корректным.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Fix your flaws. Stay cool.
👍21❤‍🔥9👏54🔥3🤔1
​​Инкапсуляция и структуры
#новичкам

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

Однако, как и со многими такими догматами случается, не всегда нужно следовать этой концепции. Давайте посмотрим на следующий код:

class MyClass
{
    int m_foo;
    int m_bar;
public:
    int addAll() const;
    int getFoo() const;
    void setFoo(int foo);
    int getBar() const;
    void setBar(int bar);
};
int MyClass::addAll() const
{
    return m_foo + m_bar;
}

int MyClass::getFoo() const
{
    return m_foo;
}

void MyClass::setFoo(int foo)
{
    m_foo = foo;
}

int MyClass::getBar() const
{
    return m_bar;
}

void MyClass::setBar(int bar)
{
    m_bar = bar;
}


Выглядит солидно. Сеттеры, геттеры, все дела. Но вот души в этом коде нет. Он какой-то.. Бесполезный чтоли.

Методы класса не делают ничего такого, что пользователь класса не смог бы сделать своими руками. Скорее руками это все сделать будет даже короче.

Этим грешат новички: поначитаются модных концепций(или не очень модных) и давай скрывать все члены.

Brah... Don't do this...

Здесь нет никаких инвариантов, которые нужно было бы сохранять. Интерфейс класса и так позволяет все, шо хош делать с объектом.

Если ваш класс используется просто как хранилище данных, то это ближе не к классу, а к сишной структуре. Сделайте вы уже поля открытыми и замените ключевое слово class на более подходящее struct.

struct MyClass
{
    int m_foo;
    int m_bar;
}


Делов-то. Зато прошлым примером можно хорошо отчитываться за количество написанных строчек кода=)

Show your inner world to others. Stay cool.

#cppcore #goodpractice #OOP
29👍23🔥7💯3😁1
Return в main

На собеседовании тут встретил сеньора-помидора, который говорил, что код не скомпилируется, если не написать return 0 в конце. Думал, что все уже знают эту особенность, но раз нет - придется рассказать. Правда это довольно коротко будет.

В С++ разрешено не указывать возвращаемое значение для функции main. Но только для нее! Это единственное исключение.

Однако процессу требуется какой-то код возврата по завершению работы, это требование ОС. Поэтому, если исполнение дошло до конца main и ничего плохого не произошло - подразумевается, что возвращается 0. Компилятор также добавляет соответствующую инструкцию в асм.

На счет сишки точно не знаю, но вроде в С99 можно было опускать инструкцию возврата.

#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
}

Поэтому этот код абсолютно валидный и не противоречит стандарту.

Так что все в порядке, город может спать спокойно без return.

Sleep well. Stay cool.

#cppcore
34😁17👍12🔥2🗿1
​​Поля_класса
#опытным

Очень часто в компаниях прибегают к различного рода код-стайлам для обозначения полей класса и выделения из на фоне серой массы локальных переменных.

Это очень удобно, когда читаешь код класса и сразу понимаешь, что вот это локальная переменная, а вот это поле класса. Читаемость сразу повышается.

Удобно еще бывает различать открытые и закрытые поля с помощью наличия или отсутствия подчеркивания.

Чисто идейно мне нравится такой стайл: открытые члены без подчеркиваний, защищенные с одним подчеркиванием, приватные - с двумя. Спереди или сзада - тут уже вкусовщина.

Но вот беда. У нас проблемы, Хьюстон.

В С++ есть определенные навязанные ограничения на нейминг сущностей со стороны underscore. Стандарт говорит:

Certain sets of names and function signatures 
are always reserved to the implementation:

- Each name that contains a double underscore (__) or
begins with an underscore followed by an uppercase letter
is reserved to the implementation for any use.
- Each name that begins with an underscore is reserved
to the implementation for use as a name in the global namespace.

Such names are also reserved in namespace ::std (17.4.3.1).


Имплементация загробастала себе права на имена сущностей, которые содержат двойное подчеркивание или начинаются с одного подчеркивания и буквы верхнего регистра, в любых скоупах. А также для глобального неймспейса зарезервированы имена начинающиеся на одинарный underscore.

Итого, чтобы не было коллизии имен, лучше избегать использования имен с префиксом подчеркивания или содержащих двойной underscore.

Закрытые поля можно помечать одной черточкой с обеих сторон. Только если решите использовать спереди - не пишите дальше заглавную букву. И будет вам счастье.

В питоне кстати именно через нижнее подчеркивание помечаются "непубличные" части API. Но там правда есть один нюанс. Зацените веселый мем по этой теме. Так что сегодня мем про питухон.

Be happy. Stay cool.

#cppcore
😁2110👍7❤‍🔥1
​​CamelCase vs Under_Score

Вдохновился прошлым постом и родилось это.

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

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

В настоящее время существует много стандартов наименования переменных, но два из них являются наиболее популярными среди программистов: это camel case («Верблюжья» нотация) и underscore (именование переменных с использованием символа нижнего подчеркивания в качестве разделителя).

Верблюжья нотация является стандартом в языке Java и в его неродственнике JavaScript, хотя ее можно встретить также и в других местах. Согласно этому стандарту, все слова в названии начинаются с прописной буквы, кроме первого. При этом, естественно, не используется никаких разделителей вроде нижнего подчеркивания. Пример: яШоколадныйЗаяцЯЛасковыйМерзавец. Обычно данный стандарт применяют к именам функций и переменных, при этом в именах классов, структур, интерфейсов используется стандарт UpperCamelCase(первая буква заглавная).

В стандарте underscore слова пишутся с маленькой буквы, а между ними стоит _ ( типа такого: йоу_собаки_я_наруто_узумаки). Обычно этот стандарт используется в названиях функций и переменных, а для названий классов, структур, интерфейсов используется стандарт UpperCamelCase. Обычно используется с родственных С языках.

Каждый из этих двух стандартов имеет свои сильные и слабые стороны. Вот основные:

👉🏿 Нижнее подчеркивание лучше читается: сравните стандарт_с_нижним_подчеркиванием и стандартНаписанияПрописнымиБуквами

👉🏿 Зато camel case делает более легким чтение строк, например:
my_first_var=my_second_var-my_third_var
и
myFirstVar=mySecondVar-myThirdVar

Очевидно, что camel case читается лучше: в случае с нижним подчеркиванием и оператором «минус» выражение с первого взгляда вообще можно принять за одну переменную.

👉🏿 Подчеркивание сложнее набирать. Даже при наличии intellisense, во многих случаях необходимо набирать символ нижнего подчеркивания. И имена получаются длиннее.

👉🏿 Camel Case непоследователен, потому что при использовании констант (которые иногда пишутся целиком заглавными буквами) нам приходится использовать нижнее подчеркивание. С другой стороны, стандарт underscore может быть полным, если вы решите использовать в названиях классов (структур, интерфейсов) нижнее подчеркивание в качестве разделителя.

👉🏿 Кроме того, для камел кейса не так уж и просто работать с аббревиатурами, которые обычно представлены в виде заглавных букв. Например, как вот правильно iLoveBDSM или iLoveBdsm?. Непонятно. Можете написать в комментах, как это по-вашему пишется)

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

Расскажите в комментах, какую нотацию вы используете? Интересно иметь большую репрезентативную статистику.

Choose your style. Stay cool.

#fun
😁22🔥115👍3❤‍🔥1🤣1
​​Перегружаем оператор взятия адреса
#опытным

Перегрузка операторов - дело не для слабонервных. Этим пугают детей и пытают людей. Только прожженные плюсовики могут сходу сказать разницу в перегрузке префиксного и постфиксного декремента/инкремента.

Но не только в этом дело. Есть такие операторы, которые как бы можно перегружать, но непонятно, для чего это делать.

Вот например, оператор взятия адреса. Да его можно перегружать. Можно его перегружать как метод:

struct Class {
int* operator &() {
return &member;
}

int member;
};


Можно, как свободную функцию:

struct A {
int member;
};

int* operator &(Class& obj) {
return &obj.member;
}


Но странновато это все.

Есть объект. У него есть занимаемое место. Зачем вообще заменять такое интуитивно понятное поведение?

Решение сделать этот оператор кастомным очень спорное. В реальном коде вы такого скорее всего никогда не встретите. А если подумаете, что неплохо было бы в своем коде заюзать эту фичу, то подумайте еще раз 300.

Но все-таки если оно есть, значит кто-то этим пользуется.

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

Однако тогда встает вопрос, как нормально взять адрес у обертки, если это пригодиться?

Но и на этот вопрос есть ответ. Но обсудим мы его несколько позже.

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

Follow the beaten path. Stay cool.

#cppcore
🔥23👍9❤‍🔥55😁2👎1
Addressof
#опытным

Говорят вот, что питон - такой легкий для входа в него язык. Его код можно читать, как английские английский текст. А вот С/С++ хаят за его несколько отталкивающую внешность. Чего только указатели стоят...

Кстати о них. Все мы знаем, как получить адрес объекта:

int number = 42;
int * p_num = &number;


Человек, ни разу не видевший код на плюсах, увидит здесь какие-то магические символы. Вроде число, а вроде какие-то руны * и &. Но плюсы тоже могут в читаемость! Причем именно в аспекте адресов.

Вместо непонятного новичкам амперсанда есть функция std::addressof! Она шаблонная, позволяет получить реальный адрес непосредственно самого объекта и доступна с С++11. Для нее кстати удалена перегрузка с const T&&

template< class T >
T* addressof( T& arg ) noexcept;

template< class T >
const T* addressof( const T&& ) = delete;


Это делает функцию еще одним примером использования константной правой ссылки .

Это конечно круто, что можно в плюсах словами брать адрес, но в чем прикол? Зачем было заводить отдельную функцию для того, что уже есть в самом языке?

А вот теперь мы возвращаемся к предыдущему посту про перегрузку оператора взятия адреса. Так как его можно перегружать, то мы можем возвращать вообще любой адрес, который потенциально никак не связан с самим объектом. В этом случае не очень понятно, как взять трушный адрес объекта. Как раз таки std::addressof - способ получить валидный адрес непосредственно самого объекта.

Также большим преимуществом является шаблонная природа функции. Это позволяет обобщенному коду работать, как с обычными классами, так и с классами, у которых перегружен оператор взятия адреса.

А с С++17 она еще и констэкспр, это для любителей компайл-тайма.

Вот вам примерчик:

template<class T>
struct Ptr
{
T* pad; // add pad to show difference between 'this' and 'data'
T* data;
Ptr(T* arg) : pad(nullptr), data(arg)
{
std::cout << "Ctor this = " << this << '\n';
}
 
~Ptr() { delete data; }
T** operator&() { return &data; }
};
 
template<class T>
void f(Ptr<T>* p)
{
std::cout << "Ptr overload called with p = " << p << '\n';
}
 
void f(int** p)
{
std::cout << "int** overload called with p = " << p << '\n';
}
 
int main()
{
Ptr<int> p(new int(42));
f(&p); // calls int** overload
f(std::addressof(p)); // calls Ptr<int>* overload, (= this)
}

// OUTPUT
// Ctor this = 0x7fff59ae6e88
// int** overload called with p = 0x7fff59ae6e90
// Ptr overload called with p = 0x7fff59ae6e88


Здесь какие-то злые персоналии перегрузили оператор взятия адреса у класса Ptr так, чтобы он возвращал указатель на одно из его полей. Ну и потом сравнивают результат работы оператора с результатом выполнения функции std::addressof.

Видно, что трушный адрес объекта, полученный с помощью this и адрес, возвращенный из std::addressof полностью совпадают. А перегруженный оператор возвращает другое значение.

Express your thoughts clearly. Stay cool.

#cpp #cpp11 #cpp17
🔥2716👍8😁7❤‍🔥3😱2
Проверяем вхождение элемента в ассоциативный контейнер

Нужно вот нам по ключу проверить вхождение элемента допустим в мапу.

Обычно мы пишем:

if (map.count(key)) {
// do something
}


Но для контейнеров без приставки "multi" это выглядит довольно странно. Действительно, если я знаю, что в мапе однозначное соответствие ключа и значения, зачем мне знать сколько вхождений элементов с этим ключом? Я хочу просто знать, есть ли он.

Такие вот маленькие семантические несостыковочки. С ними вроде все смирились, но осадочек остался...

И 20-е плюсы наконец нам подарили замечательный публичный метод для всех ассоциативных контейнеров contains. Он проверяет, если ли в контейнере элементы с данным ключом. Теперь код выглядит так:

if (map.contains(key)) {
// do something
}


И вот уже стало чуть приятнее и понятнее читать код.

Если есть доступ к 20-м плюсам, то переходите на использование этого метода.

Make things clearer. Stay cool.

#STL #cpp20
❤‍🔥26👍153🔥3🆒2🤪1
​​Double lookup
#опытным

Решил сделать небольшое дополнение к предыдущему посту по результатам дискуссии в комментариях.

Не нужно использовать методы count(key) и contains(key), если вы потом собираетесь работать с этим ключом в ассоциативном контейнере(например изменять объект по ключу). Это может привести к так называемому double lookup. То есть двойной поиск по контейнеру.

Возьмем для примера std::map для показательности. И вот такой код:

std::map<std::string, std::string> map;

std::string get_value(const std::string& key) {
if (!map.contains(key)) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return map[key];
}
}


Здесь мы по ключу key выдаем какое-то значение value. Эти значения вычисляются при помощи долгой функции longCalculations, поэтому было решено закэшировать все уже вычисленные значения в мапе. Так мы сможем обойти долгие вычисления и быстро дать ответ в случае, если уже искали это значение.

Только вот в чем проблема. Поиск по мапе - логарифмическая по времени операция. И в этом примере мы всегда делаем 2 поиска: первый раз на contains(мы должны пройтись по контейнеру, чтобы понять, есть ли элемент) и второй раз на operator[](нужно пройтись по контейнеру, чтобы вставить в него элемент/получить к нему доступ). Долговато как-то. Может можно получше?

В случае, если ключ есть в мапе, то мы можем делать всего 1 поиск! С помощью метода find и итератора на элемент.

std::string get_value(const std::string& key) {
auto it = map.find(key);
if (it == map.end()) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return it->second;
}
}


Мы в начале попытались найти конкретный элемент мапы по ключу. И если его нет, то нам все-таки придется выполнить второй поиск, чтобы найти подходящее место для элемента. Но вот если ключ есть, тогда мы можем использовать сам итератор для возврата значения и второго поиска не будет!


Методы count и contains нужно использовать только тогда, когда у вас нет надобности в получении доступа к элементам контейнера. Find в этом случае, по моему мнению, немного синтаксически избыточен. А вот говорящие методы - самая тема. Например, у вас есть множество, в котором хранятся какие-то поля джейсона. И вам нужно как-то трансформировать только те значения, ключи которых находятся во множестве.

std::set<std::string> tokens;
std::string json_token;
Json json;
if (tokens.contains(json_token)) {
transformJson(json, json_token);
}


Все прекрасно читается и никакого двойного поиска!

Don't do the same work twice. Stay cool.

#cppcore #goodpractice
👍2311❤‍🔥3🔥3👎1
​​const this

Все мы знаем, что this - указатель на объект, на котором сейчас находится контекст исполнения. Будь то метод, default member initializer или лямбда выражение. И вот есть в этих ваших интернетах такое мнение, что этот this - нулевой неявный аргумент метода и он передается в него в таком виде:

void Foo::bar(Foo * const this, Type1 param1, Type2 param2) {}


То есть типа this - константный указатель.

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

Да ему нельзя ничего присваивать. Например

void Foo::change(Foo *foo) { this = foo; }


При компиляции кода появится примерно такая ошибка: lvalue required as left operand of assignment. Но если приглядеться, то ни про какой const там речь не идет. Ему не хватает lvalue слева.

Все потому что this - prvalue выражение. То есть можно даже сказать, что это и не указатель. Это выражение, значение которого равно адресу объекта. Оно не может быть использовано слева от знака равенства и у него нельзя взять адрес.

И именно поэтому ему нельзя ничего присваивать, а не потому что это константный указатель.

Но вот cv-квалификация метода может повлиять на cv-квалификацию указателя на объект. Тип this в обычном методе - Foo *(указатель на Foo). Однако для cv-квалифицированных методов this становится cv Foo *. То есть:

void Foo::ConstMemberFunction(Type1 param1, Type2 param2) const {
// this - const Foo *
this->field = param1; // Error!
}


Сделано это, естественно, чтобы мы никак не могли изменить объект, на который указывает this, в константном методе.

Так что this - prvalue и точка!

Make points in your life. Stay cool.

#cppcore
❤‍🔥22👍179🔥4😁2
​​Почему приватные методы находятся в описании класса?
#опытным

Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать?

Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.

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

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

А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.

В чем прикол такого дизайн решения?

Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.

А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.

Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.

Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
👍21🔥74❤‍🔥2
​​std::apply
#опытным

Метапрогеры очень любят работать с компайл-тайм структурами, типа std::array, std::pair и std::tuple. Когда работают с такими структурами, то интересны прежде всего элементы этих структур. И очень хочется иногда как-то единообразно передавать их распакованные элементы куда-то в другую функцию.

Именно этим и занимается std::apply, которая появилась в С++17. По факту, эта такое дженерик решение для того, чтобы вызвать какую-то функцию с аргументами из элементов tuple-like объектов.

Простейшее, что можно с ней делать - вывести на экран все элементы тапла.

const std::tuple<int, char> tuple = std::make_tuple(5, 'a');
std::apply([](const auto&... elem)
{
((std::cout << elem << ' '), ..., (std::cout << std::endl));
}, tuple);


Здесь мы применяем fold-expression и оператор-запятая. Можете освежить знания в этом посте.

Можно придумать чуть более сложную задачу. Надо написать функцию, которая принимает неограниченное число параметров, в том числе и tuple-like объекты. Все параметры надо распаковать в строку, а tuple-like объекты выделить с помощью фигурных скобок. Объекты естественно могут быть вложенные.

Может получится что-то такое:

template <typename T, typename = void>
struct is_tuple_like : std::false_type {};
template <typename T>
struct is_tuple_like<T, std::void_t<decltype(std::tuple_size<T>::value), decltype(std::get<0>(std::declval<T>()))>> : std::true_type {};
template <typename T>
constexpr bool is_tuple_like_v = is_tuple_like<T>::value;

template<typename Tval, typename ... T>
void serialize_tuple_like(std::stringstream &outbuf, const Tval& arg, const T& ... rest) noexcept {
if constexpr (is_tuple_like_v<Tval>){
outbuf << "{ ";
std::apply([&outbuf](auto const&... packed_values) {
serialize_tuple_like(outbuf, packed_values ...);
}, arg);
outbuf << " }";
}
else{
outbuf << arg;
}

if constexpr(sizeof...(rest) > 0){
outbuf << ' ';
serialize_tuple_like(outbuf, rest ...);
}
}

template<typename ... T>
std::string args_to_string(const T& ... args) noexcept {
std::stringstream outbuf{};
if constexpr(sizeof...(args) > 0){
serialize_tuple_like(outbuf, args ...);
}
return outbuf.str();
}

int main(){
std::cout << args_to_string("test", 1,
std::tuple{"tuple1", 2, 3.0,
std::tuple{"tuple2", "boom"}},
std::pair{"pair", 4},
std::array{5, 6, 7, 8, 9});
}


Вывод будет такой:
test 1 { tuple1 2 3 { tuple2 boom } } { pair 4 } { 5 6 7 8 9 }


Даже не знаю, как эту лапшу разбирать. Идея такая что is_tuple_like_v проверяет аргумент на соответствие tuple-like объекту. Если нам на очередном вызове serialize_tuple_like попался такой объект, то мы берем и распаковываем его параметры в рекурсивный вызов serialize_tuple_like. Если у нас не tuple-like объект, то просто выводим его в стрим. Наверное, нужны проверки на то, что объект можно вывести в стрим, но решил, что это немного борщ для этого кода.

У функции довольно специфичные кейсы использования. Впрочем, как и у всей метапроги.

Don't live in metaverse. Stay cool.

#template #cpp17
😱24👍13🔥84😁3❤‍🔥1
Подкасты

Если вы думали, что подкаст - это такая штука, которая нужна только, чтобы под нее ехать в транспорте, кушать или тренироваться, то у меня для вас хорошие новости. Подкасты еще могут помогать образовываться!

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

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

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

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

У ребят уже 18 выпусков, так что контента вам хватит за глаза.

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

Вот ссылочка на плейлист с выпусками: https://www.youtube.com/watch?v=wknD9AGvKdc&list=PL0YYm7t_DM63uOt3OF2qRpB5rL27aceLs

Enjoy education. Stay cool.
👍32❤‍🔥10🔥83👎2
Дефолтные и ссылочные параметры

Есть такой паттерн или стиль написания кода, как входные+выходные параметры у функции. Вместо того, чтобы возвращать результат работы функции в возвращаемом значении, его возвращают через ссылочные параметры.

И вот допустим, у нас программист Иннокентий. У него в проекте есть функция, которая хорошо и без нареканий работает и отправляет запрос базе

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{});

Если запрос отослан удачно, возвращаем тру, если нет, то фолс.

Строка запроса к базе может содержать плейсхолдеры(это метки в подготовленном запросе, куда будут подставляться актуальные значения параметров), а может и не содержать. Если они есть, то вместе с query передаются и параметры для подстановки в запрос. Если нет, то у нас есть пустое значение по-умолчанию.

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

А в проекте у него используется паттерн с выходными параметрами. Обычно это реализовывается так: в начале в аргументах функции идут все входные параметры и затем все выходные. Вроде как логично.

Он, согласно своему код стайлу, пишет:

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{},
DbConnectionState& state);


Реализовал функцию и запустил билд. А он, хромоногий, упал.

Нельзя указывать параметры функции без значения по умолчанию после параметров, имеющих это значение.

Видимо это сделано так, потому что иначе появляется пространство для неопределенности.

Ладно еще в этом случае компилятор видит разные типы и может как-то соотнести 2 и 2. Но например в случае тривиальных типов все не так однозначно. Они могут неявно конвертироваться друг в друга и тут уже распарсить нельзя.

void foo(int i, float j = 5.0, unsigned k);

foo(1, 2); // Невозможно понять, вызвали ли вы foo(1, 5.0, 2) или
например по ошибке передали слишком мало аргументов


Закатив глаза и особо не думая, он исправляет:

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{},
DbConnectionState& state = DbConnectionState::OK);


И билд тоже падает! Но теперь уже по другому поводу.

Неконстантные левоссылочные параметры нельзя определять со значениями по-умолчанию.

Причина тут очень простая. Вы не можете создать левую ссылку из значения типа rvalue reference. У объекта должно быть имя, чтобы его можно было присвоить неконстантной левой ссылке. У DbConnectionState::OK имени нет, поэтому и упали.

Выход только один. Нарушать свой код-стайл. Придется пихать параметр DbConnectionState& state либо первым параметром, либо между query и params.

Первый способ вообще в принципе нарушает все негласные соглашения в объявлениях функции среди всех языков.

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

Кенни не придумал ничего лучше и сильно расстроился. Пошел в лес, увидел там машину, сел в нее и сгорел.

А что Иннокентий в принципе мог сделать в этой ситуации? Жду ваших вариантов в комментариях)

Don't worry. All difficulties will pass.

#cppcore
🔥157❤‍🔥7👍3🤷‍♂2🤔1
​​std::jthread

С std::thread в С++ есть один интересный и возможно назойливый нюанс. Давайте посмотрим на код:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
}


Простой Хелло Ворлд в другом потоке. Но при запуске программы она тут же завершится примерно с таким сообщением: terminate called without an active exception.

Эм. "Я же просто хотел быть счастливым вывести в другом потоке сообщение. Неужели просто так не работает?"

В плюсах много чего просто так не работает)

А вот такая программа:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
std::this_thread::sleep_for(std::chrono::seconds(1));
}


Все-таки напечатает Hello, World!, но потом все равно завершится с std::terminate.

Уже лучше, но осадочек остался. Ничего не понятно. Давайте разбираться.

С помощью слипа мы немного затормозили main тред и сообщение появилось. То есть мы оттянули выход из main и завершение программы и дали возможность подольше поработать новосозданному потоку. А что происходит при выходе из скоупа функции? Вызов деструкторов локальных объектов.

Так вот в деструкторе единственного локального объекта и проблема. Согласно документации, для каждого потока мы обязательно должны выбрать одну из 2-х стратегий поведения: отсоединить его от родительского потока или дождаться его завершения. Делается это методами detach и join соответственно.

И если мы не вызовем один из этих методов для объекта потока, то в своем деструкторе он вызовет std::terminate. То есть корректный код выглядит так:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
thr.join();
}


Мы дожидаемся конца исполнения потока и только после этого завершаем программу. Теперь никаких терминаторов.

Но зачем эти формальности? Вообще говоря, часто мы хотим присоединить поток почти сразу перед вызовом его деструктора. А вот отсоединяем поток мы почти сразу после создания объекта. Мы же заранее знаем, хотим ли мы отпустить поток в свободное плавание или нет. И, учитывая эти факты, было бы приятно иметь возможность не вызывать join самостоятельно, а чтобы он за нас вызывался в деструкторе.

И С++20 приходит здесь нам на помощь с помощью std::jthread. Он делает ровно это. Если его не освободили и не присоединили мануально, то он присоединяется в деструкторе.

Поэтому такой код сделает то, что мы ожидаем:

int main()
{
std::jthread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
}


jthread не только этим хорош. Его исполнение можно еще отменять/приостанавливать. Но об этом уже в другой раз.

Кстати. Вопрос на засыпку. Слышал, что там какие-то сложности у кланга с jthread были. Сейчас все нормально работает?

Make life easier. Stay cool.

#cpp20 #concurrency
🔥30👍20❤‍🔥64😁1
Получаем адрес стандартной функции

Иногда людям приходится работать с указателями на функции. И у них может возникнуть надобность взять адрес у стдшной функции. Например, вы хотите как-то трансформировать каждый символ в строке c помощью std::transform и в качестве коллбэка передаете именно указатель на функцию

std::transform(s.begin(), s.end(), s.begin(), std::toupper);


Вроде ничего страшного не должно случиться. Но согласно стандарту, поведение программы в этом случае unspecified и потенциально даже ill-formed.

Все потому что нельзя брать адреса у стандартных функций. Начиная с С++20 это явно прописано в стандарте.

Let F denote a standard library function, a standard 
library static member function, or an instantiation of
a standard library function template.

Unless F is designated an addressable function,
the behavior of a C++ program is unspecified
(possibly ill-formed) if it explicitly or implicitly
attempts to form a pointer to F.

Possible means of forming such pointers include
application of the unary & operator, addressof,
or a function-to-pointer standard conversion.

Moreover, the behavior of a C++ program is
unspecified (possibly ill-formed) if it attempts
to form a reference to F or if it attempts to form
a pointer-to-member designating either a standard library
non-static member function or an instantiation of
a standard library member function template.


Нельзя формировать указатели и ссылки на стандартные функции и нестатические методы.


Но почему? Чем они отличаются от обычных функций?

На самом деле ничем. Дело в том, что будет с функциями в будущем.

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

А стандарт - это штука не статичная. В него постоянно добавляются новые фичи и обновляются в том числе старые инструменты. Например, с приходом С++11 у нас появилась мув-семантика. И условный метод вектора push_back обзавелся новой перегрузкой для правой ссылки. И это сломало код, который брал адрес метода push_back.

#include <vector>

template<typename T>
void Invoke(std::vector<int>& vec, T mem_fun_ptr, int arg)
{
(vec.*mem_fun_ptr)(arg);
}

int main()
{
std::vector<int> vec;
Invoke(vec, &std::vector<int>::push_back, 42);
}


Этот код успешно собирается на 98 плюсах, но не может этого сделать на 11-м стандарте. Можете поиграться с примером на годболте.

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

std::transform(s.begin(), 
s.end(),
s.begin(),
[](unsigned char c){ return std::toupper(c); });


Don't break your future. Stay cool.

#cppcore #cpp20
👍347🔥7❤‍🔥3😱1
Получаем адрес перегрузки
#новичкам

Представьте, что у вас есть функция, которая вызывает любой коллбэк:

template<class T, class... Args>
void call_callback(T callback, Args... args) {
callback(args...);
}


И есть другая функция, которую вы вызываете через call_callback.

int func() {
return 42;
}

call_callback(func);


Все работает прекрасно. Но теперь мы добавляем перегрузку func и пытаемся их вызвать через call_callback.

int func() {
return 42;
}

int func(int num) {
return num;
}

call_callback(func);
call_callback(func, 42);


Получаем ошибку

error: no matching function for call to
‘call_callback(<unresolved overloaded function type>)


Компилятор не может понять, какую конкретно перегрузку вы имели ввиду.

Дело в том, что имя функции неявно приводится к указателю на эту функцию. Тип указателя на функцию зависит от ее сигнатуры. И просто по имени невозможно понять, с какой сигнатурой функцию мы имеем ввиду.

Что же делать?

Дать компилятору подсказку и использовать static_cast.

call_callback(static_cast<int(*)()>(func));
call_callback(static_cast<int(*)(int)>(func), 42);


Теперь все работает, как надо.

Give useful hints. Stay cool.

#cppcore
21👍2911🔥9😁2
std::visit
#опытным

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

Так вот чтобы голова не болела при работе с std::variant надо 2 раза в день после еды принимать std::visit.

Эта функция позволяет применять функтор к одному или нескольким объектам std::variant. И самое главное, что вам не нужно беспокоиться по поводу того, какой именно объект находится за личиной варианта. Компилятор все сам сделает.

template< class Visitor, class... Variants >  
constexpr visit( Visitor&& vis, Variants&&... vars );

template< class R, class Visitor, class... Variants >
constexpr R visit( Visitor&& vis, Variants&&... vars );


Так выглядят ее сигнатуры. Первым параметром передаем функтор, дальше идут варианты.

Попробуем использовать эту функцию:

using var_t = std::variant<int, long, double, std::string>;

std::vector<var_t> vec = {10, 15l, 1.5, "hello"};

for (auto& v: vec)
{
var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
std::visit([](auto&& arg){ std::cout << arg; }, w);
}
//OUTPUT:
// 20 30 3 hellohello


Главное, чтобы функтор умел обрабатывать любую комбинацию типов, которую вы можете передать в него. Обратите внимание, что мы используем здесь generic лямбду, которая может принимать один аргумент любого типа.

Если вы хотите передать в std::visit несколько объектов, то функтор должен принимать ровно такое же количество аргументов и уметь обрабатывать любую комбинацию типов, которая может содержаться в вариантах.

std::visit([](auto&&... arg){ ((std::cout << arg << " "), 
...,
(std::cout << std::endl)); }, vec[0], vec[1]);
std::visit([](auto&&... arg){ ((std::cout << arg << " "),
...,
(std::cout << std::endl)); }, vec[0], vec[1], vec[2]);
// OUTPUT
// 10 15
// 10 15 1.5


Используем здесь дженерик вариадик лямбду, чтобы она могла принимать столько аргументов, сколько нам нужно. И эта конструкция работает для любого количества переданных объектов std::variant;

Так что std::variant и std::visit - закадычные друзья и им друг без друга грустно! Не заставляйте их грустить.

Have a trustworthy helper. Stay cool.

#template #cpp17
3👍276🔥6❤‍🔥32🤔2