Skip to content

Instantly share code, notes, and snippets.

@eao197
Last active April 10, 2024 10:03
Show Gist options
  • Save eao197/246470e171756322c4c612092f51e863 to your computer and use it in GitHub Desktop.
Save eao197/246470e171756322c4c612092f51e863 to your computer and use it in GitHub Desktop.
Примеры для второй части лекции про шаблоны C++
/***
SFINAE: Substitution failure is not an error
*/
// В основном базируется на std::enable_if
// https://en.cppreference.com/w/cpp/types/enable_if
#include <type_traits>
int main() {
using T1 = std::enable_if<8 == sizeof(void*), long>::type;
using T2 = std::enable_if<4 == sizeof(int)>::type;
static_assert(std::is_same_v<long, T1>);
static_assert(std::is_same_v<void, T2>);
}
// Обычно вместо std::enable_if<C, T>::type пишут std::enable_if_t<C, T>.
#include <iostream>
#include <cmath>
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, bool>
is_equal(T a, T b)
{
return std::abs(std::abs(a) - std::abs(b)) < 0.0001;
}
template<typename T>
std::enable_if_t<std::is_integral_v<T>, bool>
is_equal(T a, T b)
{
return a == b;
}
int main()
{
std::cout << " int: " << is_equal(1, 1) << std::endl;
std::cout << "double: " << is_equal(0.0, 0.0000002) << std::endl;
}
/***
Может иметь несколько форм:
enable_if для возвращаемого значения;
enable_if для параметра шаблона по умолчанию;
enable_if для аргумента функции/метода по умолчанию.
*/
// Пример использования enable_if в параметре шаблона.
// Чтобы можно было написать auto в качестве типа возвращаемого значения.
#include <iostream>
#include <type_traits>
template<typename A, typename B,
typename D = std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>> >
auto
add(A a, B b) { return a+b; }
template<typename A, typename B,
typename D = std::enable_if_t<std::is_floating_point_v<A> && std::is_floating_point_v<B>> >
float
add(A a, B b) { return static_cast<float>(a +b); }
int main() {
auto r1 = add(1, 2l);
static_assert(std::is_same_v<long, decltype(r1)>);
auto r2 = add(2.0, 3.0f);
static_assert(std::is_same_v<float, decltype(r2)>);
}
// При определении шаблонного конструктора.
// Пример простого шаблонного конструктора.
#include <iostream>
#include <type_traits>
#include <string>
template<typename T>
class ValueHolder
{
T _value;
public:
template<typename U>
ValueHolder(U && v) : _value{std::forward<U>(v)}
{
std::cout << "template constructor" << std::endl;
}
[[nodiscard]] const T &
value() const { return _value; }
};
int main() {
ValueHolder<std::string> v1{"Hello"};
std::cout << "v1: " << v1.value() << std::endl;
ValueHolder<std::string> v2{std::string{"World"}};
std::cout << "v2: " << v2.value() << std::endl;
}
// Ведет к ошибке компиляции вот в таком случае:
//
// ValueHolder<std::string> v3{v2};
//
// Версия с SFINAE
#include <iostream>
#include <type_traits>
#include <string>
template<typename T>
class ValueHolder
{
T _value;
public:
template<typename U,
typename Guard = std::enable_if_t<
!std::is_same_v<std::decay_t<U>, ValueHolder>
>
>
ValueHolder(U && v) : _value{std::forward<U>(v)}
{
std::cout << "template constructor" << std::endl;
}
[[nodiscard]] const T &
value() const { return _value; }
};
int main() {
ValueHolder<std::string> v1{"Hello"};
std::cout << "v1: " << v1.value() << std::endl;
ValueHolder<std::string> v2{std::string{"World"}};
std::cout << "v2: " << v2.value() << std::endl;
ValueHolder<std::string> v3{v2};
std::cout << "v3: " << v3.value() << std::endl;
}
// Используется std::decay: https://en.cppreference.com/w/cpp/types/decay
//
// Performs the type conversions equivalent to the ones performed when passing
// function arguments by value.
//
// В частности, для нашего случая: Otherwise, the member typedef type is
// std::remove_cv<std::remove_reference<T>::type>::type.
//
// SFINAE посредством параметра по умолчанию
//
#include <iostream>
#include <type_traits>
template<typename A, typename B>
auto
add(A a, B b,
std::enable_if_t<std::is_integral_v<A> && std::is_integral_v<B>> * = nullptr)
{ return a+b; }
template<typename A, typename B>
float
add(A a, B b,
std::enable_if_t<std::is_floating_point_v<A> && std::is_floating_point_v<B>> * = nullptr )
{ return static_cast<float>(a+b); }
int main() {
auto r1 = add(1, 2l);
static_assert(std::is_same_v<long, decltype(r1)>);
auto r2 = add(2.0, 3.0f);
static_assert(std::is_same_v<float, decltype(r2)>);
}
/////////////////////////////////////////////////////
/***
Случай, когда приходится использовать нотацию this->member.
*/
#include <iostream>
template<typename T>
struct Base {
T _x;
};
template<typename T>
struct Derived : public Base<T> {
void print() const {
std::cout << _x << std::endl;
}
};
int main() {
Derived<int> v{10};
v.print();
}
// Ошибка компиляции, т.к. в Derived<T>::print имя _x неизвестно.
// Возможные исправления:
// Вариант №1
void print() const {
std::cout << this->_x << std::endl;
}
// Вариант №2
template<typename T>
struct Derived : public Base<T> {
using Base<T>::_x;
void print() const {
std::cout << _x << std::endl;
}
};
/////////////////////////////////////////////////////
/***
Лямбда-функции и шаблоны
Если нам нужно куда-то передать лямбду в качестве параметра, то либо std::function,
либо шаблон. Но std::function -- это аналог std::shared_ptr, за std::function может
скрываться невидимая пользователю аллокация памяти.
*/
#include <iostream>
template<typename L>
auto CallLambda(L && lambda) { return lambda(); }
struct Demo {
Demo() = default;
Demo(const Demo &) { std::cout << "Demo copy ctor" << std::endl; }
Demo & operator=(const Demo &) {
std::cout << "Demo copy operator" << std::endl;
return *this;
}
};
template<typename L>
decltype(auto) CallLambda2(L && lambda) { return lambda(); }
int main()
{
Demo a;
const auto & b1 = CallLambda([&a]() -> Demo & { return a;});
(void) b1;
const auto & b2 = CallLambda2([&a]() -> Demo & { return a;});
if(&a == &b2) std::cout << "same object" << std::endl;
}
// Отдельная история происходит когда нам нужно сохранить лямбду как поле
// какого-то класса. Простой вариант -- std::function, но если не подходит, то...
// Проблемный вариант:
#include <iostream>
#include <type_traits>
template<typename Lambda>
class LambdaHolder
{
Lambda _lambda;
public:
LambdaHolder(Lambda && lambda) : _lambda{std::move(lambda)} {}
decltype(auto) Call(int v) { return _lambda(v); }
};
template<typename Lambda>
[[nodiscard]] auto
MakeHolder(Lambda && lambda)
{
using LambdaType = std::decay_t<Lambda>;
return LambdaHolder<LambdaType>{std::move(lambda)};
}
void Func(int v) { std::cout << "func: " << v << std::endl; }
int main() {
const auto first = [](int v) { std::cout << "first: " << v << std::endl; };
auto second = [](int v) {
std::cout << "second: " << v << std::endl;
return v+1;
};
MakeHolder(Func).Call(0);
// MakeHolder(first).Call(1);
MakeHolder(second).Call(2);
MakeHolder([&first](int v) { first(v+10); }).Call(3);
}
// Проблема в том, что first -- это const.
// Решение:
template<typename Lambda>
class LambdaHolder
{
Lambda _lambda;
public:
template<typename ActualLambda>
LambdaHolder(ActualLambda && lambda) : _lambda{std::forward<ActualLambda>(lambda)} {}
decltype(auto) Call(int v) { return _lambda(v); }
};
/////////////////////////////////////////////////////
/***
Variadic templates
*/
template<typename... TArgs>
void f(TArgs ...args) {}
// Разница между TArgs... args и TArgs && ...args:
#include <iostream>
template<typename... Args>
void Handle(Args... /*args*/) {}
struct Demo {
Demo() = default;
Demo(const Demo &) { std::cout << "Demo copy ctor" << std::endl; }
Demo & operator=(const Demo &) {
std::cout << "Demo copy operator" << std::endl;
return *this;
}
};
int main() {
Demo d1, d2, d3;
Handle(d1, d2, d3);
}
// Второй вариант:
#include <iostream>
template<typename... Args>
void Handle(Args && .../*args*/) {}
struct Demo {
Demo() = default;
Demo(const Demo &) { std::cout << "Demo copy ctor" << std::endl; }
Demo & operator=(const Demo &) {
std::cout << "Demo copy operator" << std::endl;
return *this;
}
};
int main() {
Demo d1, d2, d3;
Handle(d1, d2, d3);
}
// Для того, чтобы развернуть parameter pack нужно записать pack...
#include <iostream>
template<typename... Args>
void Handle(Args && ...args) {
int values[]{args...};
for(int v : values) std::cout << v << std::endl;
}
int main() {
Handle(5, 6, 7, 8);
}
// Узнать размер pack-а можно посредством sizeof...(pack):
#include <iostream>
#include <array>
template<typename... Args>
void Handle(Args && ...args) {
std::array<int, sizeof...(args)> values{args...};
for(int v : values) std::cout << v << std::endl;
}
int main() {
Handle(5, 6, 7, 8);
}
// При распаковке pack-а можно делать и вызов функции:
#include <iostream>
#include <array>
int square(int v) { return v * v; }
template<typename... Args>
void Handle(Args && ...args) {
std::array<int, sizeof...(args)> values{square(args)...};
for(int v : values) std::cout << v << std::endl;
}
int main() {
Handle(5, 6, 7, 8);
}
// Можно завершать рекурсию посредством терминальных функций и
// посредством if constexpr.
// Терминальная функция:
#include <iostream>
#include <array>
namespace min_impl {
template<typename T>
T GetMinElement(T currentMin) {
return currentMin;
}
template<typename T, typename... Tail>
T GetMinElement(T currentMin, T toCompare, Tail ...tail) {
return GetMinElement(currentMin < toCompare ? currentMin : toCompare, tail...);
}
} /* namespace min_impl */
template<typename T, typename... Tail>
T MinElement(T head, Tail ...tail) {
return min_impl::GetMinElement(head, tail...);
}
int main() {
std::cout << MinElement(10) << std::endl;
std::cout << MinElement(10, 0) << std::endl;
std::cout << MinElement(-1, 0) << std::endl;
std::cout << MinElement(-1, 0, -2) << std::endl;
std::cout << MinElement(10, 20, 30, 5, 0, -1, 100, 50) << std::endl;
std::cout << MinElement(10, 20, 30, 40, 50) << std::endl;
std::cout << MinElement(50, 40, 30, 20, 10) << std::endl;
}
// Использование if constexpr:
#include <iostream>
#include <array>
namespace min_impl {
template<typename T, typename... Tail>
T GetMinElement(T currentMin, T toCompare, Tail ...tail) {
if constexpr(0 == sizeof...(tail))
return toCompare < currentMin ? toCompare : currentMin;
else
return GetMinElement(GetMinElement(currentMin, toCompare), tail...);
}
} /* namespace min_impl */
template<typename T, typename... Tail>
T MinElement(T head, Tail ...tail) {
if constexpr(0 == sizeof...(tail))
return head;
else
return min_impl::GetMinElement(head, tail...);
}
int main() {
std::cout << MinElement(10) << std::endl;
std::cout << MinElement(10, 0) << std::endl;
std::cout << MinElement(-1, 0) << std::endl;
std::cout << MinElement(-1, 0, -2) << std::endl;
std::cout << MinElement(10, 20, 30, 5, 0, -1, 100, 50) << std::endl;
std::cout << MinElement(10, 20, 30, 40, 50) << std::endl;
std::cout << MinElement(50, 40, 30, 20, 10) << std::endl;
}
// Использование variadic templates для perfect forwarding.
#include <iostream>
#include <memory>
#include <string>
template<typename T, typename... Args>
std::unique_ptr<T> MyMakeUnique(Args && ...args) {
return std::unique_ptr<T>{ new T(std::forward<Args>(args)...) };
}
int main() {
auto s1 = MyMakeUnique<std::string>(std::size_t{20}, 'A');
std::cout << *s1 << std::endl;
auto s2 = MyMakeUnique<std::string>(*s1 + "-tail");
std::cout << *s2 << std::endl;
}
// Можно обратить внимание на использование круглых скобок при вызове
// конструктора T внутри MyMakeUnique.
/////////////////////////////////////////////////////
/***
Fold expression -- https://en.cppreference.com/w/cpp/language/fold
( pack op ... ) (1) -> Unary right fold.
( ... op pack ) (2) -> Unary left fold
( pack op ... op init ) (3) -> Binary right fold
( init op ... op pack ) (4) -> Binary left fold
Раскрывается в:
1) Unary right fold (E op ...) -> (E1 op (... op (EN-1 op EN)))
2) Unary left fold (... op E) -> (((E1 op E2) op ...) op EN)
3) Binary right fold (E op ... op I) -> (E1 op (... op (EN−1 op (EN op I))))
4) Binary left fold (I op ... op E) -> ((((I op E1) op E2) op ...) op EN)
*/
#include <iostream>
template<typename... Args>
auto sum(Args ...args) {
return (... + args);
}
template<typename T>
T square(T v) { return v * v; }
template<typename... Args>
auto sum_squares(Args ...args) {
return (... + square(args));
}
int main() {
std::cout << sum(1, 2, 3, 4.3) << std::endl;
std::cout << sum_squares(1, 2, 3, 4.3) << std::endl;
}
// Нужно учитывать приоритеты операций если в fold expression есть начальное значение:
template<typename... Args>
int sum(Args&&... args)
{
// return (args + ... + 1 * 2); // Error: operator with precedence below cast
return (args + ... + (1 * 2)); // OK
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment