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

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Частичная специализация шаблонов функций
#опытным

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

Частичной специализации шаблонов функции не существует. И точка!

То, что называют выдают за нее - это обычная перегрузка шаблонных функций.

Звучит, как пустяковая проблема. Какая разница, как назвать молоток, если им все равно можно забить гвоздь?

Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.

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

void f(int) { std::cout << "int-overload" << std::endl; }; 
void f(int*){ std::cout << "int-p-overload" << std::endl; }

template<class T> void f(T) { std::cout << "T-overload" << std::endl; };
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }


Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.

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


template<typename T>
class Foo {};

template<typename T>
class Foo<T*> {};


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

template <typename T1>
struct Foo<T1> {};
// Так нельзя делать, это несвязанные шаблоны
template <typename T1, typename T2>
struct Foo<T1,T2> {};


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

template<class T> void f(T) { std::cout << "T-overload" << std::endl; }; 
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }

template<class T> void f<T*>(T*){std::cout << "T-p-specialization" << std::endl;}


Ну и как компилятору выбирать между T-p-overload и T-p-specialization?

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

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

Don't be confused. Stay cool.

#template #cppcore
Перегружаем шаблоны классов
#опытным

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

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

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

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

template <typename... T>
struct Foo;

template <typename T1>
struct Foo<T1> {};

template <typename T1, typename T2>
struct Foo<T1,T2> {};


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

Вот такие фокусы.

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

template <typename T, int N>
struct Foo<T, N> {}; // forbidden


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

Hack the boundaries. Stay cool.

#template #cppcore
Дефолтные параметры виртуальных методов
#опытным

Как и в обычных функциях и методах класса, в виртуальных методах тоже можно определять параметры по-умолчанию. Однако, они могут вести себя не совсем так, как вы этого ожидаете. Спасибо, @d7d1cd, за идею для поста)

Правильный ответ из поста выше - 0 2 0 0 .

#include <iostream>
struct base {
virtual void foo(int a = 0) { std::cout << a << " "; }
virtual ~base() {}
};

struct derived1 : base {};

struct derived2 : base {
void foo(int a = 2) { std::cout << a << " "; }
};

int main() {
derived1 d1{};
derived2 d2{};
base & bs1 = d1;
base & bs2 = d2;
d1.foo();
d2.foo();
bs1.foo();
bs2.foo();
}


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

То есть, если мы вызываем метод у объекта наследника(или через ссылку, или указатель), то выбирается дефолтное значение метода наследника.

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

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

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

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

Слышится легкое: "Эх, я когда-нибудь смогу выучить плюсы?...".

Don't be confusing. Stay cool.

#cppcore
​​noexcept
#новичкам

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

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

void doSomething() noexcept;


noexcept также принимает опциональный булевый параметр - выражение. В основном это показывают примерно так:

void doSomething() noexcept(true);
void doSomething() noexcept(false);


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

Внимание! Очень важное уточнение. Если мы пометили функцию noexcept, это не значит, что она не бросает исключений! Она может это делать, только будут последствия. При вылете любого исключения из небросающей функции будет вызван std::terminate. И никакие try-catch вам не помогут.

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

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

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

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

Вот где noexcept действительно важен - это специальные методы класса, а конкретно мув-конструктор и перемешающий оператор присваивания. Пометив их noexcept, вы разрешаете вектору при реаллокации перемещать элементы, функции своп менять местами переменные с помощью мув-семантики и многое другое. Стандартная библиотека старается давать сильную гарантию исключений(commit or rollback), поэтому для нее очень важны небросающие перемещающие методы класса.

Stay safe. Stay cool.

#cppcore #cpp11 #STL
std::move_if_noexcept
#опытным

В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.

Вот реализация этой функции в gcc:

template<typename _Tp>
struct __move_if_noexcept_cond
: public _and<_not<is_nothrow_move_constructible<_Tp>>,
is_copy_constructible<_Tp>>::type { };

