- •1. Абстракция и декомпозиция. Основные виды декомпозиции программ.
- •Модульная декомпозиция
- •В заголовочный файл не следует помещать элементы реализации модуля, в том числе и внутренние функции, которые необходимы для реализации, однако не существенны для клиентского когда модуля.
- •Объектная декомпозиция
- •2. Понятие класса и объекта. Переменные-члены и функции-члены. Обращение к членам класса через объект. Указатель this. Константные функции-члены.
- •3. Спецификаторы доступа. Понятие инкапсуляции. Отличие конструкций class и struct. Методы доступа.
- •4. Конструкторы классов, синтаксис, разновидности, моменты вызова конструкторов. Роль конструкторов в соблюдении инвариантов классов.
- •5. Конструкторы по умолчанию (default constructors). Тривиальные и нетривиальные сгенерированные конструкторы классов. Конструирование массивов объектов.
- •6. Списки инициализации. Синтаксис, отличие от присвоений в теле конструктора, необходимость в существовании.
- •7. Деструкторы классов, синтаксис, цель, моменты вызова деструкторов.
- •8. Моменты копирования объектов. Поведение по умолчанию. Конструктор копий и оператор копирующего присвоения.
- •9. Временные объекты. Явные и неявные конструкторы. Оптимизации rvo/nrvo. Временные объекты
- •Неявные и явные конструкторы
- •Запрещение копирования
- •Оптимизация копирования
- •10. Основные отличия между классами-значениями и классами-сущностями. Запрещение копирования объектов. Основные отличия между классами-значениями и классами-сущностями.
- •11. Перемещение объектов. Конструктор перемещения и оператор перемещающего присвоения. Понятие rvalue-ссылки. Функция std::move.
- •12. Перегрузка операторов. Оправданное и неоправданное использование. Пример перегрузки простейшего оператора. Операторы, которые нельзя перегружать.
- •13. Внутриклассовые и глобальные перегруженные операторы. Перегрузка операторов сдвига. Применение перегрузки сдвига для взаимодействия с потоками ввода/вывода.
- •14. Перегрузка операторов сравнения и арифметических операторов. Основные правила реализации и применения.
- •15. Перегрузка операторов индексной выборки, префиксного и постфиксного инкремента/декремента. Перегрузка операторов преобразования типа.
- •16. Статические переменные-члены. Цель применения. Синтаксис. Особенности компоновки.
- •17. Статические функции-члены. Синтаксис, особенности применения. Фабричный метод. Статические функции-члены
- •Фабричный метод
- •19. Физическое и логическое постоянство объектов. Модификатор mutable.
- •20. Класс std::string из стандартной библиотеки. Основная функциональность, способы применения. Особенности внутренней структуры.
- •21. Композиция объектов. Иерархии целое-часть. Структура простейшей композиции по значению в памяти. Ответственность за уничтожение объектов при композиции.
- •22. Ссылочная композиция. Разрываемая композиция. Кратность композиции. Одиночная, множественная и недетерминированная кратность.
- •23. Применение контейнера std::vector для композиции с недетерминированной кратностью. Композиция объектов-значений и объектов-сущностей.
- •24. Композиция объектов с кратностью многие-ко-многим. Основные особенности объектных отношений, способы реализации.
- •25. Наследование классов. Необходимость в отношении наследования. Структура наследования в памяти. Повышающее преобразование типа.
- •26. Критерии оценки корректности применения наследования. Примеры корректного и некорректного применения наследования.
- •27. Конструкторы и деструкторы при наследовании. Моменты и порядок вызовов конструкторов. Передача аргументов конструкторам базового класса.
- •28. Спецификатор доступа protected. Защищенные конструкторы и методы.
- •29. Понижающее преобразование типа (downcast). Опасности. Поля идентификации типов.
- •30. Виртуальные функции. Полиморфизм. Цель. Синтаксис, примеры использования.
- •31. Реализация виртуальных функций. Указатель vptr и таблица vtable. Вызов виртуальной функции. Инициализация служебных данных для работы виртуальных функций в конструкторах.
- •32. Контроль переопределения виртуальных функций. Требования к сигнатурам. Ключевые слова override и final. Ковариантность возвращаемых типов.
- •33. Чисто виртуальные функции и абстрактные классы. Вызов чисто виртуальной функции в конструкторе до завершения инициализации объекта.
- •34. Понятие интерфейса. Применение интерфейсов.
- •35. Множественное наследование конкретных классов. Синтаксис, структура в памяти, особенности применения и реализации.
- •36. Преобразование типов при множественном наследовании в верхнем и нижнем направлениях. Коррекция указателя this.
- •37. Множественное наследование классов с повторяющимся базовым. Синтаксис, структура в памяти, особенности применения и реализации.
- •38. Виртуальные базовые классы. Синтаксис, структура в памяти, особенности применения и реализации. Понятие “самого производного” класса и его роль в организации работы виртуальных базовых классов.
- •39. Механизм rtti - назначение, особенности применения. Структура std::type_info, оператор typeid для выражений и типов.
- •40. Применение оператора dynamic_cast для указателей и ссылок. Основные цели использования. Отличия от операторов static_cast, reinterpret_cast и const_cast.
- •41. Альтернативные решения, заменяющие dynamic_cast. Виртуальные функции для понижающего преобразования. Типовое решение Visitor.
- •42. Обработка исключений. Цели, синтаксис выброса и обработчиков. Выбор обработчика по типу. Передача данных исключения по значению, указателю и ссылке. Исключения языка и стандартной библиотеки.
- •44. Шаблоны функций и классов. Синтаксис определения шаблонов. Инстанцирование шаблонов. Модель включения и явное инстанцирование.
- •Шаблоны классов
- •45. Аргументы шаблонов - типы, константы, шаблонные аргументы шаблонов. Дедукция фактических аргументов шаблонов.
- •46. Понятие обобщенной концепции. Статический полиморфизм по сравнению с динамическим полиморфизмом.
- •Статический полиморфизм
- •47. Итераторы stl - основные разновидности, итераторы контейнеров, итераторы, не связанные с контейнерами.
- •48. Классификация алгоритмов стандартной библиотеки. Примеры применения наиболее часто используемых алгоритмов.
- •49. Функциональные объекты stl. Простые функциональные объекты. Стандартные функциональные объекты. Связыватели std::bind.
- •50. Понятие лямбда-выражения. Синтаксис, особенности использования. Реализация лямбда-выражений компилятором. Список захвата лямбда-выражения.
- •51. Специализация шаблонов. Полная и частичная специализация. Статический выбор вариантов на основе специализации шаблонов.
- •52. Необычный рекуррентный шаблон. Структура, варианты применения.
45. Аргументы шаблонов - типы, константы, шаблонные аргументы шаблонов. Дедукция фактических аргументов шаблонов.
Аргументы шаблонов классов могут иметь типы по умолчанию, если какой-либо из типов используется чаще других:
template< typename T = int >
class Test
{
// ...
};
Ниже приведен полный пример полезного класса-шаблона для обобщенного АТД “стек” фиксированного размера. Отметим несколько основных правил написания шаблонов классов:
При определении шаблона класса может возникнуть путаница с использованием его имени внутри определения. Когда контекст требует использовать имя класса, например, чтобы задать конструктор, оно указывается как обычно:
// Конструктор
Stack ( int _size = 10 );
Когда же речь идет о классе как о типе, рекомендуется явно указывать его в обобщенном виде с указанием аргумента шаблона:
// Оператор копирующего присвоения
Stack< T > & operator = ( const Stack< T >& _s );
2. Как и для обычного класса, реализация методов шаблона класса может находиться как внутри объявления класса, так и за его пределами. Если размещать реализацию методов отдельно от определения класса, то нужно использовать следующий синтаксис:
template< typename T >
Stack< T >::Stack ( int _size )
: m_size( _size )
{
m_pData = new T[ m_size ];
m_pTop = m_pData;
}
3. Чаще всего тела методов шаблонов классов размещают непосредственно в заголовочном файле после объявления класса. Это работает корректно, даже если функции не объявляются как встраиваемые (inline). CPP-файла для шаблона-класса чаще всего не создают вообще. Именно так выглядит практически весь код стандартной библиотеки шаблонов. Такой стиль реализации, не свойственный обычным классам C++, обуславливается особенностями компоновки шаблонов. Пока примем это как утверждение без объяснения, а детально разъясним позже.
4. Пока не известен конкретный тип аргумента шаблона, ничего нельзя утверждать о размере этого объекта. Возникает вопрос способа передачи обобщенных значений в методы стека - по значению или по ссылке? Во избежание избыточных копирований для больших объектов обычно в обобщенном коде передают ссылки, надеясь что производительность передачи ссылки для маленьких объектов (например, char) не слишком уступит передаче по значению:
void push ( const T& _value );
Каждый уникальный тип аргумент породит новый метод в классе Test. Единственным ограничением является то, что шаблоном-членом не может быть виртуальная функция, поскольку компилятор обязан знать точное количество таких функций для формирования таблицы VTABLE и последующего переопределения в классах-наследниках.
Технические проблемы использования шаблонов
С разработкой шаблонов классов и функций связан ряд неприятных технических проблем. Написать компилирующийся и инстанцирующийся со всем желаемым набором типов обобщенный код является намного более сложной задачей, чем написание обычной необобщенной версии алгоритма или структуры данных.
Первое, с чем сталкиваются при работе с шаблонами - это сложные тексты ошибок компиляции, если что-либо записано не так. Нередко, вместо аккуратного простого описания о произошедшей ошибке, при компиляции шаблонного кода может быть выдана серия абсолютно непонятных ошибок сложной структуры, в которых не просто разобраться. В литературе такое поведение компиляторов называют “ошибками-романами”, сетуя на их количество и длину. В популярной книге о технике использования шаблонов “Шаблоны С++: справочник разработчика” приведен пример текста сгенерированной компилятором ошибки, вызванного совсем небольшой оплошностью в коде вызова шаблона функции, обрабатывающей связные списки строк. Длина сообщения действительно впечатляет, но это далеко на худший случай, который может встретиться на практике.
Второй проблемой являются имена генерируемых экземпляров функций и методов. На уровне реализации, имена включают все аргументы шаблонов, и могут становиться достаточно длинными. Длину идентификаторов также можно увидеть и оценить в приведенном выше примере. В более ранних версиях компиляторов и компоновщиков при интенсивном использовании шаблонов иногда случалась довольно редкая для обычного программного кода ошибка превышения допустимой длины идентификаторов.
Существует также ряд синтаксических нюансов, которые нужно соблюдать при написании кода с шаблонами. Например, некоторые более ранние компиляторы (например, Visual Studio до версии 2010) плохо реагируют на идущие подряд закрывающие угловые скобки вложенных шаблонов. Такой код может не скомпилироваться, поскольку компилятор может воспринять последовательность символов “>>” как оператор сдвига, а не как 2 идущие подряд закрывающие угловые скобки:
std::vector< std::vector< int >> vv;
Проблему решают вставкой дополнительного пробела:
std::vector< std::vector< int > > vv;
// ^ стоит поставить пробел
Еще одной надоедливой синтаксической проблемой является использование вложенных имен, зависящих от аргумента шаблона. Предположим, реализация предполагает, чтобы все аргументы T в приведенном ниже шаблоне имели вложенный синоним типа value_type:
template< typename T >
class Test
{
T::value_type x;
};
Существуют и более неприятные синтаксические странности. Например, в коде иногда может потребоваться объяснить некоторым компиляторам, что речь идет об аргументах шаблона, а не об операторе <, добавив странно выглядящее синтаксическое средство “. template”:
class Test
{
public:
template< typename T>
int f ( T _x );
};
template< typename T >
void f ( T _x )
{
Test t;
std::cout << t. template f< T >( _x );
// Оригинально: std::cout << t.f< T >( _x );
}
Еще одной проблемой практического применения шаблонов является разница в поддержке между компиляторами. Очень часто возникают ситуации, когда код прекрасно компилируется на одном компиляторе, но выдает ошибки на другом. Это связано с отступлением конкретными компиляторами от норм стандартов, а также с дефектами в реализации самих компиляторов. Чтобы получить действительно переносимый код, иногда потребуется вносить исправления после реализации и регулярно собирать программу на нескольких компиляторах.
Аргументы шаблонов, не являющиеся типами
Среди аргументов шаблонов могут быть не только типы. Допускается использование констант в качестве аргументов шаблонов. Например, размер стека можно было бы передавать не как аргумент конструктора, а задавать в списке аргументов шаблона, и тогда можно было бы обойтись без выделения динамической памяти, воспользовавшись обычным массивом:
template < typename T, int SIZE = 10 >
class Stack
{
private:
T data[ SIZE ];
int numElems;
public:
Stack();
void push( const T & );
void pop();
T & top() const;
bool empty() const;
bool full() const;
};
Существуют ограничения на допустимые типы констант - это должны быть целочисленные типы либо перечисления (в экзотических случаях допускаются также адреса глобальных объектов). Обычно такие константы используют для конфигурации режимов работы шаблона. Следует понимать, что любой код, использующий такие виды аргументов, видит конкретное значение константы при инстанцировании:
if ( SIZE > 10 )
do1();
else
do2();
Т.е, с точки зрения экземпляра шаблона, выражение SIZE > 10 является константным во время компиляции. Большинство компиляторов успешно оптимизирует код, содержащий константные выражения. Для приведенного выше примера в зависимости от значения SIZE будет выбрано первое либо второе действие, и никакой проверки условия во время выполнения происходить не будет. Результирующий фрагмент будет либо вызывать функцию do1(), либо функцию do2(), в зависимости от значения SIZE, с которым он будет инстанцирован.
Существует также особый вид аргументов шаблонов с непроизносимым названием - шаблонные параметры шаблонов (еще более непроизносимое на английском языке - template template parameters). Например, требуется реализовать АТД “очередь” на основе какого-либо контейнера. Чтобы позволить программисту выбирать между, например, двусвязным списком или деком (очередь с двумя концами), можно использовать шаблонный параметр шаблона:
template < typename T,
template < typename ELEM > class CONT = std::list >
class Queue
{
private:
CONT<T> elems;
public:
void push ( T const & _v )
{
elems.push_back( _v );
}
void pop ()
{
elems.pop_front();
}
T top () const
{
return elems.front();
}
bool empty () const
{
return elems.empty();
}
};
По умолчанию будет создаваться очередь на основе двусвязного списка элементов типа T:
Queue< T > q1; // Queue< T, std::list >
Чтобы изменить базовый контейнер, стоящий в основе реализации очереди, достаточно указать его явно:
Queue< T, std::deque > q1;
Такое переключение возможно, поскольку интерфейс std::list и std::deque преднамеренно хорошо согласован - основные открытые функции имеют одинаковые имена и подходящие типы аргументов. Разумеется, это не случайно, авторы стандартной библиотеке предусмотрели такое совпадение имен именно с целью облегчения написания обобщенного кода.