Skip to content

Instantly share code, notes, and snippets.

@glensand
Last active February 13, 2021 08:56
Show Gist options
  • Save glensand/33fd95588c971a0860d6de4b486fb792 to your computer and use it in GitHub Desktop.
Save glensand/33fd95588c971a0860d6de4b486fb792 to your computer and use it in GitHub Desktop.
Введение в шаблоны с++

Шаблон

Вместо введения

Само слово шаблон дает понимание этого механизма. Язык позволяет создавать нечто обобщенное, эксплуатирующее статический полиморфизм, при помощи этого механизма. Равно как и в языках разметки текста, в с++ шаблоны языковых единиц имеют аргументы, которые будут подставлены (инстанциированы). До этого этапа шаблонный код стоит рассматривать как синтаксически (с точки зрения ЯП) верный "текст". На этапе инстанциации аргументы шаблона подставляются как есть, генерируется (условно) код, который может быть скомпилирован в объектный файл.

Аргументы шаблона

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

template <typename T>
class ValueHolder
{
  public:
  T GetValue()  { return m_value; }
  private:
  T m_value;
}

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

// example.cpp
// ...
ValueHolder<int> holderInstance;

При компиляции этого translation unit (коими в с++ являются файлы исходного кода), получается что-то похожее на следующий сниппет:

class ValueHolder_int32
{
  public:
  int GetValue()  { return m_value; }
  private:
  int m_value;
}

После подстановки компилятор сможет запустить семантические проверки и выдать предупреждения или ошибки, в случае если полученный код не является валидным. С нетиповыми параметрами ситуация аналогична. Не трудно догадаться, что для успешной работы обобщенного кода (или метакода) объявление и определение оного должны быть доступны в модуле трансляции, в котором происходит инстанциация. Иными словами, в отличае от обычного кода, шаблонный код, зачастую приходится полностью помещать в заголовочные файлы: ровно так реализована стандартная библиотека шаблонов (stl).

Правила выведения типа

Для прочтения этой части необходимо понимать и отличать rvalue и lvalue, хорошим плюсом будет знание семантики перемещения. Я рекомендую статью. Правила выведения справедливы для универсальных ссылок, или universal reference (реже передающая ссылка). Для начала определимся что является и что не является универсальной ссылкой, затем поговорим о том почему она называется именно так:

template <typename T>
void do(T&& arg){ // arg является универсальньй ссылкой
    auto&& arg_ref_ref = arg; // arg_ref_ref так же является универсальной ссылкой
}

Очевидно, внешний вид совершенно как у rvalue ссылок. Но это не rvalue ссылки! Эти красавицы могут быть связаны с объектами любого типа и любой ссылочности, включая const, volatile, const T& и любые иные комбинации. В то время как, rvalue ссылка может быть связана только с rvalue, не константная lvalue ссылка может быть связана только с lvalue! Но внешность бывает обманчива, следующие вещи не являются universal reference:

template<typename T>
class list final{
    public:
    void puch_back(T&& val);    // val является rvalue
                                // так как код метода будет сгенерирован в момент инстанциации класса определенным типом, и при вызове метода он уже не будет считаться шаблоном,
                                // хотя и находится внутри щаблона класса, следовательно никакого выведения происходить не будет
}

template <typename T>
void do(std::list<T>&& list){   // list не является универсальной ссылкой, хотя выведение типа T и происходит, результирующий тип list является зависимым, потому list это rvalue ссылка.
                                // это лекго проверить, попытаться передать в эту функцию lvalue список, компилятор выдаст ошибку так как не сможет найти подходящую перегрузку
    // ...
}

Чаще всего universal reference можно встретить в шаблоне функций, которые передают свои аргументы дальше (реже используеют их напрямую), поэтому наблюдается такая тесная связь с std::forward. Легче всего осознать правила выведения на простых примерах, которые впоследствие могут быть сколько угодно расширены (лучше делать это мысленно):

template <typename T>
void do(T&& param){
    // ... some relevant work
}
widget w;
widget& w_ref = w;
const widget& w_const_ref = w;
const widget w_const = w;