template<typename _Tp>
[[nodiscard,gnu::always_inline]]
constexpr
__conditional_t<__move_if_noexcept_cond<_Tp>::value, const _Tp&, _Tp&&>
move_if_noexcept(_Tp& __x) noexcept
{ return std::move(__x); }


Две части: условия мува и сам мув.

Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.

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

Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.

Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:

struct NoMove1
{
// prevents implicit declaration of default move constructor;
// however, the class is still move-constructible because its
// copy constructor can bind to an rvalue argument
NoMove1(const NoMove1&) {}
};
static_assert(std::is_move_constructible_v<NoMove1>); // Here
static_assert(!std::is_trivially_move_constructible_v<NoMove1>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove1>);
 
struct NoMove2
{
// Not move-constructible since the lvalue reference
// can't bind to the rvalue argument
NoMove2(NoMove2&) {}
};
static_assert(!std::is_move_constructible_v<NoMove2>); // And here
static_assert(!std::is_trivially_move_constructible_v<NoMove2>);
static_assert(!std::is_nothrow_move_constructible_v<NoMove2>);


То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.

В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.

Live legally. Stay cool.

#template #cppcore #STL
​​Что будет если бросить исключение в деструкторе? Ч1
#новичкам

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

Значит единственный адекватный ответ - вызовется std::terminate. И здесь даже не нужно упоминать никаких double exception. То есть:

struct Class {
~Class() {throw 1;}
};
int main() {
try {
{Class cl;}
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}


В этом случае исключение не поймается, а просто вызовется std::terminate. И точка. Никаких дополнений.

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

Если вы определяете деструктор дефолтным, то он noexcept. И даже если вы определяете кастомный деструктор, но не указываете ему политику исключений, он все равно помечен noexcept.

Однако мы можем сделать деструктор бросающим. Мы должны явно прописывать политику исключений:

struct Class {
// HERE
~Class() noexcept(false) {throw 1;}
};
int main() {
try {
{Class cl;}
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}


Только в этом случае на консоли появится caught exception.

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

Пока вы явно не пометили деструктор бросающим, при вылете исключения из деструктор будет вызван std::terminate.

Be explicit in your intentions. Stay cool.

#cppcore #cpp11 #interview
​​Что будет если бросить исключение в деструкторе? Ч2
#новичкам

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

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

Вот так:

struct Class {
// HERE
~Class() noexcept(false) {throw 1;}
};
int main() {
try {
Class cl;
throw 1.0;
} catch(...) {
std::cout << "caught exception" << std::endl;
}
}


В этом случае при бросании 1.0, исключение увидит блок catch и перед входом в него начнет раскручивать стек, вызывая деструкторы всех локальных объектов во всех фреймах, которые пролетело исключение.
В нашем коде деструктор Class будет вызван до блока catch и получается, что у нас ситуация, в которой есть 2 необработанных исключения. Эта ситуация называется double exception и она приводит к немедленному вызову std::terminate.

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

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

Stay safe. Stay cool.

#cppcore #cpp11 #interview
Что будет если бросить исключение в деструкторе? Уверенно ходим по тонкому льду.
#опытным

std::uncaught_exception() в С++17 заменилась на std::uncaught_exceptions(), которая теперь не просто сообщает тот факт, что программа сейчас находится в состоянии раскрутки стека, но и в принципе, какое количество живых исключений сейчас в программе существует. Чтобы понять, зачем нужно знать количество исключений рассмотрим следующую ситуацию.

constexpr std::string_view defaultSymlinkPath = "system/logs/log.txt";

struct 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();
if (!std::uncaught_exception()) {
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};

struct Calculator
{
int64_t Calc(const std::vector<std::string> &params);
// ....
~Calculator()
{
try {
Logger logger("log.txt");
Logger.Log("Calculator destroyed");
}
catch (...) {
// ....
}
}
};

int64_t Process(const std::vector<std::string> &params) {
try {
Calculator calculator;
return Calculator.Calc(params);
}
catch (...) {
// ....
}
}


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

Из Calc вылетает исключение -> видит блок catch -> запускается раскрутка стека и деструктор калькулятора -> в деструкторе калькулятора создается логгер и записывает сообщение в файл -> при выходе из скоупа вызывается деструктор логгера -> std::uncaught_exception возвращает true и создание симлинка не происходит.

Однако в этом случае вы можете попробовать создать символическую ссылку! Дело в том, что деструктор Logger не будет вызываться непосредственно в результате размотки стека — он будет вызван после создания нового объекта из деструктора калькулятора. Таким образом, вы можете выбросить исключение из деструктора Logger'- вам нужно только поймать это исключение, прежде чем оно выйдет из деструктора калькулятора.

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

Чтобы исправить подобное поведение придумали std::uncaught_exceptions(), которая возвращает количество активных исключений на данный момент. И вот как выглядит логгер с использованием std::uncaught_exceptions():

struct Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
int m_exceptions = std::uncaught_exceptions(); // <=

Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName } {}

void Log(std::string_view);

~Logger() noexcept(false)
{
fileStream.close();
if (m_exceptions == std::uncaught_exceptions())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};


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

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

Помните, что такая возможность есть, но используйте ее в самом крайнем случае.

Walk on a thin ice. Stay cool.

#cpp17 #cppcore
​​decltype(auto) vs auto&&. Вывод типа переменной
#опытным

decltype(auto) и auto&& позволяют нам не беспокоиться о конкретных типах сущностей, включая ref квалификацию. Основные кейсы применения decltype(auto) - это автоматический вывод типа возвращаемого значения функции и переменной на основе инициализатора. Но чем отличаются decltype(auto) и auto&& в контексте этих кейсов? Спасибо @SoulslikeEnjoyer за идею для постов в этом комменте https://t.me/c/2009887601/47858.

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

Чтобы продемонстрировать различия нам нужна вот такая заготовка:

int g = 0;
int foo() {return 42;}
int& foo1() {return g;}
int&& foo2() {int i = 42; return std::move(i);}

auto&& a1 = foo();
auto&& a2 = foo1();
auto&& a3 = foo2();

decltype(auto) b1 = foo();
decltype(auto) b2 = foo1();
decltype(auto) b3 = foo2();


Здесь касательно вывода типа нас интересует, как ссылочность типа возвращаемого значения функции (или ее отсутствие), сказывается на типе переменных.

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

int && a1 = foo();
int & a2 = foo1();
int && a3 = foo2();

int b1 = foo();
int & b2 = foo1();
int && b3 = foo2();


Здесь можно четко увидеть разницу: при возврате из функции не-ref-квалифицированного типа, decltype(auto) в точности передает тип, а вот auto&& определяет тип как правую ссылку.

Проблема ли это?

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

Поэтому в большинстве кейсов при объявлении переменной использование decltype(auto) избыточно, auto&& прекрасно справляется.
Единственное, когда его нужно использовать - в тестах всякого шаблонного непотребства, чтобы получить точный тип возвращаемого значения и сравнить его с образцом.

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

Be laconic. Stay cool.

#cppcore #cpp14
​​decltype(auto) vs auto&&. Вывод типа возвращаемого значения
#опытным

Второе место, в котором целесообразно использовать decltype(auto) и auto&& - это автоматический вывод типа возвращаемого значения функции. Вот тут между ними есть важные различия, которые мы сейчас и обсудим.

decltype(auto) bar() {
int i;
int& ref = i;
return ref;
}

decltype(auto) bar1() {
int i = 42;
return i;
}

decltype(auto) bar2() {
return 42;
}

decltype(auto) bar3() {
int i;
return std::move(i);
}

auto&& baz() {
int i;
int& ref = i;
return ref;
}

auto&& baz1() {
int i = 42;
return i;
}

auto&& baz2() {
return 42;
}

auto&& baz3() {
int i;
return std::move(i);
}


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

int & bar() {
int i;
int & ref = i;
return ref;
}

int bar1() {
int i = 42;
return i;
}

int bar2() {
return 42;
}

typename std::remove_reference<int &>::type && bar3() {
int i;
return std::move(i);
}

int & baz() {
int i;
int & ref = i;
return ref;
}

int & baz1() {
int i = 42;
return i;
}

int && baz2() {
return 42;
}

typename std::remove_reference<int &>::type && baz3() {
int i;
return std::move(i);
}


Единственная разница появляется, когда мы возвращаем из функции lvalue или prvalue объект. В этом случае auto&& выводит тип правой ссылки, а decltype(auto) - обычный бессылочный lvalue объект. И в этом факте вся проблема. Может быть ее не видно сейчас, когда мы возвращаем целочисленный литерал. Но задние ряды уже догадались. Давайте же перейдем к реальной проблеме:

struct CLASS {
CLASS(int i) : num{i} {
std::cout << "ctor " << num << std::endl;
}
~CLASS() {
std::cout << "dtor " << num << std::endl;
}
int num;
};

decltype(auto) get_class_decltype() {
return CLASS(0);
}

auto&& get_class_auto() {
return CLASS(42);
}

int main() {
decltype(auto) a = get_class_decltype();
std::cout << "separator" << std::endl;
decltype(auto) b = get_class_auto();
std::cout << "separator" << std::endl;
}


Делаем простой класс, чтобы отследить его время жизни. И два варианта функции, которые возвращают rvalue объекты. Вот вывод для этого кода:
Вывод:
ctor 0
separator
ctor 42
dtor 42
separator
dtor 0


Здесь видно, что деструктор первого объекта вызывается в самом конце. При присваивании результата функции get_class_decltype() локальному объекту происходит оптимизация copy-elision, благодаря которой объект созданный внутри функции "бесшовно" становится локальным объектом caller'а без всяких мув- и копи- конструкторов.

А вот деструктор второго объекта, который мы планировали возвращать из get_class_auto, вызывается сразу после конструирования. На самом деле сигнатура функции get_class_auto выглядит так:

CLASS&& get_class_auto();


Мы пытаемся вернуть локальный объект функции через правую ссылку. Такое в С++ хоть и скомпилируется, но приведет к висячей ссылке и в последствии к UB при использовании ссылки.

Аналогичное будет происходить, если возвращать lvalue.

Какой вывод?

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

Be safe. Stay cool.

#cppcore
decltype(auto) vs auto&&. Прокси объекты и висячие ссылки.
#опытным

В прошлом посте мы видели, что разница в использовании decltype(auto) и auto&& при выводе типа возвращаемого значения функции проявляется только при возврате объектов, на которые явно не навесили ссылки. Иногда мы можем это отследить глазами и потенциально использовать более простую версию вывода типа. Однако не все мы можем отследить глазами.

Доступ к элементам std::deque всегда возвращает честную левую ссылку. И это почти полностью справедливо для std::vector, кроме одного исключения.

Это std::vector<bool>. Эта специализация вектора возвращает не честную ссылку на объект типа bool, в временный proxy объект. Дело в том, что тип bool занимает как минимум 1 байт, так как это минимально адресуемая ячейка памяти. Но логически он хранит всего 1 бит информации. Если бы мы могли как-то по-хитрому хранить булевы значения, чтобы каждое из них занимало всего 1 бит, то мы бы уменьшили потребление памяти как минимум в 8 раз! Именно это и делается в специализации для bool. Там булевы значения хранятся в виде битов более вместительного типа(int), а для получения доступа к значениям используется proxy объект reference, который неявно приводится bool.

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

template<typename Container, typename Index>
auto&& processAndAccess(Container& c, Index i) {
// do something
// ...
return c[i];
}

std::vector<int> v = {1, 2, 3};
// OK - returns int&
processAndAccess(v, 1) = 3;

std::vector<bool> v2 = {true, false, false};
// NOT OK - returns vector<bool>::reference&& which is a dangling reference
processAndAccess(v2, 1) = true;


Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.


При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.

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

Спасибо @thonease за предоставления исходного кода)

По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).

Be safe. Stay cool.

#cppcore #template
​​Ответ

Поговорим о том, что не так в коде из предыдущего поста:

#include <cstdio>

void bar(char * s) {
printf("%s", s);
}

void foo() {
char s[] =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}

int main() {
foo();
}


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

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

🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.

🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.

Как мог бы выглядеть бы код на современных плюсах?

#include <print>
#include <string_view>

void bar(std::string_view s) {
std::println("{}", s);
}

void foo() {
constexpr std::string_view s =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}

int main() {
foo();
}


Всего 2 простых улучшения:

Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.

Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.

Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.

Believe in magic. Stay cool.

#cppcore #cpp23 #cpp17
std::midpoint
#новичкам

Простая задача - получить среднее арифметическое двух чисел. Берем и пишем, как на уроке математики:

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

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

Давайте посмотрим на пример:

template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::forward<Callback>(callback)(std::forward<Args>(args)...);
}


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

Есть ли проблема в этом коде? Задумайтесь на секунду.

И проблема есть!

Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:

class Data {
public:
void memberFunction(int value) {
std::cout << "Data::memberFunction called with value: " << value << "\n";
}
};

Data data;

// Создаем указатель на метод класса
auto methodPtr = &Data::memberFunction;

// Вызываем метод через указатель на объекте
(data.*methodPtr)(42);


Согласитесь, что этот синтаксис отличается от std::forward<Callback>(callback)(std::forward<Args>(args)...);.

Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?

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

template< class F, class... Args >  
std::invoke_result_t<F, Args...> invoke( F&& f, Args&&... args );


Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::invoke(std::forward<Callback>(callback), std::forward<Args>(args)...);
}


struct S {
void foo(int i) {}
};

process_and_call(&S::foo, S{}, 42);

Прекрасная функция, которая может сильно упростить работу с callback'ами.

Be universal. Stay cool.

#cppcore #template
​​Все вызываемые сущности в С++. Ч1
#новичкам

В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!

👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:

void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}

free_function(42);



👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:

void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}

std::invoke(free_function, 42);


Внутрь invoke free_function передастся как именно как указатель.

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

auto fun_ptr = &free_function;

fun_ptr(42);
(*fun_ptr)(42);

Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:

void free_function(int a) {
std::println("Free function: {}", a);
}

call_callback(free_function, 42);


👉🏿 Ссылки на функции

Да, и такие тоже есть:

auto& fun_ref = free_function;
fun_ptr(42);
(*fun_ptr)(42); // интересно, что такой синтаксис разрешен


Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:

void check_name(auto& obj) {
// by passing free_function to check_name, obj become function reference
}
check_name(free_function);



👉🏿 Статические методы классов

struct Handler {
static void static_method(int a) {
std::cout << "Static method: " << a << std::endl;
}
};

Handler::static_method(42);

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

auto static_method_ptr = &Handler::static_method;
std::invoke(static_method_ptr, 42);


👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.

👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.

class SequenceGenerator {
int current;
int step;
public:
SequenceGenerator(int start = 0, int step_size = 1)
: current(start), step(step_size) {}

// Оператор вызова без аргументов - возвращает следующее число
int operator()() {
int val = current;
current += step;
return val;
}
};
SequenceGenerator gen;
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen); // заполняем вектор с помощью функтора



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

Have all the tools. Stay cool.

#cppcore
Все вызываемые сущности в С++. Ч2
#опытным

Продолжаем перечислять все сущности, которые можно "вызывать" в С++.

И начинаем с продолжения перечисления всего, что относится к функторам:

👉🏿👉🏿 Лямбды

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

int start = 5;
int step = 2;

auto gen = [current = start, step]() mutable {
return std::exchange(current, current + step);
};

std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen);


Накидайте огней, если хотите узнать, как 3 строчки из лямбды в предыдущем посте превратились в одну с помощью std::exchange)

👉🏿👉🏿 Объект класса с определенным оператором приведения к указателю на функцию

Солидная часть подписчиков даже не слышала о такой сущности. Но работает это тривиально: при "вызове" такого объекта он кастится к указателю на функцию и уже с его помощью реально делается вызов:

class FunctionObjectCast {
public:
using fun_ptr = int ()(int);
// Оператор приведения к указателю на функцию
operator fun_ptr() {
return &staticMethod;
}

// Статический метод, который будет вызываться через указатель
static int staticMethod(int x) {
return x * 2;
}
};

FunctionObjectCast obj;
obj(42); // cast here


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

👉🏿👉🏿 Лямбды без захвата. Именно с помощью оператора приведения к указателю на функцию лямбды без захвата можно маскировать под указатели на функции. Это позволяет использовать их в качестве коллбэков к сишным функциям или колдовать такую магию.

std::vector<int> nums = {5, 3, 9, 1};
std::qsort(nums.data(), nums.size(), sizeof(int),
[](const void* a, const void* b) -> int {
return ((int)a - (int)b);});


std::qsort принимает последним параметром именно указатель на функцию, поэтому конкретно в этом кейсе и срабатывает приведение к этому указателю.

В случае привычного вызова самой лямбды используется operator().

👉🏿 Нестатический метод

struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};

Handler{}.foo(42);


👉🏿 Указатель на нестатический метод. Если вы хотите передать метод класса в качестве коллбэка, то вам необходимо это делать через указатель на метод. Метод класса - это такая же функция, только первым параметром она неявно принимает this. Если делать явный вызов метода через указатель на него, то эта неявная передача становится явной:

struct Handler {
void foo(int a) {
std::cout << "Non-static method: " << a << std::endl;
}
};

auto non_static_method_ptr = &Handler::foo;
std::invoke(non_static_method_ptr, Handler{}, 42);


Чтобы вызвать метод класса по его указателю через std::invoke нужно вторым параметром передать объект класса, на котором мы хотим вызвать метод. Далее идут аргументы в порядке, определенном в методе.

Замечу, что использования оператора взятия адреса(&) перед именем метода здесь обязательно. Имена нестатических методов не имеют желания неявно приводится к указателям, как имена функций. Вот так нельзя:
std::invoke(Handler::foo, Handler{}, 42);

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

👉🏿 Указатель на поле класса. Недавно обсуждали, что с помощью std::invoke можно "вызывать" поля класса. Неочевидно, но бывает полезно.

struct Payment {
double amount;
std::string category;
}

Payment p{100500, "confetki"};
std::cout << std::invoke(&Payment:amount, p) << std::endl;
// OUTPUT:
// 100500


Фух. Вроде все. Пишите, если что забыл.

Have all the tools. Stay cool.

#cppcore
std::exchange
#опытным

В прошлом посте мы пасхалкой использовали 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
Лямбды без захвата
#опытным

Сегодня немного проясню ситуацию с лямбдами без захвата.

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

Давайте взглянем на простую лямбду:

auto fun = [](int i) { return i*2;};


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

class __lambda_11_15
{
public:
inline int operator()(int i) const
{
return i * 2;
}

using retType_11_15 = int (*)(int);
inline constexpr operator retType_11_15 () const noexcept
{
return __invoke;
}

private:
static inline /*constexpr */ int __invoke(int i)
{
return __lambda_11_15{}.operator()(i);
}
};


И тут много интересного!

Например, у лямбды без захвата все же генерируется оператор вызова operator().

В добавок к этому определяется оператор приведения к указателю на функцию operator retType_11_15 (), который фактически приводит приватный статический метод класса к указателю. А для переиспользования кода, статический метод на лету конструирует объект и вызывает у него operator().

То есть вот примерно как это работает:

int apply_function(int (*func)(int), int value) {
return func(value); // Вызываем переданную функцию
}

int main() {
auto fun = [](int i) { return i2;};
fun(42);
apply_function(fun, 42);
return 0;
}


Здесь у нас 2 вида использования лямбды: через объект замыкания и через коллбэк в apply_function. Посмотрим, что будет вызываться в каждом конкретном случае:

int main()
{
__lambda_11_15 fun = __lambda_11_15{};
fun.operator()(42);
apply_function(fun.operator __lambda_11_15::retType_11_15(), 42);
return 0;
}


В случае вызова через объект замыкания триггерится operator(), а при передаче в другую функцию - оператора приведения к указателю на функцию.

Зачем два способа вызова? Почему нельзя обойтись просто приведением к указателю на функцию?

Вызывать лямбду через указатель на функцию - это лишить себя основной оптимизации компилятора - инлайнинга. Если передавать лямбду, как полноценный тип замыкания, то компилятор будет знать, как встроить код его operator() внутрь callee, потому что все типы определены на этапе компиляции. А по указателю на функцию можно передать все, что угодно. В простых случаях, как в apply_function, может и все хорошо будет. Но в более сложных - вы лишитесь оптимизации.

Но благо указатели на функции используются преимущественно в сишном апи и по-другому просто не получится. В С++ есть шаблоны и объекты и надобность в использовании указателей на функции практически отпадает.

Надеюсь, теперь вы чуть больше о лямбдах знаете)

Know more. Stay cool.

#cppcore #cpp11
std::mem_fn
#опытным

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

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
​​Разница между std::stoi+std::to_string и std::from_chars+std::to_chars
#опытным

В C++ есть два основных подхода к конвертации чисел в строки и обратно:

Старомодный  — std::stoi, std::to_string (C++11)

Модномолодежный — std::from_chars, std::to_chars (C++17)

В чем принципиальная разница между ними и когда какой подход использовать? Сегодня мы широкими мазками ответим на эти вопросы.

Особенности старомодного подхода:

👉🏿 Основное - это исключения. Все нештатные ситуации обрабатываются с их помощью, что ведет к неким накладным расходам.

👉🏿 Работа в высокоуровневом ООП стиле. Используются классы и возвращаются классы, без всяких сырых буферов.

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

👉🏿 Поддержка локалей. Грубо говоря, это механизм для учёта региональных особенностей представления данных. То есть std::stoi, std::to_string реализованы с учетом возможности спецификации локалей и соотвественно изменения результатов конвертации. С локалями возможна такая штука:
// В США (локаль "en_US"):
std::to_string(3.14); // "3.14" (точка как разделитель)

// В Германии (локаль "de_DE"):
std::to_string(3.14); // Может вернуть "3,14" (запятая)!
Естественно, что поддержка такой фичи чего-то да стоит.

Особенности модномолодежного подхода:

👉🏿 Функции std::from_chars, std::to_chars спроектированы быть настолько легкими и быстрыми, насколько это возможно на таком уровне абстракции.

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

👉🏿 Отсутствие исключений. Функции возвращает объект ошибки, который явно нужно проверять руками.

👉🏿 Не проверяет локали.

👉🏿 Поддерживают частичный парсинг.

👉🏿 Поддержка явной гарантии round-trip. Если вы запишите в строку число с помощью std::to_chars и прочитаете его с помощью std::from_chars, то вы всегда получите изначальный результат. Главное, чтобы обе функции были вызваны с использованием одинаковой реализации стандартной библиотеки. Но у std::stoi, std::to_string и этого нет.


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

Возможность поэтапного парсинга также не оставляет выбора - используйте std::from_chars.

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

Choose the right tool. Stay cool.

#cppcore #cpp11 #cpp17