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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Оператор запятая внутри operator[]

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

Именно поэтому начиная с С++20 использование оператора запятая внутри оператора квадратные скобки признано устаревшим(deprecated).

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

Итого:

void f(int *a, int b, int c) 
{
a[b,c]; // deprecated
a[(b,c)]; // OK
}


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

Кроме того, скорее всего уже были намеки, что в 23-х плюсах появится мультипараметрический интерфейс для operator[]. Поэтому стандарт заранее позаботился о том, чтобы был всего одна разрешенная семантика использования нескольких параметров в этом операторе.

Remove error-prone things. Stay cool.

#cpp20 #cpp23
23👍18🔥7😁6🤯1
​​Новый год на пороге, а значит наступило время подводить итоги уходящего года! 🤩

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

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

Поэтому хочу выразить огромную благодарность вам всем.
С П А С И Б О !!! Вы драйверы этого канала и конечно же нашего развития. Почти каждый день выносить свои знания на суд огромной аудиторий потрясающих специалистов - конечно тут волей-неволей сильно прокачаешься во всех мелочах.

Круто, что канал выработал свой неповторимый стиль. Мы стараемся разбавлять нудятину о С++ всякими смешнявками и мемами. А также придумали несколько новых форматов. Мне лично очень зашел формат #ревью. Это очень крутой способ затронуть несколько сложных тем, проверить зоркость подписчиков, ну и просто потрындеть за плюсы. Вроде очевидная вещь, но я такого ни у кого не встречал.

Ну что это мы все о себе, да о себе...

Желаем вам в Новом году не останавливаться на достигнутом и мчаться вперед на всех парах! Ставьте перед собой цели и достигайте их. И главное - не давайте змею искусителю сбить вам с пути😉. Никаких питонов - только Плюсы, только хардкор!

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

Happy New Year! Stay cool.
25🔥80🎄37👍1796👎4
​​Допотопный доступ к многомерному массиву Ч1
#опытным

Начнем рассказ о том, как люди до С++23(то есть до сих пор) жили с оператором[], принимающим только один параметр.

И начнем мы с банальщины. Вот у нас есть класс матрицы. По всем канонам С++ мы должны получать доступ к ее элементам вот так matrix[i][j]. Этот формат сохраняет констистентность с доступом к элементам одномерных массивов.

Однако сразу натыкаемся на проблему. Класс один, а вызываем мы оператор два раза. Несостыковочка.

Ее решает паттерн прокси. Мы создаем прокси класс и возвращаем его объект из первого индекса. Дальше у этого прокси класса определяем оператор[] и на выходе получаем наш элемент.

Условно, из первого оператора возвращаем ссылку на строку матрицы, а из второго - уже сам элемент.

Выглядит это примерно так:

template <typename T>
struct ArraySpan {
ArraySpan(T * arr, size_t arr_size) : data_{arr}, size_{arr_size} {}
ArraySpan(T * arr_begin, T * arr_end)
: data_{arr_begin}
, size_{std::distance(arr_begin, arr_end)} {}

T& operator[](std::size_t i) {
return *(data_ + i);
}

size_t size() const {return size_;}
T * data() {return data_;}
private:
T * data_;
size_t size_;
};

template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init) : ROWS{rows}, COLS{cols}, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;
ArraySpan<T> operator[](std::size_t row) {
return ArraySpan{data.data() + row * COLS, COLS};
}
std::vector<T>& underlying_array() { return data; }
size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}
private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};

int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (int i = 0; i < mtrx.row_count(); ++i) {
for (int j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
for (int i = 0; i < mtrx.row_count(); ++i) {
auto row = mtrx[i];
for (int j = 0; j < row.size(); ++j) {
std::cout << std::setw(2) << row[j] << " ";
}
std::cout << std::endl;
}
}

В примере мы 2 раза проходимся по матрице, чтобы продемонстрировать возможность индексации элементов через объект самой матрицы и объекта строки.

Можно было бы конечно не писать отдельно наш прокси тип ArraySpan, а использовать готовый std::span из С++20, но оставим так. Идея использовать такой легковесный объект понятна - нам не нужно лишнего оверхеда на копирование или создание сложного объекта, чтобы просто получить доступ к элементу матрицы.

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

В чем недостаток такого способа? Куча прокси классов, которые возможно и не нужны. Возможно, нам просто нужна двумерная структура, чтобы получать доступ к конкретным элементам. Использование карты для морского боя не предполагает использование отдельных строк или столбцов. Хотелось бы просто индексировать конкретные элементы. Но тем не менее мы вынуждены использовать прокси класс.

А если структура трухмерная? Уже 2 прокси нужно будет. Больше вложенность - больше классов-прослоек. Не очень удобно. Поэтому и придумали другие способы, о которых расскажу в следующих постах.

Find the way out. Stay cool.

#cppcore #cpp20 #cpp23
18👍12🔥7❤‍🔥2😁2
​​Допотопный доступ к многомерному массиву Ч2

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

Самый простой из них - вместо оператора[], который может принимать только один параметр, использовать оператор(), который можно перегружать, как нашей душе угодно.

Он может принимать ничем не ограниченный набор параметров. И к тому же их несколько штук можно определить. Например, если передаем 1 аргумент, то возвращается ссылка на строку матрицы. А если 2 аргумента, то возвращаем ссылку на сам элемент.

Вот так это может выглядеть:

template <typename T>
struct ArraySpan {
ArraySpan(T * arr, size_t arr_size) : data_{arr}, size_{arr_size} {}
ArraySpan(T * arr_begin, T * arr_end) : data_{arr_begin}, size_{std::distance(arr_begin, arr_end)} {}

T& operator()(std::size_t i) {
return *(data_ + i);
}
size_t size() const {return size_;}
T * data() {return data_;}
private:
T * data_;
size_t size_;
};

template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init) : ROWS{rows}, COLS{cols}, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;

ArraySpan<T> operator()(std::size_t row) {
return ArraySpan{data.data() + row * COLS, COLS};
}

T& operator()(std::size_t row, std::size_t col) {
return data[row * COLS + col];
}
std::vector<T>& underlying_array() { return data; }

size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}

private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};

int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (int i = 0; i < mtrx.row_count(); ++i) {
for (int j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx(i, j) << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
for (int i = 0; i < mtrx.row_count(); ++i) {
auto row = mtrx(i);
for (int j = 0; j < row.size(); ++j) {
std::cout << std::setw(2) << row(j) << " ";
}
std::cout << std::endl;
}
}


Идейно здесь все тоже самое, да и пример почти идентичный, только используем оператор() вместо квадратных скобок. Это нам позволило в одном классе определить, как мы хотим давать доступы по одному и двум индексам. Это также упростило компилятору задачу по инлайнингу доступа к элементу по двум индексам, так это один вызов функции.

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

Из неочевидного: для типа строки тоже придется использовать круглые скобки, чтобы сохранить сходство обработки с матрицами(на примере во втором цикле для доступа к конкретному элементу прокси массива тоже используем ()). Ну или не придется, но тогда сходства работы не будет и возникнет еще сильнее путаница.

Ну и непонятно, мы вообще функцию вызываем или что??


Тем не менее, этот способ есть даже в FAQ на странице, посвященному стандарту C++. И он совместим с нотацией индексации в Фортране. Поэтому метод довольно распространенный.

Be consistent. Stay cool.

#cppcore
👍23🔥10❤‍🔥31😁1
Допотопный доступ к многомерному массиву Ч3

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

Благодаря появившейся в С++11 аггрегированной инициализации, мы можем инициализировать структуры с помощью фигурных скобок:

struct Example {
int i, j;
};

Example test{1, 2};
std::cout << test.i << " " << test.j << std::endl;
// OUTPUT:
// 1 2


Этим можно воспользоваться для индексирования многомерных массивов. Определяем оператор[], который принимает на вход структуру, а вызываем мы этот оператор с помощью списка инициализации, который превращается в структуру.

struct Indexes {
size_t row;
size_t col;
};

template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init)
: ROWS{rows}, COLS{cols}
, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;
// MAGIC HERE
T& operator[](Indexes indexes) {
return data[indexes.row * COLS + indexes.col];
}
std::vector<T>& underlying_array() { return data; }
size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}
private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};