do(w); // Тип param : Widget&, тип T : Windget&
do(w_ref); // Тип param : Widget&, тип T : Windget&
do(w_const_ref); // Тип param : const Widget&, тип T : const Windget&
do(w_const; // Тип param : const Widget&, тип T : const Windget&
do(std::move(w)); // Тип param : Widget&&, тип T : Windget
do(widget{}); // Тип param : Widget&&, тип T : Windget

const widget&& w_const_ref_ref = q;
do(w_const_ref_ref); // Тип param : const Widget&&, тип T : const Windget
// Аналогично с auto&&...

Прямая передача

Прямая передача неразрывно связана с семантикой перемещения, и используется для ее реализации (там где это необходимо). Сперва читателю нужно запомнить что функции std::mode и std::forward ничего не перемещают и не передают, они выполняют лишь приведения типов переданных аргументов: std::move безусловно приводит аргумент к rvalue ссылке, std::forward выполняет условное приведение, в зависимости от типа, которым она была инстанциирована. И нужно это вот зачем:

template <typename T>
void perf_critical_func(T&& arg);

template <typename T>
void do_work(T&& arg){
    // Согласна одному из определений, у rvalue объекта нельзя взять адрес, у переменной arg, которая может являться ссылкой на rvalue 
    // адрес взять можно, потому перед передачей объекта дальше, сслыка должна быть сконвертирована в rvalue ссылку, если это возможно.
    perf_critical_func(std::forward<T>(arg));
}

Шаблоны с переменным количеством аргументов

Или просто вариадики (parameter pack) сверхполезная вещь, без которой метапрограммирование и повседневная разработка были намного более скушные, перед разработчиком вставала диллема: использовать макросы, перегружать операторы(boost::assign) или обрезать функционал и оставлять код чистым. Пакеты параметров разрешили эту проблему. Описание функции, принимающей пакет параметров будет выглядеть так:

template <typename... Ts>
void emplace(Ts... args); 

Оператор ... (элипсис), который может быть знаком читателю по языку С, здесь имеет иное значение, которое тоже зависит от контекста. Совместно с ключевым словом typename элипсис указывает на то, что перед нами пакет типов, в аргументах функции - пакет аргументов. Как его использовать? Для непосредственного применения пакет должен быть раскрыт, существует несколько контекстов, в которых можно провернуть такой трюк:

  • Первое что приходит на ум - вызов функции с передачей параметров
    template <typename... Ts>
    void foo(Ts... args){
        bar(args...); // При помощи элипсиса пакет будет раскрыт, в функцию bar передадутся параметры, по одному
    }
    Для понимания этого явления лучше всего представить себя компилятором, и попытаться вызвать эту функцию и сгенерировать код, так и посутупим:
    //..
    void run(){
        foo("fizzbuzz", 1, 2.f);
    }
    Будет сгенерирован прмерно такой код(пример условный, параметры шаблона оставлены намерено, однако, на этом этапе в них нет смысла):
    template <typename const char*, typename int, typename float>
    foo(const char* _1, int _2, float _3){
        bar(_1, _2, _3)
    }
    Рекомендую внимательно изучить пример и попытаться провернуть тот же трюк ещё несколько раз, придумаю все более сложные случаи.
  • Менее очевидный и распространенный, однако чрезвычайно полезный контекст - миксинг, или наследование пакета параметров
    template<typename... Ts>
    class derived : public Ts...{
        // ...
    };
  • Так же операция контекстом может являться применение бинарных и унарных операторов:
    // args - variadic pack
    const auto value = rand() % 5 == 0;
    const auto res = (value || args...); // пакет раскроется и операция применится к каждому элементу (поэлементно)

Шаблоны с переменным количеством аргументов дают возможность создавать методы для in-place конструирования объектов. Примерно так устроен emplace_back во многих контейнерах stl:

// ...
template <typename T>
class vector final { 
    public:
    // ...
    template <typename... Ts>
    void emplace_back(Ts... args){
        auto* buff = get_buff(); // получили свободный буфер в конце вектора, сделали ресайз если нужно
        new (buf) T(std::forward<Ts>(args)...); 
        // Это компилляция самых важных знаний, здесь используется универсальная ссылка, прямая передача и распаковывается пакет                 // аргументов
    }
    // ...
}

Упражнение:

  • Вектор (саморасширяющийся динамический массив) Реализовать вектор (итератор пока не требуется), набор тестов. Код должен быть приемлемого качества, так как он необходим для выполнения последующих заданий. NOTE: Отличие метода at от [] заключается в том, что "at" бросает исключение если запрошенный элемент не существует, оператор [] игнорирует такую ситацию. Интерфейс:
template <typename T>
class vector final
{
  public:
  
  vector() = default;
  vector(std::size_t initial_size);
  
  const T& at(std::size_t index) const;
  T& at(std::size_t index);
  
  const T& operator[](std::size_t index) const;
  T& operator[](std::size_t index);
  
  void push_back(const T& value);
  
  void erase(std::size_t index);
  
  std::size_t size() const;
  
  private:
  //...
};
  • Итератор вектора. Концепцию обобщенного доступа и обобщенных алгоритмов долгое время разрабатывал Алекс Степанов, что впоследствие вылилось в фундаментальный труд "Начала программирования", и стандартную библиотеку шаблонов в общем, ее замечательную часть <algorithm> в частности. В этом пункте нужно реализовать итератор для контейнера vector (сначала сделаем biderectional). Дополняем интерфейс следующим образом (некоторые методы опущены намерено):
template <typename T>
class vector final
{
public:
    class iterator final
    {
    public:
        const T& operator->() const;
        T& operator->();
        
        const T& operator*() const;
        T& operator*();
    
        T& operator++();       // Prefix
        T operator++(int);     // Postfix
        
        T& operator--();
        T operator--(int);
        
        bool operator==(const iterator& rhs) const;
        bool operator!=(const iterator& rhs) const;
        bool operator<(const iterator& rhs) const;
    }
    
    //..
    iterator begin();
    iterator end();
}
  • Добавить метод emplace_back
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment