Программирование на основе классов и шаблонов

Шаблоны

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План

  1. Принципы разработки: KISS, DRY, YAGNI, BDUF, APO и бритва Оккама.
  2. Понятие шаблона функции и шаблонной функции. Выведение типов шаблонных аргументов. Специализация шаблона функции. Примеры шаблонов функций.
  3. Понятие шаблона классов и шаблонного класса. Параметры шаблонов классов. Специализация шаблона класса. Примеры шаблонов классов.
  4. Частичная специализация.
  5. Наследование и шаблоны классов.
  6. Статические проверки.
  7. typedef и using для определения псевдонимов. Шаблон псевдонима.

Занимательный факт

Мы прошли модуль 1 "Основны ООП" и начинаем модуль 2 "Шаблоны ООП"! Мы - молодцы 👏! Посмотрите какой большой путь мы проделали:

  1. Системы сборки кроссплатформенного программного обеспечения из исходного кода
  2. Концепции и парадигмы программирования
  3. Теоретические основы ООП
  4. Описание и реализация классов
  5. Перегрузка операций и отношения между классами
  6. Наследование

А впереди ещё так много интересного!

Порадовались? Теперь хватит! 😈

Завершение модуля 1 означает, что по плану вы у вас:

  • Выполнены и сданы отчёты ЛР с №1 по №4
  • Пройдено РК №1 (обсуждение практики)

Контрольные мероприятия модуля 2

Дедлайны (план) модуля:

  • 14 неделя: ДЗ.
  • 16 неделя: выполнены и сданы отчёты ЛР с №5 по №8, РК №2 (защита практики).
  • 17 неделя: отчёт по практике.

Последствия пропуска сроков

  • Низкие оценки по ЛР (за значительную задержку).
  • Много вопросов по практике.
  • Много вопросов на экзамене.

В итоге зачетка перестанет работать на вас 😞

center

Начнем с разминки

О чем-нибудь эта картинка вам говорит?

center

А эти мантры?

KISS, DRY, YAGNI, BDUF, SOLID, APO и бритва Оккама.

P.S. Тут краткий обзор этих слов.

Давайте познакомимся с некоторыми "мантрами"!

1. YAGNI

You Aren’t Gonna Need It / Вам это не понадобится

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

Используйте всю мощь git-репозиториев и учитесь фиксировать изменения правильно! После этого не бойтесь удалять лишнее. История git вам в помощь!

2. DRY

Don’t Repeat Yourself / Не повторяйтесь

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

В большинстве случаев дублирование кода происходит из-за незнания системы.

3. KISS

Keep It Simple, Stupid / Будь проще

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

P.S. Мема здесь нет, так как этот слайд полностью удовлетворяет принципам KISS.

4. Big Design Up Front

Глобальное проектирование прежде всего

Прежде чем переходить к реализации, убедитесь, что все хорошо продумано.

5. SOLID

Не будем забегать вперед!

center

6. Avoid Premature Optimization

Избегайте преждевременной оптимизации

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

Придерживайтесь KISS и YAGNI, тогда все будет тип-топ!

P.S. Не будь Таносом и не оптимизируй чрезмерно!

7. Бритва Оккама

Не следует множить сущее без необходимости

Подумайте, нужны ли новые сущности они, поскольку они могут в конечном итоге усложнить вашу кодовую базу.

P.S. Предыдущие принципы вам в помощь!

А чем отличается сеньор разработчик от джуна?

center

К чему это всё?

А все эти принципы прекрасно подходят к модулю 2! Особенно к первой лекции из модуля.

P.S. И да, эта лекция снова не про полиморфизм =)

center

Продолжаем нагугливать качественный учебный материал!

Основано на материале от @ashtanyuk и @semenyakinVS.

Адаптированный материал публикуется с сохранением условий распространения.

Шаблоны функций

Концепция шаблонов возникла из принципа программирования Don't repeat yourself.

center

Так DRY же и без ООП хорошо живет!

Дан код:

int main() {
  const int a = 3, b = 2, c = 1;

  const int abMax = (a >= b) ? a : b;
  const int max = (abMax >= c) ? abMax : c;

  return 0;
}

Тернарный оператор:

<условие>? <если true> : <если false>

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

int max(int a, int b) { return (a >= b ? a : b); }

//...

int main() {
  const int a = 3, b = 2, c = 1;

  const int abMaxInt = max(a, b);
  const int maxInt = max(abMax, c);

  return 0;
}

Процедурное программирование делает код чище!

Однако, что если логику получения максимального элемента надо поддерживать для всех числовых типов: для всех размеров (1, 2, 4, 8 байт), как знаковых, так и беззнаковых (signed / unsigned), для чисел с плавающей точкой (float, double)?