int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (size_t i = 0; i < mtrx.row_count(); ++i) {
for (size_t j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx[{i, j}] << " "; // MAGIC HERE
}
std::cout << std::endl;
}
}


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

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

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

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

struct Index {
size_t row;
};

template<typename T>
T& Matrix<T>::operator[](Index index) {
return ArraySpan{data.data() + index.row * COLS, COLS};
}

auto row = mtrx[{1}];


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

Дело в том, что аггрегированно инициализировать типы можно и меньшим количеством аргументов. То есть с помощью {1} я могу создать и объект Index, и объект Indexes.

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

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

Don't be limited. Stay cool.

#cppcore #cpp11
👍24🔥1341❤‍🔥1
Безымянный lock_guard

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

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

// std::map<std::string, std::string> SomeClass::map_;
// std::mutex SomeClass::mtx_;

bool SomeClass::UpdateMap(const std::string& key, const std::string& value) {
std::lock_guard{mtx_};
auto result = map_.insert({key, value});
return result.second;
}


Для С++17 вполне синтаксически верный код. И он будет запускаться. Но потокобезопасности не будет.

"Но как же! Я же использовал lock_guard!"


Да вот только этот lock_guard безымянный. То есть является временным объектом. Соответственно, его деструктор вызовется ровно до insert'а и мьютекс освободится. А значит ничего безопасного тут нет.

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

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

Stay alerted. Stay cool.

#cppcore
🔥40👍19😁94
RAII обертки мьютекса
#новичкам

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

Для начала - для работы с мьютексами нужны обертки. Очень некрасиво в наше время использовать чистые вызовы lock и unlock. Это наплевательское отношение к современному и безопасному(посмеемся) С++.

Здесь прямая аналогия с выделением памяти. Для каждого new нужно вызывать соответствующий delete. Чтобы случайно не забыть этого сделать(а в большом объеме кода что-то забыть вообще не проблема), используют умные указатели. Это классы, используя концепцию Resource Acquisition Is Initialization, помогают нам автоматически освобождать ресурсы, когда они нам более не нужны.

Также для каждого lock нужно вызвать unlock. Так может по аналогии сделаем класс, который в конструкторе будет лочить мьютекс, а в деструкторе - разблокировать его?

Хорошая новость в том, что это уже сделали за нас. И таких классов даже несколько. Если не уходить в специфическую функциональность, то их всего 2. Это std::lock_guard и std::unique_lock.

lock_guard - базовая и самая простая обертка. В конструкторе лочим, в деструкторе разлачиваем. И все. Больше ничего мы с ним делать не можем. Мы можем только создать объект и удалить его. Даже получить доступ к самому мьютексу нельзя. Очень лаконичный и безопасный интерфейс. Но и юзкейсы его применения довольно ограничены. Нужна простая критическая секция. Обезопасил и идешь дальше.


bool MapInsertSafe(std::unordered_map<Key, Value>& map, const Key& key, Value value) {
std::lock_guard lck(mtx);
if (auto it = map.find(key))
return false;
else {
it->second = std::move(value);
return true;
}
}


unique_lock же больше походит на std::unique_ptr. Уже можно получить доступ в объекты, поменяться содержимым с другим объектом unique_lock, поменять нижележащий объект на другой. Ну и раз мы все равно даем возможность пользователю получить доступ к нижележащему объекту, то удобно вынести в публичный интерфейс unique_lock те же методы, которыми можно управлять самим мьютексом. То есть lock, unlock, try_lock и тд - это часть интерфейса std::unique_lock.

Уже понятно, что этот вид обертки нужно использовать в более сложных случаях, когда функциональности lock_guard не хватает.

А в основном не хватает возможности использовать методы lock и unlock. Например, при работе в кондварами std::unique_lock просто необходим. Читатель заходит в критическую секцию, локает замок и затем видит, что определенные условия еще не наступили. Допустим еще нет данных для чтения. В такой ситуации надо отпустить мьютекс и заснуть до лучших времен. А при пробуждении и наступлении подходящих условий, опять залочить замок и начать делать работу.

void worker_thread()
{
std::unique_lock lk(m);
cv.wait(lk, []{ return ready; });
// process data
lk.unlock();
}


Кондвар в методе wait при ложном условии вызывает unlock у unique_lock. При пробуждении и правдивом условии вызывает lock и ждет освобождения мьютекса.

Юзкейсы можно обсуждать очень долго, но вывод один: всегда используйте обертки над мьютексами! Возможно, они спасут вашу жизнь.

А уж какую обертку использовать подскажет сама задача.

Stay safe. Stay cool.

#concurrency
👍28🔥136😁3
Еще один плюс RAII
#опытным

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

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

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

std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
mtx_.lock();
if (auto it = cache.find(key); it != cache_.end()) {
mtx_.unlock();
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
mtx_.unlock();
return result;
}
}


Простой код запроса к базе с кэшом. Что будет в том случае, если метод Select бросит исключение? unlock никогда не вызовется и мьютекс коннектора к базе будет навсегда залочен! Это очень печально, потому что ни один поток больше не сможет получить доступ к критической секции. Даже может произойти deadlock текущего потока, если он еще раз попытается захватить этот мьютекс. А это очень вероятно, потому что на запросы к базе скорее всего есть ретраи.

Мы могли бы сделать обработку исключений и руками разлочить замок:

std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
try {
mtx_.lock();
if (auto it = cache.find(key); it != cache_.end()) {
mtx_.unlock();
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
mtx_.unlock();
return result;
}
}
catch (...) {
Log("Caught an exception");
mtx_.unlock();
}
}


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

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

Выход один - использовать RAII.

std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
std::lock_guard lg{mtx_};
if (auto it = cache.find(key); it != cache_.end()) {
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
return result;
}
}


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

Спасибо Михаилу за идею)

Stay safe. Stay cool.

#cpp11 #concurrency #cppcore #goodpractice
🔥33👍1262
Не вызывайте пользовательский код по локом
#опытным

Вы пишите свой потокобезопасный класс, вставляете свои любимые примитивы синхронизации, обмазываете это все подходящим интерфейсом. И тут вам понадобилось вызвать какой-то пользовательский код, какой-нибудь метод или функцию, которую вы сами не писали. И вы это вставляете под замок. Ошибка!

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

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

А если функция кинет исключение, а вы блокируете мьютекс без оберток(осуждаем)? Мьютекс не освободится. И при повторной его блокировке сразу получили UB. Это как раз пример из предыдущего поста.

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

std::shared_ptr<Value> IStorage::GetDataWithCache(const Key& key) {
std::unique_lock ul{mtx_};
if (auto it = cache.find(key); it != cache.end()) {
return it->second;
} else {
ul.unlock(); // HERE
std::shared_ptr<Value> result = GetData(key);
ul.lock();
cache.insert({key, result});
return result;
}
}

GetDataWithCache - это метод интерфейсного класса IStorage и мы сделали приватный виртуальный метод GetData. За то, что делает GetData мы не отвечаем и там потенциально может быть много реализаций. При получении данных из файла там может быть еще один лок. А общение с базой может занять довольно много времени. Поэтому мы просто отпускаем замок перед вызовом этого метода и не паримся о последствиях.

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

Кстати, в этом примере есть одна не совсем очевидная проблема(помимо более очевидных хехе). Она не относится непосредственно к теме поста. Можете попробовать в комментах ее найти и предложить решение.

#goodpractice
🔥17👍96❤‍🔥2
Обзор книжки #1

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

Но начнем не с базовых книжек. Есть один очень крутой фундаментальный труд по многопоточке в плюсах - "С++. Практика многопоточного программирования" Энтони Уильямса. Та самая знаменитая "Concurrency in Action".

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

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

Так вот Энтони Уильямс - как раз такой гид. Все в книге разложено четко и выверено.

1️⃣ Сначала погружаемся в саму концепцию параллелизма и многозадачности. Понимаем ее преимущества, способы организации и требования. И наконец-то понимаем разницу между потоками и процессами.

2️⃣ Изучаем самый простой механизм обеспечения безопасности - мьютекс.

3️⃣ Изучаем более сложные инструменты синхронизации - кондвары, фьючи, промисы.

4️⃣ Только после этого всего, когда вы уже наловчились думать в многопотоке и выявлять потенциальные проблемы, он переходит к модели памяти и атомикам. Тут конечно, все не просто и эта тема очень сложна для понимания. Возможно придется перечитывать несколько раз.

После этого можно сказать, что вы знаете про все инструменты из threading STL. И вас можно отпускать. Но нет!

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

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

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

Я сам начинал познание многопоточки именно с нее. Тогда была версия только для 14-х плюсов(сейчас уже для 17-х). Есть такие книги, которые читаешь снова и снова. И с каждым прочтением переосмысливаешь свои знания и по-другому смотришь на мир. Вот это одна из таких книжек. Но вот реально, каждый раз читаю и что-то новое узнаю. Моя любимая часть - модель памяти, лок-фри программы. Там всегда взрыв мозга и новые инсайты)

Знаю, знаю. У вас в голове уже вопрос: "Куда деньги нести? Дайте мне уже это золото!"

Не спешите. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу разыграть его среди подписчиков.

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

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

Победителя естественно выберем рандомайзером.

Если вы уже читали эту книжку, то оставьте свои впечатления о ней в комментах.

Be lucky. Stay cool.

#books
🔥36👍1310🤩3👎1😁1
​​Мапа и оператор[]
#новичкам

Не так редко можно увидеть код примерно такого вида:

if (map_.count(token)) {
map[token]++;
} else {
map[token] = 1;
}


Типа если в мапе нет ключа, то создаем там объект со значением 1, если есть, то просто инкрементируем значение.

Не знаю, почему многие боятся написать просто вот так:

map[token]++;


Наверное не знают пары секретиков. Сегодня я о них поведаю.

Первый - оператор[] для мапы создает объект, если его не было в мапе. То есть неважно, был ли ключ в мапе или нет, вы можете вызвать operator[] и ничего плохого не произойдет. Просто в словаре будет еще один ключ, если до этого не было.

Посмотрим на пример:

struct CLASS {
CLASS() {std::cout << "default" << std::endl; i = 0;}
void operator++(int) {std::cout << "increment" << std::endl; i++; }
int i;
};

int main() {
std::map<std::string, CLASS> map;
map["qwe"]++;
std::cout << map["qwe"].i << std::endl;
}


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

Вывод:
default
increment
1


То есть для значения нового элемента мапы вызывается конструктор по умолчанию. Объект создается налету.

Одно беспокойство убрали.

"Но тут в примере в дефолтном конструкторе вы явно инициализируете поле i в ноль. Почему реальное интовое значение в мапе будет выставляться в ноль?"

Да. Потому что гарантируется value initialization при создания объектов в ассоциативных контейнерах.

Value инициализация для интовой переменной может выглядеть вот так:

int();
int{};
int object{};
new int();
new int{};


Эффект value initialization на тривиальных типах - они инициализируются нулем.

std::cout << int() << std::endl;
std::cout << int{} << std::endl;
int object{};
std::cout << object << std::endl;
std::cout << *(new int()) << std::endl;
std::cout << *(new int{}) << std::endl;


Вывод:
0
0
0
0
0


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

Know secrets. Stay cool.

#cppcore #STL
🔥37👍246🎉3🤯2❤‍🔥1
​​Наследование? От лямбды? Ч1
#опытным

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

Как это вообще возможно?

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

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

Значит это вполне легальный кандидат на наследование!

Придется конечно немного поколдовать вокруг отсутствия имени, но для С++ это не проблема.

template<class Lambda>
struct DerivedFromLambda : public Lambda
{
DerivedFromLambda(Lambda lambda) : Lambda(std::move(lambda)) {}
using Lambda::operator();
};

int main(){
auto lambda = []{return 42;};
DerivedFromLambda child{lambda};
std::cout << child() << std::endl;
}

// OUTPUT:
// 42


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

Дальше создаем лямбду и создаем объект обертки, просто передавая лямбду в конструктор. Мы специально не указываем явно шаблонный параметр DerivedFromLambda, потому что мы не знаем настоящего имени лямбды. Мы даем возможность компилятору самому вывести нужный шаблонный тип на основании инициализатора. Это возможно благодаря фиче С++17 Class Template Argument Deduction.

Но даже и на С++11-14 можно написать подобное. Ведь у нас есть оператор decltype, который возвращает в точности тип того выражения, которое мы в него передали. Тогда мы бы создавали объект так:

auto lambda = []{return 42;};
DerivedFromLambda<decltype(lambda)> child{lambda};


Зачем это нужно только? К этому мы будем потихоньку подбираться следующие пару постов.

Do surprising things. Stay cool.

#template #cppcore #cpp11 #cpp17
👍3021🔥12🤯10💯2
Наследование? От лямбды? Ч2
#опытным

Лямбды - это функциональные объекты по своей сути. Объекты классов с перегруженным оператором(). Зачем вообще от такой сущности наследоваться? Можно же просто сделать обычный класс с такими же перегруженным оператором и расширять его сколько влезет.

Ну как будто бы да. От одной лямбды наследоваться особо нет смысла. А что насчет множественного наследования?

Идейно - наш наследник будет наследовать публичный интерфейс всех своих родителей. То есть в наследнике будет много перегруженных операторов() для разных входных параметров. Вот это уже чуть удобнее. Мы можем находу создавать объект, который единообразно с помощью перегрузок будет обрабатывать какие-то вещи.

Покажу чуть подробнее:

template<class Lambda1, class Lambda2>
struct DerivedFromLambdas : public Lambda1, Lambda2
{
DerivedFromLambdas(Lambda1 lambda1, Lambda2 lambda2)
: Lambda1(std::move(lambda1))
, Lambda2{std::move(lambda2)} {}
using Lambda1::operator();
using Lambda2::operator();
};

int main(){
DerivedFromLambdas child{[](int i){return "takes int";}, [](double d){return "takes double";}};
std::cout << child(42) << std::endl;
std::cout << child(42.0) << std::endl;
return 0;
}
// OUTPUT:
// takes int
// takes double


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

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

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

Have a sense. Stay cool.

#template #cppcore #cpp17
🔥31👍106🤯3❤‍🔥11
Наследование? От лямбды? Ч3

А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!

template<typename ... Lambdas>
struct DerivedFromLambdas : Lambdas...
{
DerivedFromLambdas(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}

using Lambdas::operator()...;
};


И что нам этот шаг дал?

Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.

Помните наш std::visit? Который применяет визитор к объекту варианта.

Так вот теперь мы можем налету делать наши визиторы!


template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{
}
using Lambdas::operator()...;
};

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

int main(){
std::vector<var_t> vec = {10, 1.5, "hello"};
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}


Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:

10 1.500000 "hello"


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

Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.

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

Visit your close ones. Stay cool.

#template #design #cpp17
22👍14🔥9🤯32👏1
Итоги конкурса

Мы долго ждали и, наконец, дождались. Вчера я честно взял генератор случайных чисел и нашел победителя и будущего счастливого обладателя книжки "С++. Практика многопоточного программирования" Энтони Уильямса. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете моей непредвзятости)

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

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

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

Be lucky. Stay cool.
👏65🔥9🎉73😭2🤔1
Считаем единички
#задачки

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

Вроде бы простая и популярная задача: посчитать количество единиц в битовом представлении числа. Уверен, что большинство из вас решали эту задачу.

Однако популярный подход - не самый эффективный, элегантный и интересный.

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

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

А чтобы умудренным опытом людям было немного интереснее, давайте ограничим условия. Задачу надо решить либо за константное время(в среднем), либо за наименьшее количество строчек. Выражения вне цикла for разделенные символом ; считаются разными строчками.

int count_ones(unsigned num) {
// Here your code
}


Challenge yourself. Stay cool.
😁2919👍9🔥32
Считаем единички. Решения

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

int count_ones(unsigned num) {
int result = 0;
while(num > 0) {
result += num & 1;
num >>= 1;
}
return result;
}


Алгоритмическая сложность этого решения - О(log(num)).
Что может быть интереснее? Например, знали ли вы, что выражение num & (num - 1) лишает число его самой правой единички? Посмотрите сами:

10 = 1010
1010 & (1010 - 1) = 1010 & 1001 = 1000

118 = 111011
111011 & (111011 - 1) = 111011 & 111010 = 111010


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

int count_ones(unsigned num) {
int result = 0;
for(; num > 0; num &= (num - 1), ++result);
return result;
}


А что насчет самого короткого решения? Зачем писать велосипед, если можно просто воспользоваться встроенной функцией компилятора(или С++20 фичей):

int count_ones(unsigned num) {
return std::popcount(num);
// or compiler extension
// return __builtin_popcount(num);
}



А что, если мы хотим константную сложность? Такое вообще возможно?

Конечно. Нам потребуется всего sizeof(num)* 8 итераций цикла и проверки последнего бита, чтобы найти нужное число. Константа? Да. Эффективно ли это? Это даже медленнее, чем самое первое решение.

Однако давайте подумаем еще чуть-чуть. Комбинаций битов в инте на самом деле не такой уж и и много. Всего 2^32. Можно создать массив байтов на 2^32 элементов и в каждой ячейке хранить количество единичек для числа равного индексу этой ячейки. Мы это как-то можем заранее нагенерить(или при первом вызове функции) и потом все вызовы функции count_ones будут занимать константное время. Правда памяти сожрется на это предостаточно.

static std::array<uint8_t, std::numeric_limit<uint32_t>::max()> ones;
// somehow fill array
int count_ones(unsigned num) {
return ones[num];
}


Кстати полезный ход. Иногда из-за сильных ограничений по входным данным задачи ее можно решить намного более оптимальным способом.

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

Но чтобы было прям наглядно понятна логика, то массив можно сделать еще меньше, если брать по 4 бита(тетрад). Различных тетрадов всего 16 штук, поэтому и нужно будет всего 16 байт доп памяти и 8 итераций цикла. Спасибо, @tutralex, за решение)

