Проверяем вхождение элемента в ассоциативный контейнер
Нужно вот нам по ключу проверить вхождение элемента допустим в мапу.
Обычно мы пишем:
Но для контейнеров без приставки "multi" это выглядит довольно странно. Действительно, если я знаю, что в мапе однозначное соответствие ключа и значения, зачем мне знать сколько вхождений элементов с этим ключом? Я хочу просто знать, есть ли он.
Такие вот маленькие семантические несостыковочки. С ними вроде все смирились, но осадочек остался...
И 20-е плюсы наконец нам подарили замечательный публичный метод для всех ассоциативных контейнеров
И вот уже стало чуть приятнее и понятнее читать код.
Если есть доступ к 20-м плюсам, то переходите на использование этого метода.
Make things clearer. Stay cool.
#STL #cpp20
Нужно вот нам по ключу проверить вхождение элемента допустим в мапу.
Обычно мы пишем:
if (map.count(key)) {
// do something
}
Но для контейнеров без приставки "multi" это выглядит довольно странно. Действительно, если я знаю, что в мапе однозначное соответствие ключа и значения, зачем мне знать сколько вхождений элементов с этим ключом? Я хочу просто знать, есть ли он.
Такие вот маленькие семантические несостыковочки. С ними вроде все смирились, но осадочек остался...
И 20-е плюсы наконец нам подарили замечательный публичный метод для всех ассоциативных контейнеров
contains
. Он проверяет, если ли в контейнере элементы с данным ключом. Теперь код выглядит так:if (map.contains(key)) {
// do something
}
И вот уже стало чуть приятнее и понятнее читать код.
Если есть доступ к 20-м плюсам, то переходите на использование этого метода.
Make things clearer. Stay cool.
#STL #cpp20
std::jthread
С std::thread в С++ есть один интересный и возможно назойливый нюанс. Давайте посмотрим на код:
Простой Хелло Ворлд в другом потоке. Но при запуске программы она тут же завершится примерно с таким сообщением:
Эм. "Я же просто хотелбыть счастливым вывести в другом потоке сообщение. Неужели просто так не работает?"
В плюсах много чего просто так не работает)
А вот такая программа:
Все-таки напечатает Hello, World!, но потом все равно завершится с std::terminate.
Уже лучше, но осадочек остался. Ничего не понятно. Давайте разбираться.
С помощью слипа мы немного затормозили main тред и сообщение появилось. То есть мы оттянули выход из main и завершение программы и дали возможность подольше поработать новосозданному потоку. А что происходит при выходе из скоупа функции? Вызов деструкторов локальных объектов.
Так вот в деструкторе единственного локального объекта и проблема. Согласно документации, для каждого потока мы обязательно должны выбрать одну из 2-х стратегий поведения: отсоединить его от родительского потока или дождаться его завершения. Делается это методами detach и join соответственно.
И если мы не вызовем один из этих методов для объекта потока, то в своем деструкторе он вызовет std::terminate. То есть корректный код выглядит так:
Мы дожидаемся конца исполнения потока и только после этого завершаем программу. Теперь никаких терминаторов.
Но зачем эти формальности? Вообще говоря, часто мы хотим присоединить поток почти сразу перед вызовом его деструктора. А вот отсоединяем поток мы почти сразу после создания объекта. Мы же заранее знаем, хотим ли мы отпустить поток в свободное плавание или нет. И, учитывая эти факты, было бы приятно иметь возможность не вызывать join самостоятельно, а чтобы он за нас вызывался в деструкторе.
И С++20 приходит здесь нам на помощь с помощью std::jthread. Он делает ровно это. Если его не освободили и не присоединили мануально, то он присоединяется в деструкторе.
Поэтому такой код сделает то, что мы ожидаем:
jthread не только этим хорош. Его исполнение можно еще отменять/приостанавливать. Но об этом уже в другой раз.
Кстати. Вопрос на засыпку. Слышал, что там какие-то сложности у кланга с jthread были. Сейчас все нормально работает?
Make life easier. Stay cool.
#cpp20 #concurrency
С 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
Получаем адрес стандартной функции
Иногда людям приходится работать с указателями на функции. И у них может возникнуть надобность взять адрес у стдшной функции. Например, вы хотите как-то трансформировать каждый символ в строке c помощью std::transform и в качестве коллбэка передаете именно указатель на функцию
Вроде ничего страшного не должно случиться. Но согласно стандарту, поведение программы в этом случае unspecified и потенциально даже ill-formed.
Все потому что нельзя брать адреса у стандартных функций. Начиная с С++20 это явно прописано в стандарте.
Нельзя формировать указатели и ссылки на стандартные функции и нестатические методы.
Но почему? Чем они отличаются от обычных функций?
На самом деле ничем. Дело в том, что будет с функциями в будущем.
Вы без проблем получите адрес одиночной функции. Но если вы добавите к ней перегрузку, то у вас все резко перестанет компилироваться, потому что компилятор не сможет понять, какую конкретно вы используете перегрузку.
А стандарт - это штука не статичная. В него постоянно добавляются новые фичи и обновляются в том числе старые инструменты. Например, с приходом С++11 у нас появилась мув-семантика. И условный метод вектора push_back обзавелся новой перегрузкой для правой ссылки. И это сломало код, который брал адрес метода push_back.
Этот код успешно собирается на 98 плюсах, но не может этого сделать на 11-м стандарте. Можете поиграться с примером на годболте.
Если вам все равно нужно сделать что-то подобное, то оберните вызов стандартной функции в лямбду. Тогда вы ничего не нарушите.
Don't break your future. Stay cool.
#cppcore #cpp20
Иногда людям приходится работать с указателями на функции. И у них может возникнуть надобность взять адрес у стдшной функции. Например, вы хотите как-то трансформировать каждый символ в строке 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
Доступ к элементам многомерных структур
#опытным
Если вы спросите разработчиков C++ о том, как они получают доступ к элементам многомерных массивов, скорее всего, вы получите несколько различных ответов в зависимости от их опыта.
Если вы спросите кого-то, кто не очень опытен или работает в нематематической области, есть большая вероятность, что ответ будет таким, что вы должны использовать несколько операторов[] подряд: myMatrix[x][y].
Есть несколько проблем с таким подходом:
⛔️ Это не очень удобно чисто внешне. Все номальные люди используют синтаксис [x, y].
⛔️ Это работает "из коробки" на реально многомерных структурах, типа вложенных массивов(типа вектора векторов). Чтобы поддержать даже такой синтаксис для кастомных классов, придется несколько приседать.
⛔️ Поэтому многие находят лазейки, чтобы делать что-то похожее на [x, y], этих лазеек много, нет какого-то стандарта.
⛔️ Стандарт использует operator[] с одним аргументом для получения доступа к элементам массивов.
⛔️ Лазейки неконсистентны с одноразмерными массивами в плане получения доступа к элементам.
⛔️ Некоторые из них преполагают спорную семантику, а некоторые делают практически нечитаемыми сообщения об ошибках компиляции.
⛔️ Возможные проблемы с инлайнингом.
Рассмотрим лазейки в будущем, а сейчас сфокусируемся на решении проблемы.
В С++23 наконец завезли многоаргументный operator[]. Теперь при проектировании своей матрицы или даже тензора перегружать оператор[] для 1, 2, 3 и более входных аргументов. Так для матрицы можно возвращать элемент, если мы передали 2 размерности, или возвращать всю строку, если мы передали только одну размерность.
Здесь мы создали матрицу 4х3, заполнили ее буквами алфавита и вывели на экран каждый элемент через matrix[x, y]. А также дальше получили целую строку матрицы через matrix[x] и вывели ее содержимое на экран:
В качестве обертки для строки используем std::span из С++20.
Очень красиво и удобно. Разработчикам математических библиотек сделали большой подарок.
Be consistent. Stay cool.
#cpp23 #cpp20
#опытным
Если вы спросите разработчиков C++ о том, как они получают доступ к элементам многомерных массивов, скорее всего, вы получите несколько различных ответов в зависимости от их опыта.
Если вы спросите кого-то, кто не очень опытен или работает в нематематической области, есть большая вероятность, что ответ будет таким, что вы должны использовать несколько операторов[] подряд: myMatrix[x][y].
Есть несколько проблем с таким подходом:
⛔️ Это не очень удобно чисто внешне. Все номальные люди используют синтаксис [x, y].
⛔️ Это работает "из коробки" на реально многомерных структурах, типа вложенных массивов(типа вектора векторов). Чтобы поддержать даже такой синтаксис для кастомных классов, придется несколько приседать.
⛔️ Поэтому многие находят лазейки, чтобы делать что-то похожее на [x, y], этих лазеек много, нет какого-то стандарта.
⛔️ Стандарт использует operator[] с одним аргументом для получения доступа к элементам массивов.
⛔️ Лазейки неконсистентны с одноразмерными массивами в плане получения доступа к элементам.
⛔️ Некоторые из них преполагают спорную семантику, а некоторые делают практически нечитаемыми сообщения об ошибках компиляции.
⛔️ Возможные проблемы с инлайнингом.
Рассмотрим лазейки в будущем, а сейчас сфокусируемся на решении проблемы.
В С++23 наконец завезли многоаргументный operator[]. Теперь при проектировании своей матрицы или даже тензора перегружать оператор[] для 1, 2, 3 и более входных аргументов. Так для матрицы можно возвращать элемент, если мы передали 2 размерности, или возвращать всю строку, если мы передали только одну размерность.
template <typename T, std::size_t ROWS, std::size_t COLS>
class Martrix {
std::array<T, ROWS * COLS> a;
public:
Martrix() = default;
Martrix(Martrix const&) = default;
constexpr T& operator[](std::size_t row, std::size_t col) { // C++23 required
assert(row < ROWS and col < COLS);
return a[COLS * row + col];
}
constexpr std::span<T> operator[](std::size_t row) {
assert(row < ROWS);
return std::span{a.data() + row * COLS, COLS};
}
constexpr auto& underlying_array() { return a; }
};
int main() {
constexpr size_t ROWS = 4;
constexpr size_t COLS = 3;
Martrix<char, ROWS, COLS> matrix;
// fill in the underlying 1D array
auto& arr = matrix.underlying_array();
std::iota(arr.begin(), arr.end(), 'A');
for (auto row {0U}; row < ROWS; ++row) {
std::cout << "│ ";
for (auto col {0U}; col < COLS; ++col) {
std::cout << matrix[row, col] << " │ ";
}
std::cout << "\n";
}
std::cout << "\n";
auto row = matrix[1];
for (auto col {0U}; col < COLS; ++col) {
std::cout << row[col] << ' ';
}
}
Здесь мы создали матрицу 4х3, заполнили ее буквами алфавита и вывели на экран каждый элемент через matrix[x, y]. А также дальше получили целую строку матрицы через matrix[x] и вывели ее содержимое на экран:
│ A │ B │ C │
│ D │ E │ F │
│ G │ H │ I │
│ J │ K │ L │
D E F
В качестве обертки для строки используем std::span из С++20.
Очень красиво и удобно. Разработчикам математических библиотек сделали большой подарок.
Be consistent. Stay cool.
#cpp23 #cpp20
Оператор запятая внутри operator[]
Много народу попалось на ловушку запятой и выбрали неправильный ответ. Все потому что это поведение реально не интуитивное и реально легко ошибиться в семантике.
Именно поэтому начиная с С++20 использование оператора запятая внутри оператора квадратные скобки признано устаревшим(deprecated).
Это значит, что компиляторы будут обязаны выдавать предупреждение на такое использование, что делает детектирование опасной ситуации более простой задачей. Причем предупреждения появляются безо всяких флагов, наподобие Wall.
Итого:
Можно использовать оборачивание в круглые скобки и это будет валидным выражением с точки зрения стандарта. Но так намного проще будет опознать что-то неладное, чем без круглых скобок.
Кроме того, скорее всего уже были намеки, что в 23-х плюсах появится мультипараметрический интерфейс для operator[]. Поэтому стандарт заранее позаботился о том, чтобы был всего одна разрешенная семантика использования нескольких параметров в этом операторе.
Remove error-prone things. Stay cool.
#cpp20 #cpp23
Много народу попалось на ловушку запятой и выбрали неправильный ответ. Все потому что это поведение реально не интуитивное и реально легко ошибиться в семантике.
Именно поэтому начиная с С++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
Допотопный доступ к многомерному массиву Ч1
#опытным
Начнем рассказ о том, как люди до С++23(то есть до сих пор) жили с оператором[], принимающим только один параметр.
И начнем мы с банальщины. Вот у нас есть класс матрицы. По всем канонам С++ мы должны получать доступ к ее элементам вот так matrix[i][j]. Этот формат сохраняет констистентность с доступом к элементам одномерных массивов.
Однако сразу натыкаемся на проблему. Класс один, а вызываем мы оператор два раза. Несостыковочка.
Ее решает паттерн прокси. Мы создаем прокси класс и возвращаем его объект из первого индекса. Дальше у этого прокси класса определяем оператор[] и на выходе получаем наш элемент.
Условно, из первого оператора возвращаем ссылку на строку матрицы, а из второго - уже сам элемент.
Выглядит это примерно так:
В примере мы 2 раза проходимся по матрице, чтобы продемонстрировать возможность индексации элементов через объект самой матрицы и объекта строки.
Можно было бы конечно не писать отдельно наш прокси тип ArraySpan, а использовать готовый std::span из С++20, но оставим так. Идея использовать такой легковесный объект понятна - нам не нужно лишнего оверхеда на копирование или создание сложного объекта, чтобы просто получить доступ к элементу матрицы.
Ну и здесь мы можем использовать прокси тип в качестве реальной строки матрицы, и не скрывать его в кишках класса. Так мы получаем доступ к большей вариативности в оперировании матрицами. Например, можно делать скалярное произведение строк и прочее.
В чем недостаток такого способа? Куча прокси классов, которые возможно и не нужны. Возможно, нам просто нужна двумерная структура, чтобы получать доступ к конкретным элементам. Использование карты для морского боя не предполагает использование отдельных строк или столбцов. Хотелось бы просто индексировать конкретные элементы. Но тем не менее мы вынуждены использовать прокси класс.
А если структура трухмерная? Уже 2 прокси нужно будет. Больше вложенность - больше классов-прослоек. Не очень удобно. Поэтому и придумали другие способы, о которых расскажу в следующих постах.
Find the way out. Stay cool.
#cppcore #cpp20 #cpp23
#опытным
Начнем рассказ о том, как люди до С++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
Фиксим проблему с конструированием POD типов
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
Теперь можно использовать все преимущества метода emplace_back и писать такой код:
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
Enjoy small things. Stay cool.
#cppcore #cpp20
#опытным
В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!
Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!
Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
struct A {
int a;
int&& r;
};
int f();
int n = 10;
A a1{1, f()}; // OK, lifetime is extended
A a2(1, f()); // well-formed, but dangling reference
A a3{1.0, 1}; // error: narrowing conversion
A a4(1.0, 1); // well-formed, but dangling reference
A a5(1.0, std::move(n)); // OK
Теперь можно использовать все преимущества метода emplace_back и писать такой код:
std::vector<Point> vec;
vec.emplace_back(1, 2, 3);
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.
Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.
Что было до с++20.
push_back спокойно переваривал следующий код:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект.
Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
vec.emplace_back(Point{1, 2, 3});
Больше букав! А если название структуры длинное? Вот вот.
Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы.
Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!
Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.
Enjoy small things. Stay cool.
#cppcore #cpp20
Когда мы вынуждены явно использовать new
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
Но можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
#опытным
Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.
Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:
👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
auto ptr = std::unique_ptr<int, void()(int*)>(new int(42), [](int* p) {
delete p;
std::cout << "Custom deleter called!\n";
});
👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
struct Class {
static std::unique_ptr<Class> Create() {
// return std::make_unique<Class>(); // It will fail.
return std::unique_ptr<Class>(new Class);
}
private:
Class() {}
};
👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.
То есть вот так нельзя делать в С++17:
struct MyStruct {
int a, b, c;
};
auto ptr = std::make_unique<MyStruct>(1, 2, 3); // Will fail C++17
auto ptr = std::unique_ptr<MyStruct>(new MyStruct{1, 2, 3}); // Norm
Но можно в С++20. Так что тем, кто необновился, придется использовать new.
В целом, все. Если что забыл - накидайте в комменты.
Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги).
Stay safe. Stay cool.
#cppcore #memory #cpp20 #cpp17
auto аргументы функций
#опытным
Проследим историю с возможностью объявлять аргументы функций, как auto.
До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов
Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:
Это круто повысило вариативность лямбд, предоставив им некоторые плюшки шаблонов.
У обычных функции, тем не менее, так и остались обычные шаблонные параметры.
Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:
Если для лямбд это было необходимым решением из-за того, что их не хотели делать шаблонными(хотя в С++20 их уже можно делать такими), то auto параметры обычных функций призваны немного упростить шаблонную логику там, где не нужно использовать непосредственно тип шаблонного параметра. Так сказать, шаблоны на чилле и расслабоне.
Осталось только добавить, что параметры auto работают по принципу выведения типов для шаблонов, а не по принципу выведения типов auto переменных.
История небольшая, но становится понятно, что С++ все больше уходит в неявную типизацию. С одной стороны это хорошо, проще писать код и не задумываться над типами. С другой стороны, чтобы этим пользоваться на высоком уровне, нужно знать всякие маленькие нюансики, которых становится все больше и больше.
Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно.
Hide unused details. Stay cool.
#cpp11 #cpp14 #cpp20 #template
#опытным
Проследим историю с возможностью объявлять аргументы функций, как auto.
До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов
Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:
auto print = [](auto& x){std::cout << x << std::endl;};
print(42);
print(3.14);
Это круто повысило вариативность лямбд, предоставив им некоторые плюшки шаблонов.
У обычных функции, тем не менее, так и остались обычные шаблонные параметры.
Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:
void sum(auto a, auto b)
{
auto result = a + b;
std::cout << a << " + " << b << " = " << result << std::endl;
}
sum(1, 3);
sum(3.14, 42);
sum(std::string("123"), std::string("456));
// OUTPUT:
// 1 + 3 = 4
// 3.14 + 42 = 45.14
// 123 + 456 = 123456
Если для лямбд это было необходимым решением из-за того, что их не хотели делать шаблонными(хотя в С++20 их уже можно делать такими), то auto параметры обычных функций призваны немного упростить шаблонную логику там, где не нужно использовать непосредственно тип шаблонного параметра. Так сказать, шаблоны на чилле и расслабоне.
Осталось только добавить, что параметры auto работают по принципу выведения типов для шаблонов, а не по принципу выведения типов auto переменных.
История небольшая, но становится понятно, что С++ все больше уходит в неявную типизацию. С одной стороны это хорошо, проще писать код и не задумываться над типами. С другой стороны, чтобы этим пользоваться на высоком уровне, нужно знать всякие маленькие нюансики, которых становится все больше и больше.
Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно.
Hide unused details. Stay cool.
#cpp11 #cpp14 #cpp20 #template
Сравниваем производительности оператора<
#опытным
В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .
Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.
Сравним 4 реализации operator<:
Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.
Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:
Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.
В конце мы запускаем бенчмарк над функцией с измерением времени выполнения, говорим ему, что мы сами будет мерять время(UseManualTime), и сколько итераций цикла нужно выполнить(Iterations(20)).
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Compare things. Stay cool.
#performance #cpp20
#опытным
В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .
Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.
Сравним 4 реализации operator<:
struct Time_comparison_unreadable {
int hours;
int minutes;
int seconds;
bool operator<(const Time_comparison_unreadable& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes) || (hours == other.hours && minutes == other.minutes && seconds < other.seconds))
return true;
else
return false;
}
};
struct Time_comparison_readable {
// fields
bool operator<(const Time_comparison_readable& other) {
if (hours < other.hours) return true;
if (hours > other.hours) return false;
if (minutes < other.minutes) return true;
if (minutes > other.minutes) return false;
if (seconds < other.seconds) return true;
return false;
}
};
struct Time_tie {
// fields
bool operator<(const Time_tie& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};
struct Time_spaceship {
// fields
auto operator<=>(const Time_spaceship &) const = default;
};
Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.
Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:
std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(1,100);
template <typename TimeClass>
static void time_comparison_experiment(benchmark::State& state) {
std::vector<TimeClass> v(1'000'000);
std::generate(v.begin(), v.end(), [&] () -> TimeClass { return TimeClass{ dist(rng) % 24, dist(rng) % 60, dist(rng) % 60 }; });
while (state.KeepRunning()) {
auto start = std::chrono::high_resolution_clock::now();
std::sort(v.begin(), v.end());
auto end = std::chrono::high_resolution_clock::now();
auto elapsed_seconds =
std::chrono::duration_cast<std::chrono::duration<double>>(
end - start);
state.SetIterationTime(elapsed_seconds.count());
std::shuffle(v.begin(), v.end(), rng);
}
}
BENCHMARK(time_comparison_experiment<Time_comparison_unreadable>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_comparison_readable>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_tie>)->UseManualTime()->Iterations(20);
BENCHMARK(time_comparison_experiment<Time_spaceship>)->UseManualTime()->Iterations(20);
Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.
В конце мы запускаем бенчмарк над функцией с измерением времени выполнения, говорим ему, что мы сами будет мерять время(UseManualTime), и сколько итераций цикла нужно выполнить(Iterations(20)).
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Compare things. Stay cool.
#performance #cpp20
Telegram
Грокаем C++
Удобно сравниваем объекты
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
struct Time {
int…
#опытным
Иногда нам нужно сортировать объекты кастомных классов. Для этого нам нужно определить оператор<, чтобы объекты могли сравниваться друг с другом. Давайте попробуем это сделать для простой структуры:
struct Time {
int…
Виртуальные функции в compile-time
#опытным
Виртуальные функции являются средством реализации динамического полиморфизма в С++. Почему он вообще называется динамическим?
Да потому что выбор конкретной реализации происходит в рантайме, а не во время компиляции.
Но что, если я вам скажу, что мы можем реализовывать полиморфизм времени компиляции с помощью виртуальных функций?
Выглядит как оксюморон, но подождите кидаться грязными тряпками. Сейчас все разберем.
Начиная с С++11 у нас есть constexpr функции. Эти функции могут быть вычислены на этапе компиляции, если их аргументы также известны на этом этапе. Аргументы могут быть константами, литералами, constexpr переменными или результатом вычисления других constexpr функций.
В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.
Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.
Начиная с С++20 constexpr функции могут быть виртуальными!
Все как мы привыкли: делаем иерархию классов с виртуальной функцией, только везде на всех этапах приписываем constexpr. И это работает!
А где это может быть использовано, посмотрим в следующий раз.
Increase your usability. Stay cool.
#cpp11 #cpp20 #cppcore
#опытным
Виртуальные функции являются средством реализации динамического полиморфизма в С++. Почему он вообще называется динамическим?
Да потому что выбор конкретной реализации происходит в рантайме, а не во время компиляции.
Но что, если я вам скажу, что мы можем реализовывать полиморфизм времени компиляции с помощью виртуальных функций?
Выглядит как оксюморон, но подождите кидаться грязными тряпками. Сейчас все разберем.
Начиная с С++11 у нас есть constexpr функции. Эти функции могут быть вычислены на этапе компиляции, если их аргументы также известны на этом этапе. Аргументы могут быть константами, литералами, constexpr переменными или результатом вычисления других constexpr функций.
constexpr int double_me(int n)
{
return n * 2;
}
// условие верное и мы не падаем
static_assert(double_me(4) == 8);
// условие ложно и компиляция прервется на этой строчке
static_assert(double_me(4) == 7);
В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.
Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.
Начиная с С++20 constexpr функции могут быть виртуальными!
struct VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const = 0;
};
struct Impl: VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const override
{
return 2 * n;
}
};
constexpr auto impl = Impl{};
// для полиморфизма с виртуальными функциями нужна ссылка
constexpr const VeryComplicatedCaclulation& impl_ref = impl;
constexpr auto a = impl_ref.double_me(4);
static_assert(a == 8); // true
Все как мы привыкли: делаем иерархию классов с виртуальной функцией, только везде на всех этапах приписываем constexpr. И это работает!
А где это может быть использовано, посмотрим в следующий раз.
Increase your usability. Stay cool.
#cpp11 #cpp20 #cppcore
Виртуальные функции в compile-time Ч2
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
#опытным
Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?
constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:
В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:
class error_code
{
private:
int val_;
const error_category* cat_;
public:
constexpr error_code() noexcept;
constexpr error_code(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code(ErrorCodeEnum e) noexcept;
constexpr void assign(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
constexpr error_code& operator=(ErrorCodeEnum e) noexcept;
constexpr void clear() noexcept;
constexpr int value() const noexcept;
constexpr const error_category& category() const noexcept;
constexpr explicit operator bool() const noexcept;
error_condition default_error_condition() const noexcept;
string message() const;
};
Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс
error_category*
, наследникам которого мы и можем делигировать принятие решения о том, является ли значение ошибкой или нет.class error_category
{
public:
// ...
virtual bool failed(int ev) const noexcept;
// ...
};
// И добавляем метод в класс error_code
class error_code
{
// ...
bool failed() const noexcept { return cat_->failed(val_); }
// ...
};
Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!
Также шаблоны - конкуренты выртуальных функций - имеют одну противную особенность. Глаза хочется выкинуть, когда видишь шаблонный код. Виртуальные функции compile time'а могут в определенных кейсах заменить шаблоны и помочь увеличить читаемость кода.
Не стоит забывать и про кодогенерацию. С ее помощью мы можем включать в код по сути все, что мы хотим. Можно хоть из файла конфигурации сгенерить хэдэр, в котором будет переменная, содержащая весь этот конфиг. Для разных, но все же похожих, сгенерированных сущностей могут быть нужны полиморфные обработчики. Вот здесь отлично вписываются виртуальные constexpr функции.
Самому мне еще не удавалось их применять. Однако у нас в канале очень много крутых спецов. Если у вас был опыт использования этой фичи - поделитесь в комментах.
Increase your usability. Stay cool.
#cpp20
Еще одно отличие С и С++
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
В плюсах такое можно сделать только с помощью нескольких инструкций.
Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
#опытным
Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.
В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!
Например, хочу я определить разреженный массив из 100 элементов и только 3 их них я хочу инициализировать единичками. Не проблема! В С это можно сделать одной строчкой:
int array[100] = {[13] = 1, [45] = 1, [79] = 1};
В плюсах такое можно сделать только с помощью нескольких инструкций.
int array[100] = {};
array[13] = array[45] = array[79] = 1;
Не так удобно.
Можно даже задавать рэндж значений. Но это правда GNU расширение.
int array[100] = {[13] = 1, [30 ... 40] = 1, [45] = 1, [79] = 1};
Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!
Для структур задавать значения полям можно вот так:
struct point { int x, y, z; };
struct point p1 = { .y = 2, .x = 3 };
struct point p2 = { y: 2, x: 3 };
struct point p3 = { x: 1};
Нужно обязательно при инициализации указать конкретное поле, которому будет присвоено значение. При чем порядок указания полей неважен! А неупомянутые поля будут инициализированы нулем.
До С++20 в плюсах вообще не было подобного синтаксиса. Начиная с 20-х плюсов при создании объекта класса мы можем аннотировать, каким полям мы присваиваем значение. Но в плюсах намного больше ограничений: поля нужно указывать в порядке объявления в теле класса, никакой инициализации массивов и еще куча тонкостей.
Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?
Be different. Stay cool.
#goodoldc #cppcore #cpp20 #interview
Designated initialization
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
Хоть
Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
Несмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
#новичкам
В продолжение предыдущего поста, почему бы нам не поговорить о том, что такое designated initialization в контексте С++ и какие особенности она имеет в языке.
Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.
struct Person {
std::string name;
std::string surname;
std::string id;
};
struct Item {
std::string name;
double price;
std::string id;
};
struct Order {
Person person;
Item purchase;
std::string pick_up_address;
};
Пусть у нас есть заказ, который состоит из данных о человеке, который заказал товар, и самого заказанного товара. Мы хотим распарсить входящий запрос от клиента и сформировать структуру Order для дальнейшей обработки. Теперь мы можем сделать это очень просто и почти играючи.
Order order{.person = {.name = "Golum",
.surname = "Iz shira",
.id = "666"},
.purchase = {.name = "Precious",
.price = 9999999.9,
.id = "13"},
.pick_up_address = "Mordor"};
То, как мы указываем каждый член структуры и присваиваем ему значение - и есть designated initialization. Собственно пример показывает всю прелесть фичи. Теперь по коду явно видно, каким полям какое значение присваивается. И даже вложенность поддерживается. Это сильно повышает читаемость и понимание происходящего.
Если хотите использовать наследование, то синтаксис такой:
struct Person
{
std::string name;
std::string surname;
unsigned age;
};
struct Employee : Person
{
unsigned salary;
};
Employee e1{ { .name{"John"}, .surname{"Wick"}, .age{40} }, 50000 };
Так как у полей родителького класса нет какого-то имени, то используются просто вложенные скобки.
А еще вы можете пропускать любые поля и они будут инициализированны по умолчанию! Давно не хватало такой возможности:
struct Point{
int x, y, z;
};
Point p{.x = 2, .z = 3}; // y is not mentioned, but it will have value of 0
Хоть
y
строит в середине, но это не мешает нам не указывать его при создании класса и это поле гарантированно будет равно 0.Правда у фичи есть определенные ограничения:
👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:
struct Point{
int x, y;
};
Point p{.y = 2, .x = 3}; // not valid in C++!
Почему бы не сделать так же, как в С? Дело в том, что в С нет деструкторов. А в С++ есть. И поля класса инициализируются в порядке их появления в объявлении класса, а уничтожаются - в обратном.
Программист может подумать, что раз я указываю какое-то поле первым в инициализации, то и значение ему будет присвоено в первую очередь. Но это не так. А учитывая, что инициализаторы могут иметь какие-то спецэффекты, например, как-то зависеть друг от друга, это может приводить к путанице.
👉🏿 Структуры должны быть POD типами, то есть вот такими же структурами без каких-либо конструкторов и специальных методов. Объекты с конструкторами должны создаваться через онные, а не напрямую. Ну это собственно просто ограничения аггрегированной инициализации, через которую и реализованы designated инициализаторы.
👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:
struct Point{
int x, y;
};
Point p{2, .y = 3}; // Not allowed
Несмотря на все ограничения, они мне кажутся вполне оправданными, а сама фича вообще супергуд. Пользуйтесь, это сильно повысит читаемость кода.
Have a clear intentions. Stay cool.
#cpp20 #cppcore
Тип возвращаемого значения тернарного оператора
#опытным
Представьте, что вам пришел какой-то запрос с json'ом и вам его нужно переложить в плюсовую структуру и дальше как-то ее обрабатывать. В джейсоне записаны какие-то персональные данные человека, но они не всегда присутствуют в полном составе. Давайте посмотрим на структуру, чтобы было понятнее:
Пусть мы обрабатываем какие-то анкетные данные или что-то в таком духе. И человеку обязательно указать свои ФИО, но адрес, эл. почту и телефон - не обязательно.
Берем джейсон и перекладываем(то есть занимаемся тем, чему 6 лет учат в тех вузах):
Просто, чтобы кучу if'ов не плодить, воспользуемся тернарным оператом. Если в json'е есть данное поле, то инициализируем опциональное поле им, если нет, то std::nullopt'ом.
Ничего криминального не сделали. Вдобавок использовали designated initialization из с++20.
Компилируем ииииииии..... Ошибка компиляции.
Пишет, что тернарный оператор не может возвращать разные типы.
Дело в том, что std::nullopt - это константа типа nullopt_t. А поле джейсона имеет тип строки. Конечно, из обоих типов можно сконструировать объект std::optional. Но тернарный оператор не знает, что мы хотим. Ему просто не разрешается возвращать разные типы.
Но почему? Это же так удобно.
С++ - это не всегда про удобство)
Представьте себе шаблонную функцию, возвращаемый тип которой выводит сам компилятор. Условно - try_stoi. Если строка может быть преобразована в int, то возвращаем число, если нет - то возвращаем нетронутую строку.
Разные ветки возвращают неконвертируемые друг в друга типы, поэтому компилятор не сможет вывести единый тип и произойдет ошибка компиляции. Если же явно проставить тип возвращаемого значения std::optional, то все заработает.
Однако для тернарного оператора мы не можем определить тип возвращаемого значения, за нас это неявно делает компилятор. Поэтому, если две ветки условия возвращают неконвертируемые типы, то мозги компилятора бухнут и он отказывается работать..
Придется явно оборачивать все в std::optional:
Это немного снижает читаемость и привносит кучу повторения кода, но имеем, что имеем.
Be flexible. Stay cool.
#cppcore #cpp17 #cpp20
#опытным
Представьте, что вам пришел какой-то запрос с json'ом и вам его нужно переложить в плюсовую структуру и дальше как-то ее обрабатывать. В джейсоне записаны какие-то персональные данные человека, но они не всегда присутствуют в полном составе. Давайте посмотрим на структуру, чтобы было понятнее:
struct PersonalData {
std::string name;
std::string surname;
std::string patronymic;
std::optional<std::string> address;
std::optional<std::string> email;
std::optional<std::string> phone;
};
Пусть мы обрабатываем какие-то анкетные данные или что-то в таком духе. И человеку обязательно указать свои ФИО, но адрес, эл. почту и телефон - не обязательно.
Берем джейсон и перекладываем(то есть занимаемся тем, чему 6 лет учат в тех вузах):
PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? json["address"] : std::nullopt,
.email = json.HasMember("email") ? json["email"] : std::nullopt,
.phone = json.HasMember("phone") ? json["phone"] : std::nullopt};
Просто, чтобы кучу if'ов не плодить, воспользуемся тернарным оператом. Если в json'е есть данное поле, то инициализируем опциональное поле им, если нет, то std::nullopt'ом.
Ничего криминального не сделали. Вдобавок использовали designated initialization из с++20.
Компилируем ииииииии..... Ошибка компиляции.
Пишет, что тернарный оператор не может возвращать разные типы.
Дело в том, что std::nullopt - это константа типа nullopt_t. А поле джейсона имеет тип строки. Конечно, из обоих типов можно сконструировать объект std::optional. Но тернарный оператор не знает, что мы хотим. Ему просто не разрешается возвращать разные типы.
Но почему? Это же так удобно.
С++ - это не всегда про удобство)
Представьте себе шаблонную функцию, возвращаемый тип которой выводит сам компилятор. Условно - try_stoi. Если строка может быть преобразована в int, то возвращаем число, если нет - то возвращаем нетронутую строку.
auto try_stoi(const std::string& potential_num) {
if (can_be_converted_to_int(potential_num)) {
return std::stoi(potential_num);
} else {
return potential_num;
}
}
Разные ветки возвращают неконвертируемые друг в друга типы, поэтому компилятор не сможет вывести единый тип и произойдет ошибка компиляции. Если же явно проставить тип возвращаемого значения std::optional, то все заработает.
Однако для тернарного оператора мы не можем определить тип возвращаемого значения, за нас это неявно делает компилятор. Поэтому, если две ветки условия возвращают неконвертируемые типы, то мозги компилятора бухнут и он отказывается работать..
Придется явно оборачивать все в std::optional:
PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? std::optional(json["address"]) : std::nullopt,
.email = json.HasMember("email") ? std::optional(json["email"]) : std::nullopt,
.phone = json.HasMember("phone") ? std::optional(json["phone"]) : std::nullopt};
Это немного снижает читаемость и привносит кучу повторения кода, но имеем, что имеем.
Be flexible. Stay cool.
#cppcore #cpp17 #cpp20
Что будет если бросить исключение в деструкторе? Ходим по тонкому льду.
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "throw" в деструкторе. Думаю, что на самом деле вы хотите использовать в деструкторе функцию, которая потенциально может бросить исключение:
Логгер может писать в файл с нестандартным именем, но хочет в деструкторе сделать симлинк на него в системной директории /system/logs/log.txt.
Одна проблема - std::filesystem::create_symlink может кинуть исключение, если например прав доступа к директории нет.
Было бы классно определять, находится ли сейчас программа в состоянии разворачивания стека. Если находится, то не делать опасные мувы, а если нет, то гуляй душа.
И такой инструмент есть, называется он std::uncaught_exception(). Это функция проверяет, есть ли сейчас живой объект исключения и исполнение еще не дошло до блока catch. То есть программа находится в режима разворачивания стека. И можно на основе этого знания какую-то логику строить.
Например, не создавать симлинк в логгере, если есть живой объект исключений:
В этом случае мы не будем вызывать опасную функцию, потому что это потенциально может привести к std::terminate.
Исключения - это все-таки исключительные ситуации. При их обработке важно правильно себя повести и сделать хоть что-то, но сохранить работоспособность программы, чем жонглировать ножами и в итоге выткнуть себе глаз.
И std::uncaught_exception() позволяет динамически изменять поведение программы, если вы уже попали в исключительную ситуацию.
Однако в C++17 эта функция призвана устаревшей и была удалена в C++20. В следующий раз посмотрим, почему так и что пришло ей на замену.
Walk on a thin ice. Stay cool.
#cpp17 #cpp20
#опытным
Но что же делать, если у вас есть безудержное желание бросить исключение в деструкторе? Возможно ли это как-то безопасно сделать?
На самом деле есть один вариант. Вряд ли вам хочется прям явно написать "throw" в деструкторе. Думаю, что на самом деле вы хотите использовать в деструкторе функцию, которая потенциально может бросить исключение:
constexpr std::string_view defaultSymlinkPath = "/system/logs/log.txt";
class Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName }
{}
void Log(std::string_view);
~Logger() noexcept(false)
{
fileStream.close();
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
};
Логгер может писать в файл с нестандартным именем, но хочет в деструкторе сделать симлинк на него в системной директории /system/logs/log.txt.
Одна проблема - std::filesystem::create_symlink может кинуть исключение, если например прав доступа к директории нет.
Было бы классно определять, находится ли сейчас программа в состоянии разворачивания стека. Если находится, то не делать опасные мувы, а если нет, то гуляй душа.
И такой инструмент есть, называется он std::uncaught_exception(). Это функция проверяет, есть ли сейчас живой объект исключения и исполнение еще не дошло до блока catch. То есть программа находится в режима разворачивания стека. И можно на основе этого знания какую-то логику строить.
Например, не создавать симлинк в логгере, если есть живой объект исключений:
~Logger()
{
fileStream.close();
if (!std::uncaught_exception())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
В этом случае мы не будем вызывать опасную функцию, потому что это потенциально может привести к std::terminate.
Исключения - это все-таки исключительные ситуации. При их обработке важно правильно себя повести и сделать хоть что-то, но сохранить работоспособность программы, чем жонглировать ножами и в итоге выткнуть себе глаз.
И std::uncaught_exception() позволяет динамически изменять поведение программы, если вы уже попали в исключительную ситуацию.
Однако в C++17 эта функция призвана устаревшей и была удалена в C++20. В следующий раз посмотрим, почему так и что пришло ей на замену.
Walk on a thin ice. Stay cool.
#cpp17 #cpp20
std::midpoint
#новичкам
Простая задача - получить среднее арифметическое двух чисел. Берем и пишем, как на уроке математики:
И дело в шляпе. Или нет?
На самом деле это некорректная реализация, потому что не учитывает переполнение целых чисел. Если сумма (a + b) будет больше, чем помещается в int, то произойдет переполнение, а вы в итоге получите неправильный ответ.
Что же делать?
Если несколько способов обойти эту проблему.
❗️ Складываем половинки двух чисел:
Даже если a и b - максимальные инты, все будет гуд. Проблему с переполнением решили.
Однако здесь появляются проблемы с двойным отбрасыванием остатка от деления. В случае передачи двух нечетных чисел, результат будет неверный:
💥 Первое число складываем с разницей двух чисел:
Если раскрыть скобки, то выходит тоже самое.
И проблем с корректностью нет.
⚡️ std::midpoint. С++20 мы наконец получили стандартную функцию, считающую среднее арифметическое двух объектов. Давайте посмотрим на ее реализацию из gcc:
Не будем вдаваться в подробности, однако стоит заметить, что стандартная функция использует оба подхода в разных ситуациях. Для целых чисел используется второй подход с вычитанием, а для чисел с плавающей точкой - с располовиниваем(так как не теряем остаток) и, даже, оригинальный подход, когда нет риска переполнения.
Да, может быть эта реализация не такая эффективная, зато гарантировано безопасная. Стандарт об этом явно говорит.
К тому же std::midpoint можно использовать для реализации бинарного поиска при нахождении индекса серединного элемента последовательности. Или для реализации алгоритмов «разделяй и властвуй», когда нужно найти индекс элемента, по которому будут разбивать последовательность пополам
В общем, если вы не упарываетесь по перфу, то она станет вашим верным другом.
Stay safe. Stay cool.
#cpp20 #cppcore
#новичкам
Простая задача - получить среднее арифметическое двух чисел. Берем и пишем, как на уроке математики:
int avg(int a, int b) {
return (a + b) / 2;
}
И дело в шляпе. Или нет?
На самом деле это некорректная реализация, потому что не учитывает переполнение целых чисел. Если сумма (a + b) будет больше, чем помещается в int, то произойдет переполнение, а вы в итоге получите неправильный ответ.
Что же делать?
Если несколько способов обойти эту проблему.
❗️ Складываем половинки двух чисел:
int avg(int a, int b) {
return a/2 + b/2;
}
Даже если a и b - максимальные инты, все будет гуд. Проблему с переполнением решили.
Однако здесь появляются проблемы с двойным отбрасыванием остатка от деления. В случае передачи двух нечетных чисел, результат будет неверный:
avg(5, 7) = 5 что неверно
💥 Первое число складываем с разницей двух чисел:
int avg(int a, int b) {
return a > b ? b + (a - b) / 2 : a + (b - a) / 2;
}
Если раскрыть скобки, то выходит тоже самое.
И проблем с корректностью нет.
⚡️ std::midpoint. С++20 мы наконец получили стандартную функцию, считающую среднее арифметическое двух объектов. Давайте посмотрим на ее реализацию из gcc:
// midpoint
#ifdef __cpp_lib_interpolate // C++ >= 20
template<typename _Tp>
constexpr
enable_if_t<__and_v<is_arithmetic<_Tp>, is_same<remove_cv_t<_Tp>, _Tp>,
_not<is_same<_Tp, bool>>>,
_Tp>
midpoint(_Tp __a, _Tp __b) noexcept
{
if constexpr (is_integral_v<_Tp>)
{
using _Up = make_unsigned_t<_Tp>;
int __k = 1;
_Up __m = __a;
_Up __M = __b;
if (__a > __b)
{
__k = -1;
__m = __b;
__M = __a;
}
return __a + __k * _Tp(_Up(__M - __m) / 2);
}
else // is_floating
{
constexpr _Tp __lo = numeric_limits<_Tp>::min() * 2;
constexpr _Tp __hi = numeric_limits<_Tp>::max() / 2;
const _Tp __abs_a = __a < 0 ? -__a : __a;
const _Tp __abs_b = __b < 0 ? -__b : __b;
if (__abs_a <= __hi && __abs_b <= __hi) [[likely]]
return (__a + __b) / 2; // always correctly rounded
if (__abs_a < __lo) // not safe to halve __a
return __a + __b/2;
if (__abs_b < __lo) // not safe to halve __b
return __a/2 + __b;
return __a/2 + __b/2; // otherwise correctly rounded
}
}
template<typename _Tp>
constexpr enable_if_t<is_object_v<_Tp>, _Tp*>
midpoint(_Tp* __a, _Tp* __b) noexcept
{
static_assert( sizeof(_Tp) != 0, "type must be complete" );
return __a + (__b - __a) / 2;
}
#endif // __cpp_lib_interpolate
Не будем вдаваться в подробности, однако стоит заметить, что стандартная функция использует оба подхода в разных ситуациях. Для целых чисел используется второй подход с вычитанием, а для чисел с плавающей точкой - с располовиниваем(так как не теряем остаток) и, даже, оригинальный подход, когда нет риска переполнения.
Да, может быть эта реализация не такая эффективная, зато гарантировано безопасная. Стандарт об этом явно говорит.
К тому же std::midpoint можно использовать для реализации бинарного поиска при нахождении индекса серединного элемента последовательности. Или для реализации алгоритмов «разделяй и властвуй», когда нужно найти индекс элемента, по которому будут разбивать последовательность пополам
В общем, если вы не упарываетесь по перфу, то она станет вашим верным другом.
Stay safe. Stay cool.
#cpp20 #cppcore
Ответ
Мы прям недавно обсуждали, что std::invoke позволяет вызвать указатель на метод класса, это у вас не должно было вызвать вопросов. Самая загвоздка вот тут:
fieldPtr здесь - это указатель на нестатическое поле класса.
Скорее всего у вас возникли такие вопросы: "Что значит вызвать поле класса?! Это вообще легально?".
И ответ на этот вопрос довольно контринтуитивный. Да, std::invoke помогает единообразно вызвать все похожие на функции сущности. То есть запускать на выполнение код. Но поле класса - это вообще говоря не код, а участок памяти. А указатель на поле класса - это оффсет от начала объекта. На функции это вообще не похоже.
Тем не менее вызов поля класса с помощью std::invoke - легальная операция. И ее результат - значение этого поля.
Странно? Безусловно. Есть ли у этого применения? Конечно!
В C++20 алгоритмах библиотеки ranges есть специальный параметр проекции. Это вызываемая сущность, которая помогает преобразовывать элементы диапазона перед обработкой.
Допустим мы хотим найти в диапазоне объект с максимальным значением определенного поля. Как бы мы это делали без диапазонов:
Классика: определяем кастомный компаратор для сравнения элементов. Но заметьте сколько кода повторяется. Не легче ли просто один раз указать, что сравнивать надо по полю amount? И библиотека диапазонов позволяет нам это сделать!
Последний параметр проекции позволяет нам сказать, как нужно преобразовывать элементы последовательности перед сравнениями.
Но это все еще не идеал. Лямбда здесь кажется оверкиллом. Вот здесь-то вызов поля класса и пригождается:
Вот и все. Просто и красиво.
Под капотом алгоритмы рэнджей обязаны использовать std::invoke, чтобы универсально вызывать все переданные коллбэки. Поэтому такой финт ушами работает в них работает. И не работает в привычной STL.
Пользуйтесь диапазонами и проекторами. Это полезные штуки, которые ощутимо упрощают код.
Be laconic. Stay cool.
#cpp20 #STL
Мы прям недавно обсуждали, что std::invoke позволяет вызвать указатель на метод класса, это у вас не должно было вызвать вопросов. Самая загвоздка вот тут:
auto fieldPtr = &Data::field;
std::invoke(fieldPtr, Data{});
fieldPtr здесь - это указатель на нестатическое поле класса.
Скорее всего у вас возникли такие вопросы: "Что значит вызвать поле класса?! Это вообще легально?".
И ответ на этот вопрос довольно контринтуитивный. Да, std::invoke помогает единообразно вызвать все похожие на функции сущности. То есть запускать на выполнение код. Но поле класса - это вообще говоря не код, а участок памяти. А указатель на поле класса - это оффсет от начала объекта. На функции это вообще не похоже.
Тем не менее вызов поля класса с помощью std::invoke - легальная операция. И ее результат - значение этого поля.
Странно? Безусловно. Есть ли у этого применения? Конечно!
В C++20 алгоритмах библиотеки ranges есть специальный параметр проекции. Это вызываемая сущность, которая помогает преобразовывать элементы диапазона перед обработкой.
Допустим мы хотим найти в диапазоне объект с максимальным значением определенного поля. Как бы мы это делали без диапазонов:
struct Payment {
double amount;
std::string category;
}
std::vector<Payment>
payments = {{100.0, "food"}, {200.0, "transport"}, {150.0, "food"},
{300.0, "entertainment"}, {50.0, "transport"}, {250.0, "food"},
{120.0, "food"}};
auto max = *std::max_element(
transactions.begin(), transactions.end(),
[](const auto& item1, const auto& item2) { return item1.amount < item2.amount; });
Классика: определяем кастомный компаратор для сравнения элементов. Но заметьте сколько кода повторяется. Не легче ли просто один раз указать, что сравнивать надо по полю amount? И библиотека диапазонов позволяет нам это сделать!
auto max = *std::ranges::max_element(payments, {}, [](const auto& elem){return elem.amount;});
Последний параметр проекции позволяет нам сказать, как нужно преобразовывать элементы последовательности перед сравнениями.
Но это все еще не идеал. Лямбда здесь кажется оверкиллом. Вот здесь-то вызов поля класса и пригождается:
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);
Вот и все. Просто и красиво.
Под капотом алгоритмы рэнджей обязаны использовать std::invoke, чтобы универсально вызывать все переданные коллбэки. Поэтому такой финт ушами работает в них работает. И не работает в привычной STL.
Пользуйтесь диапазонами и проекторами. Это полезные штуки, которые ощутимо упрощают код.
Be laconic. Stay cool.
#cpp20 #STL
std::exchange
#опытным
В прошлом посте мы пасхалкой использовали std::exchange, давайте же разберем эту функцию по-подробнее.
По названию в целом понятно, что она делает - что-то обменивает. Но не как std::swap, меняет значения местами. Все немного хитрее.
Она заменяет старое значение новым и возвращает старое значение. Вот примерная реализация:
Как говорил Константин Владимиров: "единожды научившись использовать std::exchange, вы дальше будете делать std::exchange всю оставшуюся жизнь".
Понятное дело, что это не какой-нибудь std::move, который реально постоянно приходится использовать. Однако важен сам паттерн. Если посмотреть в кодовые базы, то будет куча мест, где можно использовать эту функцию. Приведу пару примеров работы std::exchange, чтобы вы поняли смысл.
Начнем со знакомого:
Мы определяем мутабельную лямбду(кстати это один из удачных примеров использования таких лямбд), которая может изменять свои захваченные по значению переменные current и step. Дальше на каждом вызове мы должны вернуть текущее значение current, но перед этим как-то его увеличить. Можно использовать прокси-переменную:
Но зачем, если у нас уже есть готовая и протестированная функция std::exchange? Это прекрасный способ немного уменьшить код и увеличить его читаемость.
Другой пример - генерация чисел Фибоначчи:
Хорошенько вдумайтесь и осознайте, что здесь происходит. Ну ведь красиво, правда?)
Если у вас есть вектор коллбэков, который вы постепенно копите и в какой-то момент обрабатываете все разом. Коллбэки безопасно выполнять вне лока. Но как их нормально обработать разом, чтобы 100500 раз не дергать мьютексы? На такой случай есть прикольная техника. Нужно под локом получить копию, а обрабатывать ее вне лока.
И все. Копию(на самом деле перемещенную копию) получаем под локом, а обрабатываем все без замков. Круто!
Ну и конечно, без принципа exchange не обойтись в lock-free программировании. Та же std::atomic_exchange работает ровно по той же логике.
Прикольная функция. Постарайтесь замечать самые банальные кейсы ее применения и со временем вы будет глубже ее понимать и использовать в более интересных ситуациях.
Be elegant. Stay cool.
#cppcore #cpp20 #multitasking #fun
#опытным
В прошлом посте мы пасхалкой использовали std::exchange, давайте же разберем эту функцию по-подробнее.
По названию в целом понятно, что она делает - что-то обменивает. Но не как std::swap, меняет значения местами. Все немного хитрее.
Она заменяет старое значение новым и возвращает старое значение. Вот примерная реализация:
template<class T, class U = T>
constexpr // Since C++20
T exchange(T& obj, U&& new_value) {
T old_value = std::move(obj);
obj = std::forward<U>(new_value);
return old_value;
}
Как говорил Константин Владимиров: "единожды научившись использовать std::exchange, вы дальше будете делать std::exchange всю оставшуюся жизнь".
Понятное дело, что это не какой-нибудь std::move, который реально постоянно приходится использовать. Однако важен сам паттерн. Если посмотреть в кодовые базы, то будет куча мест, где можно использовать эту функцию. Приведу пару примеров работы std::exchange, чтобы вы поняли смысл.
Начнем со знакомого:
auto gen = [current = start, step]() mutable {
return std::exchange(current, current + step);
};
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen);
Мы определяем мутабельную лямбду(кстати это один из удачных примеров использования таких лямбд), которая может изменять свои захваченные по значению переменные current и step. Дальше на каждом вызове мы должны вернуть текущее значение current, но перед этим как-то его увеличить. Можно использовать прокси-переменную:
int val = current;
current += step;
return val;
Но зачем, если у нас уже есть готовая и протестированная функция std::exchange? Это прекрасный способ немного уменьшить код и увеличить его читаемость.
Другой пример - генерация чисел Фибоначчи:
auto gen = [current = 0, next = 1]() mutable {
return current = std::exchange(next, current + next);
};
std::vector<int> fib(10);
std::generate(fib.begin(), fib.end(), gen);
for (int i = 0; i < fib.size(); ++i) {
std::cout << fib[i] << " ";
}
// OUTPUT:
// 1 1 2 3 5 8 13 21 34 55
Хорошенько вдумайтесь и осознайте, что здесь происходит. Ну ведь красиво, правда?)
Если у вас есть вектор коллбэков, который вы постепенно копите и в какой-то момент обрабатываете все разом. Коллбэки безопасно выполнять вне лока. Но как их нормально обработать разом, чтобы 100500 раз не дергать мьютексы? На такой случай есть прикольная техника. Нужно под локом получить копию, а обрабатывать ее вне лока.
class Dispatcher {
// ...
// All events are dispatched when we call process
void process() {
const auto tmp = [&] {
std::lock_guard lock{mutex_};
return std::exchange(callbacks_, {});
}();
for (const auto& callback : tmp) {
std::invoke(callback);
}
}
};
И все. Копию(на самом деле перемещенную копию) получаем под локом, а обрабатываем все без замков. Круто!
Ну и конечно, без принципа exchange не обойтись в lock-free программировании. Та же std::atomic_exchange работает ровно по той же логике.
Прикольная функция. Постарайтесь замечать самые банальные кейсы ее применения и со временем вы будет глубже ее понимать и использовать в более интересных ситуациях.
Be elegant. Stay cool.
#cppcore #cpp20 #multitasking #fun
Telegram
Грокаем C++
Mutable lambdas
#опытным
Лямбда выражения имеют одну интересную особенность. И эта особенность аффектит то, что можно делать внутри лямбды.
Простой пример:
int val = 0;
auto lambda1 = [&val]() { std::cout << ++val << std::endl; };
auto lambda2 = [val]()…
#опытным
Лямбда выражения имеют одну интересную особенность. И эта особенность аффектит то, что можно делать внутри лямбды.
Простой пример:
int val = 0;
auto lambda1 = [&val]() { std::cout << ++val << std::endl; };
auto lambda2 = [val]()…
std::mem_fn
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
Какие варианты у нас есть в такой ситуации?
❗️ std::function
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL
#опытным
Допустим, у вас есть вектор тасок и вам нужно выполнить каждую из них и поместить результаты выполнения в другой вектор:
struct Task {
int execute() {
return 42;
}
};
std::vector<Task> tasks(10);
std::vector<int> results;
Примерно так это может выглядеть в суперупрощенном виде.
Эту задачу легко решить с помощью цикла:
results.reserve(tasks.size());
for (auto& task: tasks) {
result.emplace_back(task.execute());
}
И дело в шляпе.
Однако cppcoreguidelines нам говорят:
Use standard algorithms where appropriate, instead of writing some own implementation.
Может ли мы здесь использовать стандартные алгоритмы? Да, конечно можем. С рэнджами это очень легко:
std::vector results = tasks |
std::views::transform(&Task::execute) |
std::ranges::to<std::vector>();
Мы уже говорили в этом посте, что алгоритмы диапазонов обязаны использовать std::invoke под капотом, поэтому std::views::transform легко переварит указатель на метод.
Но что, если вы живете в эру до С++20? У вас есть стандартный std::transform, однако он уже не такой умный и не умеет принимать указатели на методы.
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), &Task::execute); // Not working!
Какие варианты у нас есть в такой ситуации?
❗️ std::function
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::function<int(Task&)>(&Task::execute));
Works, but at what costs?
std::function в данной ситуации - это бить по воробьям ракетами. std::function обычно работает сильно медленнее, чем прямой вызов. Поэтому давайте посмотрим на других кандидатов.
👍 Использовать лямбду
std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), [](auto& input) { return input.execute(); });
Это работает, но приходится городить огород вокруг execute. Лямбды всегда вносят некоторый "шум" в код из-за чего его сложнее воспринимать. Поэтому это не самый идеальный вариант. Есть что-то получше?
✅ std::mem_fn. Спонсор сегодняшней передачи. std::mem_fn принимает указатель на метод и возвращает тонкую обертку над ним, которая условно позволяет вызывать методы класса не так
(x.*&Item::Foo)()
, а вот так: (&Item::Foo)(x)
. То есть позволяет унифицировать синтаксис вызова указателя на метод класса с синтаксисом вызова обычной функции. С помощью std::mem_fn наш код трансформации выглядит вот так:std::transform(tasks.begin(), tasks.end(),
std::back_inserter(results), std::mem_fn(&Task::execute));
Минимум кода вокруг execute без потери производительности. Кайф!
Отличная функция, которая позволит вашему коду быть выразительным и быстрым.
Express yourself. Stay cool.
#cppcore #cpp20 #STL