Можно воспользоваться перегрузкой функций:

char max(char a, char b) { return (a >= b ? a : b); }

unsigned char max(unsigned char a, unsigned char b) { return (a >= b ? a : b); }

short int max(short int a, short int b) { return (a >= b ? a : b); }

unsigned short int max(unsigned short int a, unsigned short int b) {
  return (a >= b ? a : b);
}

int max(int a, int b) { return (a >= b ? a : b); }

unsigned int max(unsigned int a, unsigned int b) { return (a >= b ? a : b); }

// ... и т.д. для всех числовых типов, включая "float" и "double"...

Выглядит мягко говоря громоздко!

В игру вступают шаблоны!

Пример max.h:

template <typename Type>
Type max(Type a, Type b) {
  return (a >= b ? a : b);
}

Пример main.cpp:

#include <iostream>

#include "max.h"

int main() {
  const int abMax = max<int>(3, 2);
  const char abMaxChar = max<char>(3, 2);
  return 0;
}

"Под капотом" компилятора

Использование шаблона выглядит так: max<int>(a, b).

После подстановки компилятор создаст "под капотом" конкретную функцию из обобщённого кода. То, что вызывается по записи max<int>(), для компилятора выглядит так:

int max<int>(int a, int b) { 
  return (a >= b ? a : b); 
}

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

Терминология для шаблонов функций

  • Обобщённое описание функции называется шаблоном функции. Шаблон без подстановки конкретного типа не превращается в реальный код.
  • Сгенерированную конкретную функцию по шаблону называют шаблонной функцией.

Щепотка "умных" слов

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

Про "умные" слова

Бывают люди, которые всегда говорят только умные и хорошие слова, но чувствуешь, что они тупые люди.

Антон Павлович Чехов. Палата № 6

Выведение типов шаблонных аргументов

Вызов шаблонной функции из примера:

// const int a = 3, b = 2;
const int abMax = max<int>(a, b);

можно записать, опустив <int>:

// const int a = 3, b = 2;
const int abMax = max(a, b);

Такая запись корректна с точки зрения языка. Компилятор проанализирует типы переменных a и b и выполнит выведение типа для передачи в качестве значения шаблонного аргумента Type.

Но вот такое:

const int a = 1;
const char bChar = 'b';
const int abMax = max(a, bChar);

не скомпилируется:

candidate template ignored: deduced conflicting types for parameter 'Type' ('int' vs. 'char')

Компилятор не может однозначно определить какой тип надо передать в качестве значения аргумента Type. У него есть вариант подставить тип int или тип char. Непонятно какая из подстановок ожидается программистом.

Чтобы избавиться от этой проблемы, можно применить явную передачу типа в шаблон:

const int a = 1;
const char bChar = 'b';
const int abMax = max<int>(a, bChar);

Deduce, You Say!

P.S. deduce - с англ. сделать вывод, итожить.

Deduce, You Say!

Deduce, You Say! - короткометражный фильм 1956 "Луни Тюнз", снятый Чаком Джонсом.

Название является игрой на фразе "черт, что ты говоришь", а "deduce" отсылает к склонности Шерлока Холмса к дедуктивным рассуждениям.

Специализация шаблона функции

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

#include <iostream>

template <typename T>
void Echo(T value) {
  std::cout << value << std::endl;
}

int main() {
  // Печатаем значения
  Echo<int>(2);       // 2
  Echo<double>(2.5);  // 2.5
}

Специализация для double:

template <>
void Echo(double value) {
  std::cout << std::scientific << value << std::endl;
}

Когда компилятор переходит к созданию экземпляра Echo<double>(), он увидит, что мы уже явно определили эту функцию, и будет использовать версию, которую определили мы, вместо того, чтобы создавать версию из обобщенного образца из шаблонного класса.

template<> сообщает компилятору, что это шаблон функции, но у которой нет никаких шаблонных параметров (поскольку в этом случае мы явно указываем все типы). Некоторые компиляторы могут позволить вам опустить эту запись, но с ней будет правильнее.

В результате программа напечатает:

2
2.500000e+00

Шаблоны классов

Заголовок:

template <typename T>
class MyType {
  T value;

 public:
  void setValue(const T& newValue) { value = newValue; }
  T& setValue() { return value; }
};

Использование:

  MyType<int> a;
  a.setValue(5);

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

Встретив запись MyType<int> в первый раз, по шаблону класса будет порождён новый шаблонный класс.

center

Терминология для шаблонов классов

  • Обобщённое описание называется шаблоном класса.
  • После передачи типа в качестве шаблонного аргумента из шаблона класса порождается новый шаблонный класс. Во всех местах, где выполняется подстановка того же типа, будет подразумеваться один и тот же тип шаблонного класса.