int count_ones(unsigned num)
{
static unsigned char c[16]={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int count=0;
while (num)
{
count+=c[num&0x0F];
num>>=4;
}
return count;
};


В общем, вы поняли. Чем меньше массив, тем больше итераций цикла и наоборот. Выбирайте, что вам больше подходит.

Если вы еще не устали, то у меня для вас есть banger. Нахождение количества единичных бит в числе - это не просто задача с литкода. У нее есть практическое применение. Есть такая штука, как расстояние Хемминга для двоичных чисел. Это количество символов, которое отличает данную строку битов от строки, заполненной нулями. То есть это и есть наше количество единичек. Эта штука используется во многих дисциплинах, в том числе и криптографии. Не удивительно, что много народу совершенствовало решение этой задачи. На мой взгляд, самое мозгодробительное решение выглядит примерно так:
10👍74🔥2🤔1😱1
int count_ones(unsigned num)
{
num = (num & 0x5555555555555555 ) + ((num >> 1) & 0x5555555555555555 );
num = (num & 0x3333333333333333 ) + ((num >> 2) & 0x3333333333333333 );
num = (num & 0x0f0f0f0f0f0f0f0f ) + ((num >> 4) & 0x0f0f0f0f0f0f0f0f );
num = (num & 0x00ff00ff00ff00ff ) + ((num >> 8) & 0x00ff00ff00ff00ff );
num = (num & 0x0000ffff0000ffff ) + ((num >> 16) & 0x0000ffff0000ffff);
return num;
}

Выглядит все так, как будто кот случайно блеванул на экран, но естественно у этой каши есть логичное объяснение, которое можете найти тут.

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

Honorable mentions:
Питонисты часто в этой задаче упоминают, что они одной левой могут превратить число в бинарное представление, потом в строку и просто посчитать одной функцией количество вхождений единички. А мы вообще-то ничем не хуже! И даже лучше! Нам не нужно конвертировать бинарное представление в строку. Достаточно std::bitset и его метода count:

int count_ones(unsigned num) {
return std::bitset<32>{num}.count();
}


Solve your problems. Stay cool.
29👍20🤯7😱21
​​Header only либы. Pros and cons
#новичкам

Мы живем в мире С++, где почти нет ничего общепринятого. Часто даже не для очень специфичной задачи, типа сериализации, приходится искать какие-то сторонние решения и сравнивать их. Давайте отложим в сторону вопросы о качестве кода и дизайне и сконцентрируемся вот на чем. Какую библиотеку выбрать: уже скомпилированную или полностью header-only? Будем рассматривать вопрос с точки зрения header-only либ. Какие у них преимущества и недостатки?

Преимущества:

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

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

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

Бо'льшая переносимость. Не все компиляторы одинаково компилируют одни и те же сущности даже на одной и той же платформе. Ваша ддлка или сошник может просто по ABI вам не подходить. Хэдэр-онли либы приходится компилировать вместе с проектом, что убирает проблему несовместимости ABI.

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

Недостатки:

⛔️ Крупные объектные файлы. Раз нам доступно определение сущностей, то мы можем попробовать их встроить. Это само по себе увеличивает размер бинарного файла. Так еще и на каждую единицу трансляции будет свое определение слабого символа этой сущности. Да, в конце они объединятся, но в объектные файлы все еще будут повторения.

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

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

⛔️ Кожаным труднее читать. Даже с лучшей документацией пользователям библиотеки часто приходится прибегать к чтению заголовков библиотеки. Заголовки в хэдэр-онли либах заполнены деталями реализации, которые мешают пониманию интерфейса. С скомпилированной библиотекой все, что вы видите, это интерфейс и краткий комментарий о том, что делает реализация, и это обычно все, что вам нужно. Даже так, ничего кроме этого вам не должно быть нужно! Нафиг детали реализации, концентрируемся на интерфейсе.

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

Make the right choice. Stay cool.
👍255😁5🔥21
​​Как header only либы обходят ODR
#новичкам

В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.

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

Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.

В чем подвох?

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

Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.

А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .

В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.

Bypass the rules. Stay cool.

#compiler #design
🔥19👍104👏1
Смешиваем std::visit и std::apply
#опытным

Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.

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

Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.

template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};

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

int main(){
std::array<var_t, 3> arr = {1.5, 42, "Hello"};
Visitor vis{[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } };

std::apply([&](auto&&... args){(std::visit(vis, std::forward<decltype(args)>(args)), ...);}, arr);
}


Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.

В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.

Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.

В общем, делаем себе укол пары кубиков метапрограммирования.

Выглядит клёво!

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

Look cool. Stay cool.

#template #cpp17
👍20🔥11😁1141