Наследование? От лямбды? Ч2
#опытным
Лямбды - это функциональные объекты по своей сути. Объекты классов с перегруженным оператором(). Зачем вообще от такой сущности наследоваться? Можно же просто сделать обычный класс с такими же перегруженным оператором и расширять его сколько влезет.
Ну как будто бы да. От одной лямбды наследоваться особо нет смысла. А что насчет множественного наследования?
Идейно - наш наследник будет наследовать публичный интерфейс всех своих родителей. То есть в наследнике будет много перегруженных операторов() для разных входных параметров. Вот это уже чуть удобнее. Мы можем находу создавать объект, который единообразно с помощью перегрузок будет обрабатывать какие-то вещи.
Покажу чуть подробнее:
Логика и механика те же, что и в прошлом посте. Только теперь мы налету конструируем объект, который умеет в 2 перегрузки функции. Если эти перегрузки действительно небольшие, то не особо понятно, зачем мне определять их как отдельные перегрузки отдельной функции. Это нужно будет их потом по коду искать. А тут все рядышком и смотреть приятно.
Только не наследуйтесь от нескольких лямбд, которые принимают одинаковый набор параметров. Компилятор не сможет разрезолвить, от какого конкретно родителя вы хотите вызвать перегрузку и билд упадет.
Дальние ряды уже начали догадываться зачем такая конструкция реально может быть нужна. Но все объяснения в следующий раз.
Have a sense. Stay cool.
#template #cppcore #cpp17
#опытным
Лямбды - это функциональные объекты по своей сути. Объекты классов с перегруженным оператором(). Зачем вообще от такой сущности наследоваться? Можно же просто сделать обычный класс с такими же перегруженным оператором и расширять его сколько влезет.
Ну как будто бы да. От одной лямбды наследоваться особо нет смысла. А что насчет множественного наследования?
Идейно - наш наследник будет наследовать публичный интерфейс всех своих родителей. То есть в наследнике будет много перегруженных операторов() для разных входных параметров. Вот это уже чуть удобнее. Мы можем находу создавать объект, который единообразно с помощью перегрузок будет обрабатывать какие-то вещи.
Покажу чуть подробнее:
template<class Lambda1, class Lambda2>
struct DerivedFromLambdas : public Lambda1, Lambda2
{
DerivedFromLambdas(Lambda1 lambda1, Lambda2 lambda2)
: Lambda1(std::move(lambda1))
, Lambda2{std::move(lambda2)} {}
using Lambda1::operator();
using Lambda2::operator();
};
int main(){
DerivedFromLambdas child{[](int i){return "takes int";}, [](double d){return "takes double";}};
std::cout << child(42) << std::endl;
std::cout << child(42.0) << std::endl;
return 0;
}
// OUTPUT:
// takes int
// takes double
Логика и механика те же, что и в прошлом посте. Только теперь мы налету конструируем объект, который умеет в 2 перегрузки функции. Если эти перегрузки действительно небольшие, то не особо понятно, зачем мне определять их как отдельные перегрузки отдельной функции. Это нужно будет их потом по коду искать. А тут все рядышком и смотреть приятно.
Только не наследуйтесь от нескольких лямбд, которые принимают одинаковый набор параметров. Компилятор не сможет разрезолвить, от какого конкретно родителя вы хотите вызвать перегрузку и билд упадет.
Дальние ряды уже начали догадываться зачем такая конструкция реально может быть нужна. Но все объяснения в следующий раз.
Have a sense. Stay cool.
#template #cppcore #cpp17
🔥31👍10❤6🤯3❤🔥1⚡1
Наследование? От лямбды? Ч3
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
А давайте сделаем еще один шаг вперед. Зачем нам наследоваться от какого-то фиксированного количества лямбд? Не будем себя ничем ограничивать. Давайте наследоваться от произвольного количества!
template<typename ... Lambdas>
struct DerivedFromLambdas : Lambdas...
{
DerivedFromLambdas(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
И что нам этот шаг дал?
Теперь мы можем благодаря variadic templates в компайл-тайме генерить структурки, которые включают произвольное количество различных вариантов вызвов оператора(). И слово "вариант" здесь неспроста.
Помните наш std::visit? Который применяет визитор к объекту варианта.
Так вот теперь мы можем налету делать наши визиторы!
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{
}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
int main(){
std::vector<var_t> vec = {10, 1.5, "hello"};
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}
Создаем вектор, который может содержать 3 типа. И не так уж просто обрабатывать элементы такого вектора. Но вооружившись std::visit и созданным налету нашим Visitor'ом мы играючи обошли все элементы и красиво вывели их на экран:
10 1.500000 "hello"
Визитор - довольно интересный паттерн. И круто, что мы с вами смогли до него дойти из, казалось бы, такой неочевидной темы, как наследование от лямбд.
Но вообще, конкретно вот эта конструкция с наследованием от лямбд называется overload паттерн. Это стандартное и более короткое название для этого дизайн решения.
@monah_tuk как-то в комментах напомнил о прекрасной тулзе, где вы можете посмотреть чуть более легкосмотрибельную версию вашего кода. Жмякнув сюда вы сможете посмотреть, во что превращается код из этого поста и, возможно, понять чуть больше.
Visit your close ones. Stay cool.
#template #design #cpp17
❤21👍14🔥9🤯3⚡2👏1
Смешиваем std::visit и std::apply
#опытным
Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.
И такую штуку можно сделать для tuple-like объектов, среди которых std::pair, std::tuple и std::array. Функция std::applyможет распаковать нам элементы этих коллекций и вызвать для них функцию, которая принимает в качестве аргументов все эти элементы по отдельности. Это же то, что нам нужно!
Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.
Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.
В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.
Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.
В общем, делаем себе укол пары кубиков метапрограммирования.
Выглядит клёво!
Опять же оставляю ссылочку на cppinsights с этим примером, там подробно показаны сгенеренные сущности и их их взаимодействие.
Look cool. Stay cool.
#template #cpp17
#опытным
Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.
И такую штуку можно сделать для tuple-like объектов, среди которых std::pair, std::tuple и std::array. Функция std::applyможет распаковать нам элементы этих коллекций и вызвать для них функцию, которая принимает в качестве аргументов все эти элементы по отдельности. Это же то, что нам нужно!
Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
int main(){
std::array<var_t, 3> arr = {1.5, 42, "Hello"};
Visitor vis{[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } };
std::apply([&](auto&&... args){(std::visit(vis, std::forward<decltype(args)>(args)), ...);}, arr);
}
Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.
В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.
Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.
В общем, делаем себе укол пары кубиков метапрограммирования.
Выглядит клёво!
Опять же оставляю ссылочку на cppinsights с этим примером, там подробно показаны сгенеренные сущности и их их взаимодействие.
Look cool. Stay cool.
#template #cpp17
👍20🔥11😁11❤4⚡1
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
🔥31👍15❤3👎2❤🔥1⚡1
Deducing this
#опытным
Все методы принимают неявный параметр - указатель this на текущий объект. Также мы можем вызывать методы для объектов с разной константностью/ссылочностью. И главное - компилятор знает в момент компиляции вызова метода настоящий тип объекта со всеми квалификаторами. Единственное, что отделяется нас от возможности введения шаблонности - это указательный тип this, который не инкапсулирует в себе информацию о квалификаторах объекта.
И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:
Особенности:
👉🏿 Мы явно указываем параметр this.
👉🏿 Явно указываем тип объекта и его квалификаторы.
👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра.
👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем.
Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:
Вот это бэнгер! Мы деквадруплицировали код!
Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта.
Настоящая магия, причем вне хогвартса!
Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self.
Вот вам пропоузал по этой замечательной фиче. А мы в нескольких следующих постах будем разбирать кейсы, где она может быть применима.
Simplify your life. Stay cool.
#cpp23 #template
#опытным
Все методы принимают неявный параметр - указатель this на текущий объект. Также мы можем вызывать методы для объектов с разной константностью/ссылочностью. И главное - компилятор знает в момент компиляции вызова метода настоящий тип объекта со всеми квалификаторами. Единственное, что отделяется нас от возможности введения шаблонности - это указательный тип this, который не инкапсулирует в себе информацию о квалификаторах объекта.
И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:
struct cat {
std::string name;
void print_name(this cat& self) {
std::cout << name; //invalid
std::cout << this->name; //also invalid
std::cout << self.name; //all good
}
void print_name(this const cat& self) {
std::cout << self.name;
}
void print_name(this cat&& self) {
std::cout << self.name;
}
void print_name(this const cat&& self) {
std::cout << self.name;
}
};
Особенности:
👉🏿 Мы явно указываем параметр this.
👉🏿 Явно указываем тип объекта и его квалификаторы.
👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра.
👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем.
Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:
template <typename T>
struct optional {
// One version of value which works for everything
template <class Self>
constexpr auto&& value(this Self&& self) {
if (self.has_value()) {
return std::forward<Self>(self).m_value;
}
throw bad_optional_access();
}
};
Вот это бэнгер! Мы деквадруплицировали код!
Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта.
Настоящая магия, причем вне хогвартса!
Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self.
Вот вам пропоузал по этой замечательной фиче. А мы в нескольких следующих постах будем разбирать кейсы, где она может быть применима.
Simplify your life. Stay cool.
#cpp23 #template
🔥37❤8👍7
Deducing this и CRTP
#опытным
У deducing this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:
Вам ничего это не напоминает? CRTP конечно.
Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:
За счет шаблонного параметра Derived, который должен быть точным типом наследника, мы можем безопасно кастануть this к указателю на наследника и вызывать у него любые методы.
Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:
Ну вот. У нас только один шаблонный метод. Но для пользователя он ничем не отличается от обычного нешаблонного метода.
Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами.
Make things more elegant. Stay cool.
#template #cpp23
#опытным
У deducing this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:
struct Machine {
template <typename Self>
void print(this Self&& self) {
self.print_name();
}
};
struct Car : public Machine {
std::string name;
void print_name() {
std::cout << "Car\n";
}
};
Car{}.print(); // Выведется "Car"
Вам ничего это не напоминает? CRTP конечно.
Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:
template <typename Derived>
struct add_postfix_increment {
Derived operator++(int) {
auto& self = static_cast<Derived&>(*this);
Derived tmp(self);
++self;
return tmp;
}
};
struct some_type : add_postfix_increment<some_type> {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};
За счет шаблонного параметра Derived, который должен быть точным типом наследника, мы можем безопасно кастануть this к указателю на наследника и вызывать у него любые методы.
Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:
struct add_postfix_increment {
template <typename Self>
auto operator++(this Self&& self, int) {
auto tmp = self;
++self;
return tmp;
}
};
struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};
Ну вот. У нас только один шаблонный метод. Но для пользователя он ничем не отличается от обычного нешаблонного метода.
Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами.
Make things more elegant. Stay cool.
#template #cpp23
👍28❤6🔥6❤🔥2🤣1
Неочевидное преимущество шаблонов
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
Как видите, здесь много
Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
#новичкам
Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным
Мы знаем, что шаблоны используются как лекарство от повторения кода, а также как средство реализации полиморфизма времени компиляции. Но неужели без них нельзя обойтись?
Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:
void qsort( void *ptr, std::size_t count, std::size_t size, /* c-compare-pred */* comp );
extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);
Как видите, здесь много
void *
указателей на void. В том числе с помощью него достигается полиморфизм в С(есть еще макросы, но не будем о них).Как это работает?
Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.
Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:
[](const void* x, const void* y)
{
const int arg1 = *static_cast<const int*>(x);
const int arg2 = *static_cast<const int*>(y);
const auto cmp = arg1 <=> arg2;
if (cmp < 0)
return -1;
if (cmp > 0)
return 1;
return 0;
}
Предикату не нужно передавать размер типа, потому что он сам знает наперед с каким данными он работает и сможет закастить void* к нужному типу.
Вот в этом предикате и проблема. Функция qsort не знает на этапе компиляции, с каким предикатом она будет работать. Поэтому компилятор очень ограничен в оптимизации этой части: он не может заинлайнить код компаратора в код qsort. На каждый вызов компаратора будет прыжок по указателю функции. Это примерна та же причина, по которой виртуальные вызовы дорогие.
Тип шаблонных параметров, напротив, известен на этапе компиляции.
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );
Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)
Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.
Use advanced technics. Stay cool.
#template #goodoldc #goodpractice #compiler
50🔥35👍8❤5⚡2👎1
Рекурсивные лямбды. Хакаем систему
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
#опытным
Конечно же вчерашний пост был первоапрельской шуткой. Не волнуйтесь, мы вас не бросим и без контента не оставим.
Поэтому возвращаемся к нашим баранам. То есть рекурсивным лямбдам. В прошлый раз мы узнали, что лямбды не могут захватывать себя, поэтому не могут быть рекурсивными. Сегодня поговорим о способах, как обойти эту проблему.
1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:
std::function<int(int)> factorial = [&factorial](int n) -> int {
return (n) ? n * factorial(n-1) : 1;
};
Но использование std::function очень затратно по всем критериям. Компиляция ощутимо замедляется, асма намного больше становится, и std::function обычно сильно медленнее обычных функций и лямбд. А еще и динамические аллокации.
Поэтому не самый хороший способ.
2️⃣ Используем С++14 generic лямбды:
auto factorial = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto i = factorial(7, factorial);
Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:
class __lambda_24_20
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(int n, type_parameter_0_0 && factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial(n - 1, factorial);
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<__lambda_24_20 &>(int n, __lambda_24_20 & factorial) const
{
if(n <= 1) {
return n;
}
return n * factorial.operator()(n - 1, factorial);
}
#endif
};
У класса есть шаблонный оператор, но это полностью завершенный тип. После объявления лямбды компилятор уже знает конкретный тип замыкания и может инстанцировать с ним шаблонный метод.
Форма использования такой лямбды оставляет желать лучшего, потому что нам нужно постоянно передавать ее в качестве параметра. Полечить это, как всегда, можно введением дополнительного уровня индирекции. Обернем лямбду в лямбду!
auto factorial_impl = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto factorial = [&](int n) { return factorial_impl(n, factorial_impl); };
auto i = factorial(7);
Теперь не нужно передавать доп параметры.
3️⃣ Если лямбда ничего не захватывает, то ее можно приводить к указателю на функцию. На этом основан следующий метод:
using factorial_t = int(*)(int);
static factorial_t factorial = [](int n) {
if (n <= 1) return n;
return n * factorial(n - 1);
};
auto i = factorial(7);
Статическая локальная переменная видна внутри лямбды, поэтому такой трюк прокатывает.
Если у вас есть какие-то еще подобные приемы - пишите в комменты.
Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.
И их есть у меня!
Об этом в следующий раз.
Always find a way out. Stay cool.
#template #cppcore #cpp11 #cpp14
❤38🔥16👍12
Рекурсивные лямбды. Кейсы
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
Тут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
#опытным
Сегодня разберем пару кейсов, в которых можно использовать рекурсивные лямбды.
1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.
Можно обходить literaly деревья:
struct Leaf { };
struct Node;
using Tree = std::variant<Leaf, Node*>;
struct Node {
Tree left;
Tree right;
};
template<typename ... Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward<Lambdas>(lambdas))...
{}
using Lambdas::operator()...;
};
int main()
{
Leaf l1;
Leaf l2;
Node nd{l1, l2};
Tree tree = &nd;
int num_leaves = std::visit(Visitor(
[](Leaf const&) { return 1; },
[](this const auto& self, Node* n) -> int {
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}
Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.
У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?
Все дело в магии явного this. Здесь мы с вами говорили, что при наследовании и вызове метода базового класса this вывыводится в тип класса наследника. А наш визитор как раз является наследником лямбды, которая обходит ноды дерева. Таким образом мы рекурсивно используем весь визитор.
Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.
2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):
auto printTuple = [](const auto& tuple) constexpr {
auto impl = []<size_t idx>(this const auto& self, const auto& t) constexpr {
if constexpr (idx < std::tuple_size_v<std::decay_t<decltype(t)>>) {
std::cout << std::get<idx>(t) << " ";
self.template operator()<idx+1>(t); // Рекурсивный вызов
}
};
impl.template operator()<0>(tuple);
};
std::tuple<int, double, std::string> tp{1, 2.0, "qwe"};
printTuple(tp);
// Output:
// 1 2 qwe
Тут нам придется использовать шаблонные лямбды с индексом текущего элемента тупла в качества шаблонного параметра. Обратите внимание, как вызываются лямбды в данном случае. Так как у нас шаблонный оператор(), то компилятору надо явно сказать, что мы вызываем шаблон и также явно передать в него шаблонный параметр. Подобные лямбды с явным вызовом шаблонного оператора() желательно оборачивать в еще одну лямбду, чтобы коллеги случайно кофеем не подавились, увидев эту кракозябру.
3️⃣ Обход вложенных директорий с помощью std::filesystem:
auto listFiles = [](const std::filesystem::path& dir) {
std::vector<std::string> files;
auto traverse = [&](this const auto& self, const auto& path) {
for (const auto& entry : std::filesystem::directory_iterator(path)) {
if (entry.is_directory()) {
self(entry.path());
} else {
files.push_back(entry.path().string());
}
}
};
traverse(dir);
return files;
};
Ну тут вроде без пояснений все плюс-минус понятно.
Вообще, в любом месте, где применима небольшая по объему кода рекурсия, вы можете использовать рекурсивные лямбды.
Пишите в комменты, если в вас есть что добавить по юзкейсам. Если кто использует какие-то генеративные алгоритмы, для реализации которых подойдет рекурсивная лямбда, тоже пишите. В общем, пишите любые мысли по теме)
Be useful. Stay cool.
#cppcore #cpp23 #template
👍27🔥14❤6⚡1😁1
std::forward_like
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
#опытным
Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.
Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.
Если std::forward объявлена так
template< class T >
constexpr T&& forward(std::remove_reference_t<T>& t ) noexcept;
template< class T >
constexpr T&& forward(std::remove_reference_t<T>&& t ) noexcept;
За счет перегрузок для lvalue и rvalue, она позволяет правильно передавать тип параметра, объявленного универсальной ссылкой, во внутренние вызовы. Здесь задействован всего один шаблонный параметр.
std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.
template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;
Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.
Зачем вообще так делать?
Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.
Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:
return forward<decltype(obj)>(obj).member;
Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:
struct adapter {
std::deque<std::string> container;
auto&& operator[](this auto&& self, size_t i) {
return std::forward_like<decltype(self)>(self.container[i]);
} };
Мы можем из оператора индексации вернуть правую ссылку на строку внутри container, если мы вызываем оператор на правоссылочном объекте. В таком случае объект нам больше не нужен и нет смысла сохранять все его данные. Поэтому можно мувать наружу содержимое контейнера. Ну а если объект адаптера обычный lvalue и не собирается разрушаться, то возвращаем левую ссылку на элемент контейнера.
Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.
В общем, эта функция разрешает вот такие оптимизации и унифицирует интерфейс для объектов разной ссылочности.
Follow the head. Stay cool.
#cpp23 #template
4🔥22👍7❤4😁2
Идеальная передача из лямбды
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
Ну а передача копии вообще никогда не была проблемой:
Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таковзакон стандарт.
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
#опытным
Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:
auto callback = [message=get_message(), &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}
Ну а передача копии вообще никогда не была проблемой:
auto callback = [message=get_message(), &scheduler]() {
// some preparetions
scheduler.submit(message);
}
Однако подобную функцию можно использовать в двух контекстах: с возможностью повторного выполнения и одноразового исполнения:
callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)
Так вот что, если мы хотим в первом случае сабмитить в шедулер копию сообщения, чтобы иметь возможность повторить вызов, а во втором случае - мувнуть сообщение в шедулер. То есть хотелось бы на основании типа ссылочности объекта подстраивать тип поля класса и передавать поле во внутренние вызовы.
Это все можно делать с помощью явного this и std::forward_like:
auto callback = [message=get_message(), &scheduler](this auto &&self) {
return scheduler.submit(std::forward_like<decltype(self)>(message));
};
Пара интересных наблюдений:
👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.
👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таков
Из адекватных примеров явного this на этом все.
Deducing this - одна из мажорных фичей 23-го стандарта. Рано или поздно все на него перейдут и нужно заранее знать кейсы, где фичу можно использовать, чтобы писать более понятный и оптимальный код.
Be a major figure. Stay cool.
#template #cpp23
2🔥21👍8❤5😁1
Частичная специализация шаблонов функций
#опытным
Во многих образовательных ресурсах на русском и английском языке я видел, как рассказывают про частичную специализацию шаблонов функций. Ресурсы конечно не очень авторитетные, но если читать про шаблоны в стандарте, то голова вспухнет. Давайте сейчас проясним этот момент.
Частичной специализации шаблонов функции не существует. И точка!
То, что называют выдают за нее - это обычная перегрузка шаблонных функций.
Звучит, как пустяковая проблема. Какая разница, как назвать молоток, если им все равно можно забить гвоздь?
Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.
Шаблонные функции, как и обычные функции, можно перегружать. Логично было принести этот функционал в шаблоны, ибо перегрузки могут быть вообще не связаны ни одним общим параметром.
Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.
Частичная специализация же характерна только для шаблонов классов и переменных.
И в специализированном классе мы должны показать с помощью треугольных скобок, для какой подгруппы типов мы специализируем шаблон. И это обязательно. Специализация должна показать, что она именно специализация какого-то общего шаблона. Ведь перегрузок шаблонов классов не бывает:
С этим разобрались. Давайте пофантазирует, что будет, если бы мы могли частично специализировать шаблон функции, при этом оставили бы дефолтный механизм перегрузки:
Ну и как компилятору выбирать между T-p-overload и T-p-specialization?
Частичная специализация шаблонов функций вводила бы неоднозначность или дополнительную сложность в порядок разрешения перегрузок функции. Да и перегрузка полностью покрывает функциональность частичной специализации. Поэтому ее не существует.
На самом деле без понимания того, какими способами можно изменить поведение шаблона, нельзя нормально понять, как компилятор выбирает правильного кандидата для вызова. Это тонкости, но на высоком уровне тонкости все больше и больше роляют.
Don't be confused. Stay cool.
#template #cppcore
#опытным
Во многих образовательных ресурсах на русском и английском языке я видел, как рассказывают про частичную специализацию шаблонов функций. Ресурсы конечно не очень авторитетные, но если читать про шаблоны в стандарте, то голова вспухнет. Давайте сейчас проясним этот момент.
Частичной специализации шаблонов функции не существует. И точка!
То, что называют выдают за нее - это обычная перегрузка шаблонных функций.
Звучит, как пустяковая проблема. Какая разница, как назвать молоток, если им все равно можно забить гвоздь?
Безусловно, вы правы. Большинству разработчиков такие тонкости знать не нужно. Но мы ведь тут грокаем С++, у нас много постов про такие тонкости. Поэтому погнали.
Шаблонные функции, как и обычные функции, можно перегружать. Логично было принести этот функционал в шаблоны, ибо перегрузки могут быть вообще не связаны ни одним общим параметром.
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
1❤22👍12🔥11
Перегружаем шаблоны классов
#опытным
В прошлом посте я говорил, что нельзя перегружать шаблоны классов.
В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.
Но как и практически любое ограничение в С++, его можно хакнуть.
Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.
Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.
Вот такие фокусы.
Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:
Но все равно это хороший инструмент, которым можно пользоваться.
Hack the boundaries. 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
1🔥22👍12❤🔥3❤1
std::move_if_noexcept
#опытным
В тему noexcept. В этом посте мы рассказали о том, что noexcept конструктор позволяет разрешить перемещения элементов при реаллокациях std::vector. Однако даже если ваш мув-конструктор определен, но не помечен noexcept, и нет копирующего конструктора, то вектору все равно разрешается перемещать элементы. За это необычное поведение ответственна функция std::move_if_noexcept. Сегодня посмотрим, за счет чего такое поведение достигается.
Вот реализация этой функции в gcc:
Две части: условия мува и сам мув.
Все работает буквально на одних type trait'ах. Если условие move_if_noexcept_cond правдиво, то результат мува кастуется к константной левой ссылке, чтобы в итоге мува в итоге не произошло. Если ложное, то результат кастится к правой ссылке, что потенциально и разрешает мув.
Условие move_if_noexcept_cond истинно только в одном случае: когда мув-конструктор бросающий и есть копирующий конструктор. Получается, что это единственная ситуация, когда мува не произойдет. Во всех остальных случаях значение скастуется к правой ссылке.
Не спрашивайте меня, почему условие мува как будто бы перевернутое. Странное решение. Если кто знает, поясните в комментах.
Кстати тут есть интересный момент. Если класс удовлетворяет трейту *move_constructible, то это не значит, что у него есть мув конструктор! *move_constructible всего лишь значит, что объект можно скрафтить из правой ссылки. А правые ссылки могут приводиться к константным левым ссылкам. И даже если ваш класс не будет иметь мув-конструктора, но его копирующий конструктор принимает константную левую ссылку, то этот класс будет удовлетворять условию is_move_constructable:
То есть использование трейта std::is_nothrow_move_constructible на классе, не имеющем мув-конструктора, абсолютно легально.
В общем, просто хотел рассказать про эти два интересных момента. Это может быть важно при проектировке своих структур данных.
Live legally. Stay cool.
#template #cppcore #STL
#опытным
В тему 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
❤23👍10🔥8
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.
Посмотрим, к чему приводит эта маленькая особенность:
Для инстанциаций вектора с любыми другими типами все хорошо работает. А для bool специализации мы получаем висячую ссылку и UB.
При использовании decltype(auto) таких проблем нет. Можете поиграться с примерами на cppinsights и godbolt.
Proxy объекты не так часто используются. Один из основных кейсов - это доступ к элементам многомерных структур. Однако при проектировании и написании кода, которым будут пользоваться другие люди, нужно учитывать такие вещи и писать в первую очередь безопасный код.
Спасибо @thonease за предоставления исходного кода)
По итогу серии постов: использование auto&& безопасно при выводе типа локального объекта, но небезопасно при выводе типа возвращаемого значения функции. В последнем случае нужно использовать decltype(auto).
Be safe. Stay cool.
#cppcore #template
#опытным
В прошлом посте мы видели, что разница в использовании 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
🔥17👍11❤9
std::invoke
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
Согласитесь, что этот синтаксис отличается от
Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?
Использовать std::invoke. Это функция буквально создана, чтобы вызывать все, что только можно вызвать. Она конечно же написана на вариабельных шаблонах, чтобы вы могли передать туда все, что душе угодно:
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#cppcore #template
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
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❤22👍12🔥8🤔1🤯1
Почему еще важен std::forward
#опытным
Подписчик @Ivaneo предложил новую рубрику #ЧЗХ, в рамках которой мы будем рассматривать мозголомательные примеры кода и пытаться объяснить, почему они работают так криво.
Также спасибо ему за предоставление следующего примера:
Как думаете, что выведется на консоль? Подумайте пару секунд.
Ну нормальный человек ответит:
Однако командная строка вам выдаст следующее:
Если не верите, по посмотрите в годболте. И можете уже сейчас написать в комментах: "ЧЗХ", "WTF", "WAT" и прочее.
А нам пораразбирацца.
Тут используется auto в аргументах функции, значит эта функция неявно шаблонная. Посмотрим, что нам выдаст cppinsights по этому коду:
Просто прекрасно. Какого черта компилятор кастит переменные к противоположным типам?
Первое, что важно понимать: внутри функции foo переменная v - это уже lvalue, так как имеет имя. Значит просто так вызвать перегрузки для правых ссылок он не может.
Но у компилятора в кармане есть стандартные преобразования, которые и идут в ход, когда нет подходящих перегрузок. Обычно это неявные преобразования из одного типа в другой. Не преобразования из одного типа ссылочности в другой тип ссылочности, а прям в другие типы данных.
То есть происходит следующее: компилятор понимает, что подходящей перегрузки нет, поэтому начинает применять стандартные преобразования в другие типы. Любой каст дает временный объект. А временный объект типа int легко биндится к float&&, как и временный объект float легко биндится к int&&.
Вот и получается обмен вызовами.
Чтобы такого не происходило, применяйте перед сном std::forward. Если есть контекст вывода типов, то он помогает правильно передавать категорию выражения объекта во внутренние вызовы.
В этом случае вывод будет ожидаемым.
Be amazed. Stay cool.
#cppcore #cpp11 #template
#опытным
Подписчик @Ivaneo предложил новую рубрику #ЧЗХ, в рамках которой мы будем рассматривать мозголомательные примеры кода и пытаться объяснить, почему они работают так криво.
Также спасибо ему за предоставление следующего примера:
#include <iostream>
void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }
void foo(auto&& v) { bar(v); }
int main() {
foo(1);
foo(2.0f);
}
Как думаете, что выведется на консоль? Подумайте пару секунд.
Ну нормальный человек ответит:
int 1
float 2
Однако командная строка вам выдаст следующее:
float 1
int 2
Если не верите, по посмотрите в годболте. И можете уже сейчас написать в комментах: "ЧЗХ", "WTF", "WAT" и прочее.
А нам пораразбирацца.
Тут используется auto в аргументах функции, значит эта функция неявно шаблонная. Посмотрим, что нам выдаст cppinsights по этому коду:
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int>(int && v)
{
bar(static_cast<float>(v));
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<float>(float && v)
{
bar(static_cast<int>(v));
}
#endif
Просто прекрасно. Какого черта компилятор кастит переменные к противоположным типам?
Первое, что важно понимать: внутри функции foo переменная v - это уже lvalue, так как имеет имя. Значит просто так вызвать перегрузки для правых ссылок он не может.
Но у компилятора в кармане есть стандартные преобразования, которые и идут в ход, когда нет подходящих перегрузок. Обычно это неявные преобразования из одного типа в другой. Не преобразования из одного типа ссылочности в другой тип ссылочности, а прям в другие типы данных.
То есть происходит следующее: компилятор понимает, что подходящей перегрузки нет, поэтому начинает применять стандартные преобразования в другие типы. Любой каст дает временный объект. А временный объект типа int легко биндится к float&&, как и временный объект float легко биндится к int&&.
Вот и получается обмен вызовами.
Чтобы такого не происходило, применяйте перед сном std::forward. Если есть контекст вывода типов, то он помогает правильно передавать категорию выражения объекта во внутренние вызовы.
#include <iostream>
void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }
void foo(auto&& v) { bar(std::forward<decltype(v)>(v)); }
int main() {
foo(1);
foo(2.0f);
}
В этом случае вывод будет ожидаемым.
Be amazed. Stay cool.
#cppcore #cpp11 #template
🔥46🤯29❤8👍7❤🔥2
Правильно захватываем по ссылке объект в лямбду
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#template #cpp11
#опытным
В недавнем посте мы рассказали о проблеме, когда лямбда, захватывающая объект по ссылке, возвращается из метода. Это потенциально может привести к тому, что объект уничтожится раньше, чем произойдет вызов лямбды, что в итоге приведет к провисшей ссылке, а значит - к UB.
struct Task {
int id;
std::function<void()> GetNotifier() {
return [&]{
std::cout << "notify " << id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}
Можно вместо захвата по ссылке использовать захват по значению. Но скорее всего это не поможет, так как хочется использовать тот же самый объект, из метода которого возвращалась лямбда.
Тут бы шаред поинтер использовать. Но втупую его заюзать тоже ничем не поможет:
struct Task {
int id;
std::function<void()> GetNotifier() {
return [curr = std::shared_ptr<Task>(this)]{
std::cout << "notify " << curr->id << std::endl;
};
}
};
int main() {
auto notify = Task { 5 }.GetNotifier();
notify();
}
Да, мы создали из this шареный указатель, но его контрольный блок никак не учитывает оригинальный объект!
curr
будет думать, что только он и его копии будут владеть объектом, а оригинальный объект просто уничтожится и curr
будет ссылаться на невалидный объект. Получили то же самое UB.Что делать?
Использовать миксин C++11 std::enable_shared_from_this. Это базовый CRTP класс, который предоставляет метод shared_from_this(). Если вы обернете исходный объект в std::shared_ptr, то метод shared_from_this возвращает копию объекта умного указателя, в котором находился исходный объект. Эта копия будет разделять контрольный блок с оригинальным объектом, поэтому пока жива хоть одна копия, исходный объект не разрушится. Выглядит это так:
struct Task : std::enable_shared_from_this<Task> {
int id;
std::function<void()> GetNotifier() {
// Захватываем shared_ptr на текущий объект
return [self = shared_from_this()] {
std::cout << "notify " << self->id << std::endl;
};
}
};
int main() {
auto notify = std::make_shared<Task>(5)->GetNotifier();
notify(); // Теперь безопасно - объект не будет уничтожен
}
В первой строчке main происходит много вещей:
1️⃣ Создается объект класса Task и оборачивается во временный объект умного указателя.
2️⃣ Вызывается метод GetNotifier у объекта внутри временного умного указателя, из которого возвращается лямбда с захваченной копией временного объекта.
3️⃣ До перехода к следующей строчке временный объект, созданный через make_shared, разрушается.
Но ничего страшного не происходит, потому что notify хранит в себе копию временного объекта умного указателя, а значит тебе эта лямбда владелец исходного объекта Task{5}. Поэтому при вызове этой лямбды никакого Ub и провисания ссылки нет.
Вообще, наверное пора рассказывать про CRTP, миксины и прочие нечисти шаблонов в С++. Если хотите такого, то жмякните лайкосик, а с нам посты по этим темам.
Watch your lifetime. Stay cool.
#template #cpp11
2👍92❤19🔥14🤯1
Динамический полиморфизм: std::variant + std::visit
#опытным
Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же std::function - сочетание виртуальных функций и шаблонов. Но о подробностях реализации в другом посте.
Другой пример - std::variant + std::visit. std::variant - шаблонный класс, который может хранить в себе объект любого типа, который есть среди его шаблонных параметров. Эдакий типобезопасный union, без UB и прочей грязи.
Вариант позволяет складывать фиксированный набор типов в один контейнер:
Но после помещения задач в контейнер мы уже точно не можем сказать, какой конкретно тип содержит каждый элемент. Как ими тогда оперировать?
Через std::visit, конечно. Эта функция, которая принимает функциональный объект, который можно вызвать для любого типа, потенциально хранящегося в варианте, к самому объекту варианта. Объект std::variant на самом деле знает, какой тип в нем хранится, просто нам он об этом не рассказывает. А std::visit'у рассказывает:
По-настоящему мощным это сочетание ставится при применении паттерна overload:
Опять же, на этапе компиляции воркер понятия не имеет, какой тип реально хранится в варианте. Решение, какой обработчик вызвать, принимается в рантайме. Поэтому пара variant+visit реализует динамический полиморфизм, хоть и не без шаблонной магии.
Visit your closest. Stay cool.
#cpp17 #template
#опытным
Несмотря на то, что шаблоны в С++ ассоциируются со статическим полиморфизмом, они также помогают реализовывать и динамический полиморфизм. Реализация того же std::function - сочетание виртуальных функций и шаблонов. Но о подробностях реализации в другом посте.
Другой пример - std::variant + std::visit. std::variant - шаблонный класс, который может хранить в себе объект любого типа, который есть среди его шаблонных параметров. Эдакий типобезопасный union, без UB и прочей грязи.
std::variant<int, float, std::string> value;
value = 3.14f; // valid
value = 42; // also valid
value = std::string{"You are the best!"}; // again valid
value = 3.14; // ERROR: 3.14 is double and double is not in template parameter list
Вариант позволяет складывать фиксированный набор типов в один контейнер:
std::vector<std::variant<int, float, std::string>> vec;
vec.push_back(3.14f);
vec.push_back(42);
Но после помещения задач в контейнер мы уже точно не можем сказать, какой конкретно тип содержит каждый элемент. Как ими тогда оперировать?
Через std::visit, конечно. Эта функция, которая принимает функциональный объект, который можно вызвать для любого типа, потенциально хранящегося в варианте, к самому объекту варианта. Объект std::variant на самом деле знает, какой тип в нем хранится, просто нам он об этом не рассказывает. А std::visit'у рассказывает:
struct PrintVisitor {
void operator()(int x) { cout << "int: " << x; }
void operator()(float x) { cout << "float: " << x; }
void operator()(string s) { cout << "string: " << s; }
};
std::variant<int, float, string> value;
value = 3.14f;
std::visit(PrintVisitor{}, value); // Prints "float: 3.14"
По-настоящему мощным это сочетание ставится при применении паттерна overload:
template<typename ...Lambdas>
struct Visitor : Lambdas...
{
Visitor(Lambdas&& ...lambdas) : Lambdas(std::forward<Lambdas>(lambdas))... {}
using Lambdas::operator()...;
};
using var_t = std::variant<int, double, std::string>;
void Worker(const std::vector<var_t>& vec){
std::for_each(vec.begin(),
vec.end(),
[](const auto& v)
{
std::visit(Visitor{
[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } }
, v);
});
}
Опять же, на этапе компиляции воркер понятия не имеет, какой тип реально хранится в варианте. Решение, какой обработчик вызвать, принимается в рантайме. Поэтому пара variant+visit реализует динамический полиморфизм, хоть и не без шаблонной магии.
Visit your closest. Stay cool.
#cpp17 #template
👍28🔥17❤12❤🔥2🤯1
std::type_identity
#опытным
Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:
В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:
Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:
Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.
Можно было сделать 3 отдельных параметра:
Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.
Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:
Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:
При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.
В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.
Также type_identity можно использовать для того, чтобы запретить вывод типов и заставить пользователя явно прописывать шаблонные параметры. Это может быть важно для точной передачи типа:
Тоже самое для вариадиков:
Прикольный инструмент для тонкой настройки вашего шаблонного кода.
Спасибо @d7d1cd за идею для поста)
Turn off deduction when it is not needed. Stay cool.
#template #cpp20
#опытным
Не так давно мы разбирали функцию std::clamp, которая ограничивает значение переменной верхней и нижней границей:
double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}
В таком виде это прекрасно работает. Однако у std::clamp есть одна проблема: все три ее параметра должны быть одного типа:
template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );
Если попытаться использовать функцию с разными типами, то получим ошибку вывода типов:
auto bounded = std::clamp(42, 3.14, 69.f); // ERROR!
Компилятор не поймет, какой тип Т имелся ввиду, потому что все три аргумента разных типов.
Можно было сделать 3 отдельных параметра:
template<class T1, class T2, class T3>
constexpr const T& clamp( const T1& v, const T2& lo, const T3& hi );
Но тогда приходилось бы навешивать какие-то compile-time проверки совместимости типов.
Есть подход получше - использовать C++20 std::type_identity. Это максимально простая обертка над типом:
template<class T>
struct type_identity { using type = T; };
template< class T >
using type_identity_t = type_identity<T>::type;
Но этот простой финт ушами дает очень важный эффект - отсутствие контекста вывода в шаблонах:
template <class T>
auto bound(T num, typename std::type_identity<T>::type low, typename std::type_identity<T>::type high) {
return std::clamp(num, low, high);
}
auto bounded = bound(25.5, 20, 25);
При использовании зависимых имен(type - зависимое имя шаблонного класса type_identity) компилятор не вывод тип Т для аргументов. Он либо полагается на явное указание аргументов при инстанциации, либо на вывод типа из других параметров. В последнем сниппете только параметр num находится в контексте вывода и по нему компилятор выводит тип Т. Типы параметров low и high не зависят от того, какие соответствующие аргументы мы передаем при вызове функции. Они определяются выведенным типом первого аргумента.
В данном случае тип num выведется в double, поэтому и типы low и high тоже будут double. При вызове просто сработает неявное преобразование от int к double.
Также type_identity можно использовать для того, чтобы запретить вывод типов и заставить пользователя явно прописывать шаблонные параметры. Это может быть важно для точной передачи типа:
template<class T>
void foo(typename std::type_identity<T>::type arg) {}
foo<int>(42); // T жёстко задаётся как int
// foo(42); // Ошибка: вывод T невозможен!
Тоже самое для вариадиков:
template <typename... Ts>
void process(typename std::type_identity<std::tuple<Ts...>>::type data) {
}
process<int, double>(std::tuple{1, 2.0}); // OK
process(std::tuple{1, 2.0}); // ERROR, не указаны типы шаблонных параметров
Прикольный инструмент для тонкой настройки вашего шаблонного кода.
Спасибо @d7d1cd за идею для поста)
Turn off deduction when it is not needed. Stay cool.
#template #cpp20
9❤23👍15🔥9🤯1