Еще один пример шаблона класса

Объявление:

template <class T>
class Stack {
  int size; int top;  T* store;

 public:
  Stack(int size);
  ~Stack();
  Stack(const Stack&);

  void push(T);
  T pop();

  Stack& operator=(const Stack&);
};

Реализация методов шаблона класса можно писать в пределах области класса, а можно выносить за эту область:

template <class T>
Stack<T>::Stack(int _size) : size(_size), top(0) {
  store = new T[size];
}

template <class T>
Stack<T>::~Stack() {
  delete[] store;
}
template <class T>
void Stack<T>::push(T value) {
  if (top < size) store[top++] = value;
}

template <class T>
T Stack<T>::pop() {
  return top ? store[--top] : 0;
}

Но есть нюанс...

Описание самого шаблона класса и реализация его методов должно помещаться внутрь заголовочных файлов. Такого "классического" разделения на .h и .cpp здесь нет, поскольку описание шаблона не может быть единицей трансляции, в отличие от описания методов обычного класса.

Объяснение этого смотри здесь.

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

Параметры шаблонов класса

У шаблонов классов может быть несколько параметров:

template <typename T1, typename T2>
class MyPair {
  T1 value1;
  T2 value2;

 public:
  MyPair(const T1& val1, const T2& val2) {
    value1 = val1;
    value2 = val2;
  }
  ...
};
...
MyPair<int, char> pair(5, 'a');

Могут быть параметры различных типов:

template <class T, int size>
class Buffer {
  T array[size];

 public:
  T* get(int i) { return (i >= 0 && i < size) ? &array[i] : 0; }
};

Получения шаблонного класса:

Buffer<char, 128> buf;

В шаблонах допускается использование различных видов параметров:

template <class T1,                  // параметр-тип
          typename T2,               // параметр-тип
          int I,                     // параметр обычного типа
          T1 DefaultValue,           // параметр обычного типа
          template <class> class T3, // параметр-шаблон
          class Character = char     // параметр по умолчанию
          >

Вот так на практике:

template <class Type, template <class> class Container>
class CrossReferences {
  Container<Type> mems;
  Container<Type*> refs;
  /* ... */
};
CrossReferences<Date, vector> cr1;
CrossReferences<string, set> cr2;

Специализации шаблонов классов

template <class T>
class Bag {
  T* elem; int size; int max_size;

 public:
  Bag() : elem(0), size(0), max_size(1) {}
  void add(T t) {
    T* tmp;
    if (size + 1 >= max_size) {
      max_size *= 2;
      tmp = new T[max_size];
      for (int i = 0; i < size; i++) tmp[i] = elem[i];
      tmp[size++] = t;
      delete[] elem;
      elem = tmp;
    } else
      elem[size++] = t;
  }
  void print() {
    for (int i = 0; i < size; i++) std::cout << elem[i] << " ";
    std::cout << std::endl;
  }
};
//...
//...
template <class T>
class Bag<T*> {
  T* elem; int size; int max_size;

 public:
  Bag() : elem(0), size(0), max_size(1) {}
  void add(T* t) {
    T* tmp;
    if (t == NULL) {  // Check for NULL
      std::cout << "Null pointer!" << std::endl;
      return;
    }
    if (size + 1 >= max_size) {
      max_size *= 2;
      tmp = new T[max_size];
      for (int i = 0; i < size; i++) tmp[i] = elem[i];
      tmp[size++] = *t;  // Dereference delete[] elem;
      elem = tmp;
    } else
      elem[size++] = *t;  // Dereference
  }
  void print() {
    for (int i = 0; i < size; i++) std::cout << elem[i] << " ";
    std::cout << std::endl;
  }
};

Использование:

int main() {
  Bag<int> xi; Bag<char> xc; Bag<int*> xp;

  xi.add(10); xi.add(9); xi.add(8);
  xi.print();  // 10 9 8
  xc.add('a'); xc.add('b'); xc.add('c');
  xc.print();  // a b c
  int i = 3, j = 87, *p = new int[2];
  *p = 8; *(p + 1) = 100;
  xp.add(&i); xp.add(&j); xp.add(p); xp.add(p + 1);
  p = NULL;
  xp.add(p);   // Null pointer!
  xp.print();  // 3 87 8 100
}

Частичная специализация

При частичной специализации указываются значения не для всех параметров шаблона. Например:

// шаблон класса
template <typename T, typename K>
class Person {
  T id;
  std::string name;
  K phone;

 public:
  Person(std::string name, K phone) : name{name}, phone{phone} {}
  void setId(T value) { id = value; }
  void print() const {
    std::cout << "Id: " << id << "\tName: " << name << "\tPhone: " << phone
              << std::endl;
  }
};
// ...

Частичная специализация шаблона для типа unsigned:

template <typename K>
class Person<unsigned, K> {
  static inline unsigned count{};
  unsigned id;
  std::string name;
  K phone;

 public:
  Person(std::string name, K phone) : name{name}, phone{phone} { id = ++count; }
  void print() const {
    std::cout << "Id: " << id << "\tName: " << name << "\tPhone: " << phone
              << std::endl;
  }
};

Использование:

int main() {
  Person<std::string, std::string> bob{"Bob",
                                       "+1234567688"};  // T - std::string
  bob.setId("13");
  bob.print();  // Id: 13  Name: Bob       Phone: +1234567688

  Person<unsigned, std::string> tom{"Tom", "+4444444444"};
  tom.print();  // Id: 1   Name: Tom       Phone: +4444444444

  Person<unsigned, std::string> sam{"Sam", "+555555555"};
  sam.print();  // Id: 2   Name: Sam       Phone: +555555555
}

Наследование и шаблоны классов

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

template <typename T>
class Person {
 public:
  Person(T id, std::string name) : id{id}, name{name} {}
  void print() const {
    std::cout << "Id: " << id << "\tName: " << name << std::endl;
  }

 protected:
  T id;
  std::string name;
};
//...

Потомок:

//...
template <typename T>
class Employee : public Person<T> {
  std::string company;

 public:
  Employee(T id, std::string name, std::string company)
      : Person<T>{id, name}, company{company} {}
  void print() const {
    Person<T>::print();
    std::cout << Person<T>::name << " works in " << company << std::endl;
  }
};

Использование:

int main() {
  Employee<unsigned> bob{123, "Bob", "Google"};
  bob.print();  // Id: 123 Name: Bob
                // Bob works in Google
}

В данном случае в начале определен шаблон базового класса Person, который использует параметр шаблона T для установки типа для переменной id. Далее определен шаблон класс Employee, который наследуется от класса Person.

Можем явно устанавливать типы:

//...
class Employee : public Person<unsigned> {
  std::string company;

 public:
  Employee(unsigned id, std::string name, std::string company)
      : Person{id, name}, company{company} {}
  void print() const {
    Person::print();
    std::cout << name << " works in " << company << std::endl;
  }
};

Использование:

int main() {
  Employee bob{123, "Bob", "Google"};
  bob.print();  // Id: 123 Name: Bob
                // Bob works in Google
}

В данном случае класс Employee представляет обычный класс, который наследуется от типа Person<unsigned>. То есть теперь для функционала базового класса параметр T будет представлять тип unsigned.

Статические проверки

Начиная с C++11, появилась возможность создавать статические проверки, выполняющиеся во время построения программы:

template <int P>
struct fact {
  static_assert(P > 0, "a positive value expected");
  static const int value = P * fact<P - 1>::value;
};
template <>
struct fact<0> {
  static const int value = 1;
}

typedef для определения псевдонимов

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

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

// typedef <current_name> <new_name>
typedef std::vector<int> vInt;

using для определения псевдонимов

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

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

// using <member_name>
using std::cout;

Псевдоним не вводит новый тип и не может изменить значение существующего имени типа.

Простейшая форма псевдонима эквивалентна механизму typedef C++03:

// C++11
using counter = long;

// C++03 эквивалент:
// typedef long counter;

using позволяет писать более красивый код и позволяет избавиться от наследия typedef, который имеет в своём имени def, который может подразумевать definition, которого на самом деле не происходит. using это замена typedef с некоторыми добавками, каких typedef не имеет.

В современном коде использовать typedef нет никакого смысла.

Псевдонимы также работают с указателями функций, но гораздо более удобочитаемыми, чем эквивалентный typedef:

// C++11
using func = void(*)(int);

// C++03 эквивалент:
// typedef void (*func)(int);

// func может быть присвоено значению указателя функции
void actual_function(int arg) { /* некоторый код */ }
func fptr = &actual_function;

Шаблон псевдонима

Ограничение typedef механизма заключается в том, что он не работает с шаблонами.using типа в C++11 позволяет создавать шаблоны псевдонимов:

template<typename T>
using ptr = T*;

// имя 'ptr<T>' теперь является псевдонимом для указателя на T
ptr<int> ptr_int;

Ещё один пример использования using с шаблонами:

template <typename T>
struct settype {
  using type = T;
};

template <typename T>
struct settype<const T> {
  using type = T;
};

settype<int>::type a1;
settype<const int>::type a2;

P.S. Подробное про псевдонимы и шаблон псевдонима Microsoft Learn и cppreference.com.

Вопросы?

center

If not, just clap your hands!