Skip to content

Instantly share code, notes, and snippets.

@exeynod
Last active March 12, 2025 17:58
Show Gist options
  • Save exeynod/83f58e7a0618d6577c26d099905503a4 to your computer and use it in GitHub Desktop.
Save exeynod/83f58e7a0618d6577c26d099905503a4 to your computer and use it in GitHub Desktop.
Подготовка к экзамену АЯ 3 семестр

Подготовка к экзамену по алгоритмическим языкам программирования

Список вопросов:

  1. Двусвязный список. Основные операции: поиск элемента по значению, вставка элемента, удаление элемента. Их алгоритмическая сложность. Сортировка. Реализация функции удаления элемента из двусвязного списка. Основные методы std::list. Пример работы с std::list

  2. Класс std::map. Внутренняя реализация map, его основные методы. Сложность поиска по ключу, сортировки, удаления элемента, добавления элемента. Пример работы с std::map

  3. Класс std::set. Внутренняя реализация set, его основные методы. Сложность поиска, удаления элемента, добавления элемента. Пример работы с std::set

  4. Класс std::unordered_map. Внутренняя реализация unordered_map, его основные методы. Сложность поиска, сортировки, удаления элемента, добавления элемента. Пример работы с std::unordered_map

  5. Класс std::vector. Внутренняя реализация vector, его основные методы. Сложность поиска, сортировки, удаления элемента, добавления элемента. Амортизированная сложность добавления элемента в конец. Пример работы с std::vector. Особенность std::vector.

  6. Класс std::vector и std::array. Отличия. Внутренняя реализация vector, его основные методы. Сложность поиска, сортировки, удаления элемента, добавления элемента. Амортизированная сложность добавления элемента в конец. Пример работы с std::array.

  7. Парадигмы ООП. Полиморфизм(статический, динамический). Инкапсуляция. Наследование. Примеры динамического и статического полиморфизмов.

  8. Парадигмы ООП. Полиморфизм (статический, динамический). Инкапсуляция. Наследование. Ключевые слова virtual, override, final. Примеры.

  9. Разработка обобщенных типов: шаблоны С++. Инстанцирование. Спецификация шаблонов. Примеры.

  10. Итераторы: определение, назначение, преимущества. Итераторы прямого доступа, итераторы ввода, итераторы вывода, двунаправленные итераторы, прямой итератор. Примеры.

  11. Современный С++: auto, decltype, range base loop, nullptr, constexpr, enum class, if constexpr.

  12. Современный С++: static_assert, default, final, override. Преимущества using по сравнению с typedef. Конструкторы принимающие initializer_list. Примеры.

  13. Современный С++: std::optional, std::variant, std::any, std::string_view. Примеры использования.

  14. Лямбда-функции, функторы, указатели на функции, std::functional. Примеры использования std::functional. Примеры использования лямбда-функций.

  15. Rvalue ссылки. Семантика перемещения. std::move, std::forward. Make-функции. Примеры.

  16. Алгоритмы STL: count_if, find_if, find, count, transform, sort, any_of, all_of, remove_if, replace_if, merge, max, max_element, is_sorted, lower_bound, upper_bound, accumulate и др.

  17. Обработка ошибок с использованием механизма обработки исключений. RAII. Примеры классов, использующих RAII. Ключевое слово noexcept.

  18. RAII. «Умные» указатели. Класс std::shared_ptr. Устройство shared_ptr. Основные методы. Примеры использования.

  19. RAII. «Умные» указатели. std::unique_ptr. Основные методы. Примеры использования.

  20. RAII. «Умные» указатели. Класс std::weak_ptr. Основные методы. Связь с классом std::shared_ptr. Примеры использования.

  21. Шаблоны проектирования. Фабрика. Пример реализации фабрики на С++.

  22. Шаблоны проектирования: синглтон, PImp. Пример реализации PImpl.

  23. Сетевое взаимодействие. Berkley sockets. Основные функции для работы с сокетами

  24. Сетевое взаимодействие. Сокеты. Библиотека boost::asio. io_service, endpoint, ip::address. Функции асинхронного и синхронного чтения и записи.

  25. Управление потоками. Состояния гонок в интерфейсе структур данных. Класс std::future, функция std::async.

  26. Переключение контекста потоков. Класс std::thread. Ключевое слово thread_local. Примеры использования thread_local.

  27. Переключение контекста потоков. Класс std::thread. Ключевое слово thread_local. Примеры использования std::thread.

  28. [Синхронизация потоков](#Синхронизация потоков). Состояние гонок. Классы std::mutex, std::lock_guard, std::unique_lock. Функция std::lock. Примеры использования мьютексов.

  29. [Синхронизация потоков](#Синхронизация потоков). Состояние гонок. Классы std::recursive_mutex, boost::shared_mutex, std::unique_lock. Функция std::lock. Пример использования std::unique_lock.

  30. Синхронизация потоков. Состояние гонок. Потокобезопасные структуры данных с блокировками.

  31. Класс std::condition_variable. Потокобезопасные структуры данных с блокировками.

  32. Синхронизация потоков. Пул потоков. Класс std::condition_variable. Примеры работы с std::condition_variable.

  33. Управление потоками. Состояния гонок. Класс std::future, функция std::async. Пример работы с std::future и std::async.

  34. Атомарные операции. Классы std::atomic, std::atomic_flag. Примеры работы с std::atomic

  35. Перегрузка new и delete. Аллокаторы, системы управления памятью. TCMalloc

  36. Пространства имен (namespace). Анонимные namespace. Что такое, для чего необходимы.

  37. Синхронизация потоков. Состояние гонок. Шаблон producer-consumer. Примеры.

  38. Функции для работы с файловой системой. std::filesystem: filesystem::path, directory_iterator. Функции exists, is_regular_file, is_directory, is_symlink. Примеры работы с std::filesystem.

Двусвязный список

Каждый узел двусвязного линейного списка содержит два поля указателей — на следующий и на предыдущий узлы. Указатель на предыдущий узел корня списка содержит нулевое значение. Указатель на следующий узел последнего узла также содержит нулевое значение.

Основные операции

Поиск элемента по значению

Сложность O(n). Проходим по всему списку и сравниваем каждый элемент с данным значением.

Вставка элемента

Сложность O(n). Сначала находим нужный элемент, затем создаём либо новый узел. Затем изменяем все указатели.

Удалениие элемента

Сложность O(n). Аналогично вставке.

Сортировка

Для сортировки вставками алгоритмическая сложность будет равна O(n^2), т.к необходми пройти по всему списку и каждый раз вставлять элементы перед требуемым. Соответственно n^2 проходов.

Таблица сложностей

pushFront popFront pushBack popBack insert delete get size
O(1) O(1) O(1) O(1) O(1) O(1) O(1)

Реализация удаления

struct node {
    void *value;
    node *next;
    node *prev;
};

struct list {
    size_t size;
    node *head;
    node *tail;
};

void* deleteNth(list *_list, size_t index) {
    node *elm = _list->head;
    void *tmp = nullptr;
    size_t i = 0;
    while (elm && i < index) {
        elm = elm->next;
        i++;
    }
    if (elm == nullptr) {
        exit(5);
    }
    if (elm->prev) {
        elm->prev->next = elm->next;
    }
    if (elm->next) {
        elm->next->prev = elm->prev;
    }
    tmp = elm->value;
    if (!elm->prev) {
        _list->head = elm->next;
    }
    if (!elm->next) {
        _list->tail = elm->prev;
    }
    delete elm;
    _list->size--;
    return tmp;
}

std::list

Вместимость
Метод Функция
empty Проверяет отсутствие элементов в контейнере
size Возвращает количество элементов в контейнере
max_size Возвращает максимально допустимое количество элементов в контейнере
Модификаторы
Метод Функция
clear Очищает контейнер
insert Вставляет элементы
emplace (C++11) Конструирует элементы "на месте" и вставляет их начиная с заданной позиции pos
erase Удаляет элементы
push_back добавляет элемент в конец
emplace_back (C++11) Конструирует элементы "на месте" в конце контейнера
pop_back Удаляет последний элемент
push_front вставляет элементы в начало списка
emplace_front (C++11) конструирует элементы "на месте" в начало списка
pop_front удаляет первый элемент
resize Изменяет количество хранимых элементов
(public функция-член) swap
Обменивает содержимое (public функция-член)
Операции
Метод Функция
merge слияние двух отсортированных списков
splice перемещает элементы из другого list
remove / remove_if удаляет элементы, удовлетворяющие определенным критериям
reverse инвертирует порядок элементов
unique удаляются последовательно повторяющиеся элементы
sort сортирует элементы

Пример работы

std::list<int> numbers{ 1, 2, 4, 5 }; // Инициализация
if (numbers.empty())
    std::cout << "The list is empty" << std::endl;
else
    std::cout << "The list is not empty" << std::endl;
numbers.push_back(23);  // { 1, 2, 3, 4, 5, 23 }
numbers.push_front(15); // { 15, 1, 2, 3, 4, 5, 23 }

Класс std map

std::map — отсортированный ассоциативный контейнер, который содержит пары ключ-значение с неповторяющимися ключами. Порядок ключей задаётся функцией сравнения Compare. Операции поиска, удаления и вставки имеют логарифмическую сложность. Данный тип, как правило, реализуется как красно-чёрное дерево.

Красно-чёрное дерево — один из видов из самобалансирующихся двоичных деревьев поиска, гарантирующих логарифмический рост высоты дерева от числа узлов и позволяющее быстро выполнять основные операции дерева поиска: добавление, удаление и поиск узла. Сбалансированность достигается за счёт введения дополнительного атрибута узла дерева — «цвета». Этот атрибут может принимать одно из двух возможных значений — «чёрный» или «красный».

Основыне методы

Метод Функция
at (C++11) Предоставляет доступ к указанному элементу с проверкой индекса
operator[] Предоставляет доступ к указанному элементу
empty Проверяет отсутствие элементов в контейнере
size Возвращает количество элементов в контейнере
max_size Возвращает максимально допустимое количество элементов в контейнере
clear Очищает контейнер
insert Вставляет элементы
emplace (C++11) Конструирует элементы "на месте" и вставляет их начиная с заданной позиции pos
emplace_hint (C++11) Элементы конструкций на месте использования подсказки
erase Удаляет элементы
swap Обменивает содержимое
count Возвращает количество элементов, соответствующих определенному ключу
find находит элемент с конкретным ключом
equal_range возвращает набор элементов для конкретного ключа
lower_bound возвращает итератор на первый элемент не меньше, чем заданное значение
upper_bound возвращает итератор на первый элемент больше, чем определенное значение
key_comp возвращает функцию, сравнивающую ключи
value_comp возвращает функцию, сравнивающую значения

Сложности основных алгоритмов

  1. Поиск по ключу - O(log(n))
  2. Сортировка - O
  3. Вставка - O(log(n))
  4. Удаление - O(log(n))

Пример использования

Класс std set

Set — ассоциативный контейнер, который содержит упорядоченный набор уникальных объектов типа Key. Сортировка элементов осуществляется применением функции Compare к ключам множества. Операции поиска, удаления и вставки имеют логарифмическую сложность. Данный тип обычно реализуется как красно-черные деревья.

Основные методы

Метод Функция
empty Проверяет отсутствие элементов в контейнере
size Возвращает количество элементов в контейнере
max_size Возвращает максимально допустимое количество элементов в контейнере
clear Очищает контейнер
insert Вставляет элементы
emplace (C++11) Конструирует элементы "на месте" и вставляет их начиная с заданной позиции pos
emplace_hint (C++11) Элементы конструкций на месте использования подсказки
erase Удаляет элементы
swap Обменивает содержимое
count Возвращает количество элементов, соответствующих определенному ключу
find находит элемент с конкретным ключом
equal_range возвращает набор элементов для конкретного ключа
lower_bound возвращает итератор на первый элемент не меньше, чем заданное значение
upper_bound возвращает итератор на первый элемент больше, чем определенное значение
key_comp возвращает функцию, сравнивающую ключи
value_comp возвращает функцию, сравнивающую значения

Сложности основных алгоритмов

  1. Поиск - O(log(n))
  2. Вставка - O(log(n))
  3. Удаление - O(log(n))

Класс std unordered map

Unordered map является ассоциативным контейнером, который содержит пары ключ-значение с уникальными ключами. Поиск, вставка и удаление выполняются за константное время.

Основные методы

Метод Функция
empty Проверяет отсутствие элементов в контейнере
size Возвращает количество элементов в контейнере
max_size Возвращает максимально допустимое количество элементов в контейнере
clear Очищает контейнер
insert Вставляет элементы
emplace Конструирует элементы "на месте" и вставляет их начиная с заданной позиции pos
emplace_hint Элементы конструкций на месте использования подсказки
erase Удаляет элементы
swap Обменивает содержимое
at Предоставляет доступ к указанному элементу с проверкой индекса
operator[] Предоставляет доступ к указанному элементу
count Возвращает количество элементов, соответствующих определенному ключу
find находит элемент с конкретным ключом
equal_range возвращает набор элементов для конкретного ключа
begin(int) cbegin(int) возвращает итератор на начало указанного сегмента
end(int) cend(int) возвращает итератор на конец указанного сегмента
bucket_count Возвращает количество bucket'ов
max_bucket_count Возвращает максимальное количество bucket'ов
bucket_size Возвращает количество элементов в конкретном bucket'е
bucket Возвращает bucket для конкретного ключа
load_factor Возвращает среднее количество элементов на bucket
max_load_factor Управляет максимальным средним количеством элементов на bucket
rehash Резервирует количество bucket'ов, не меньшее запрошенного, соответственно перестраивая хэш-таблицу.
reserve Запасает место для, как минимум, указанного числа элементов. Это восстанавливает хэш-таблицу.

Класс std vector

Элементы хранятся непрерывно, а значит доступны не только через итераторы, но и через смещения, добавляемые к указателям на элементы (data() или же, для непустых массивов, — &vect[0]). Это означает, что указатель на элемент вектора может передаваться в любую функцию, ожидающую указатель на элемент массива. (начиная с C++03) Хранилище вектора обрабатывается автоматически, расширяясь и сужаясь по мере необходимости. Векторы обычно занимают больше места, чем статические массивы, поскольку некоторое количество памяти выделяется про запас на обработку будущего роста. Таким образом, память для вектора требуется выделять не при каждой вставке элемента, а только после исчерпания резервов. Общий объём выделенной памяти можно получить с помощью функции capacity(). Резервная память может быть возвращена системе через вызов shrink_to_fit().

Перераспределения обычно являются дорогостоящими операциями в плане производительности. Функция reserve() может использоваться для предварительного выделения памяти и устранения перераспределений, если заранее известно количество элементов.

Основные методы

Метод Функция
at Предоставляет доступ к указанному элементу с проверкой индекса
operator[] Предоставляет доступ к указанному элементу
front Предоставляет доступ к первому элементу
back предоставляет доступ к последнему элементу
data (C++11) Предоставляет прямой доступ к внутреннему содержимому
empty Проверяет отсутствие элементов в контейнере
size Возвращает количество элементов в контейнере
max_size Возвращает максимально допустимое количество элементов в контейнере
reserve Зарезервировать память.
capacity Возвращает количество элементов, которые могут одновременно храниться в выделенной области памяти
shrink_to_fit (C++11) Уменьшает использование памяти, высвобождая неиспользуемую
clear Очищает контейнер
insert Вставляет элементы
emplace (C++11) Конструирует элементы "на месте" и вставляет их начиная с заданной позиции pos
erase Удаляет элементы
push_back добавляет элемент в конец
emplace_back (C++11) Конструирует элементы "на месте" в конце контейнера
pop_back Удаляет последний элемент
resize Изменяет количество хранимых элементов
swap Обменивает содержимое

Сложности основных алгоритмов

  1. Произвольный доступ — постоянная O(1)
  2. Вставка и удаление элементов в конце — амортизированная постоянная O(1)
  3. Вставка и удаление элементов — линейная по расстоянию до конца вектора O(n)

Особенность std::vector

Способ, которым std::vector < bool > сделан компактным, определяется реализацией. Одной из потенциальных оптимизаций является сливание векторных элементов таким образом, что каждый элемент занимает один бит, а не байт, как обычный элемент типа bool.

std::vector< bool > ведет себя аналогично std::vector, но для того, чтобы быть компактным, он:

  • Не обязательно хранит свои данные в одном непрерывном куске памяти.
  • Предоставляет std::vector::reference как метод доступа к отдельным битам.
  • Не использует std::allocator_traits::construct чтобы построить битовые значения.

Vector и array

std::array - это контейнер, инкапсулирующий массив фиксированного размера. Основную часть данных хранит на стеке

Эта структура имеет ту же семантику, что и C-массивы. Размер и эффективность array<T,N> такие же, как у C-массива T[N]. array предоставляет некоторые возможности стандартных контейнеров, такие как знание собственного размера, поддержка присваивания, итераторы произвольного доступа и т.д.

Парадигмы ООП. Полиморфищм. Инкапсуляция. Наследование

Все языки OOP, включая С++, основаны на трёх основополагающих концепциях, называемых инкапсуляцией, полиморфизмом и наследованием. Рассмотрим эти концепции.

Инкапсуляция

Инкапсуляция - это объединение кода и данных таким образом, чтобы защищать данные от непреднамеренного использования и внешнего вмешательства. Существует 3 основных типа доступа: private(только внутреннее изменение), protected(доступ для потомков), public(открытый доступ).

Наследование

Наследование - процесс, посредством которого один обхект может приобретать свойства другого. Точнее объект можеть унаследовать характерные черты одного объекта и внести в них изменения, характерные только для него.

Полиморфизм

Полиморфизм - это свойство, которое позволяет одно и то же имя использовать для решения схожих, но технически разных задач. Целью полиморфизма, применительно к объектно-ориентированному программированию, является использование одного имени для задания общих для класса действий.

Применительно к C++ сущетсвует два типа поиморфизма: статический и динамический.

Статический полиморфизм

Статический полиморфизм реализуется с помощью шаблонов классов. Класс создаётся во время компиляции из шаблона (статическое связывание)

Пример:

template <typename T>
class Comparison {
public:
    T max(T a, T b) {
        return (a > b) ? a : b;
    }
    T min(T a, T b) {
        return (a < b) ? a : b;
    }
};

Из шаблона создаются два объекта. Один для работы с целыми числами, а другой для работы c вещественными.

Диинамический полиморфизм

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

Пример:

class Comparison {
public:
    int max(int a, int b);
    double max(double a, double b);
};

Парадигмы ООП. Ключевые слова

Virtual

Ключевое слово virtual используется для объявления виртуальной функции. Функция-член класса может быть объявлена виртуальной, если: класс, содержащий виртуальную функцию, базовый в иерархии порождения, а также реализация функции зависит от класса и будет различной в каждом порожденном классе.

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

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

Override

Ключевое слово override следует после объявления метода и означает, что данный метод является виртуальным и переобределяет метод базового класса. Если такого метода в базовом классе нет, компилятор сообщает об этом.

Пример для virtual и override:

class figure
{
protected:
  double x, y;
public:
  figure(double a = 0, double b = 0) { x = a; y = b; }
  virtual double area() {return(0);}
};
class rectangle : public figure
{
public:
  rectangle(double a = 0, double b = 0) : figure(a, b) {};
  double area() override {return(x*y);}
};
class circle : public figure
{
public:
  circle(double a = 0) : figure(a, 0) {};
  double area() override {return(3.14*x*x);}
};

Final

Ключевое слово позволяет запретить переопределение виртуального метода в дочернем классе. Также final может запретить использование класса как базового в дальнейшем

Пример:

struct Base
{
    virtual void foo();
};

struct A : Base
{
    void foo() final; // Base::foo is overridden and A::foo is the final override
    void bar() final; // Error: non-virtual function cannot be overridden or be final
};

struct B final : A // struct B is final
{
    void foo() override; // Error: foo cannot be overridden as it's final in A
};

struct C : B // Error: B is final
{
};

Шаблоны C++

Механизм шаблонов в языке С++ позволяет решать проблему унификации алгоритма для различных типов: нет необходимости писать различные функции для целочисленных, действительных или пользовательских типов – достаточно составить обобщенный алгоритм, не зависящий от типа данных, основывающийся только на общих свойствах. Например, алгоритм сортировки может работать как с целыми числами, так и с объектами типа «автомобиль».

Пример шаблонной функции:

template<class T>
T _min(T a, T b){
    if( a < b){
        return a;
    }
    return b;
}

Инстанциирование

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

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

Пример:

namespace N
{
  template<class T>
  class Y // определение шаблона
  {
    void mf() { }
  };
}
// template class Y<int>; 	  // ошибка: шаблон класса Y не видим в глобальном пространстве имён
using N::Y;
// template class Y<int>; 	  // ошибка: явное инстанцирование вне пространства имён шаблона
template class N::Y<char*>;       // OK: явное инстанцирование
template void N::Y<double>::mf(); // OK: явное инстанцирование

Когда код ссылается на шаблон в контексте, который требует полностью определённого типа, или когда полнота типа влияет на код, и этoт конкретный тип не был явно инстанцирован, то происходит неявное инстанцирование. Например, когда создаётся объект этoго типа, но не указатель на этoт тип.

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

Пример:

template<class T>
struct Z // определение шаблона
{
    void f() {}
    void g(); // нет определения
};
template struct Z<double>; // явное инстанцирование Z<double>
Z<int> a; 		   // неявное инстанцирование Z<int>
Z<char>* p; 		   // здесь ничего не инстанцируется
p->f(); 		   // здесь происходит неявное инстанцирование Z<char> и Z<char>::f().

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

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

template<>
std::string _min(std::string a, std::string b){
    if(a.size() < b.size()){
        return a;
    }
    return b;
}

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

Итераторы

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

  • Оператор * возвращает элемент, на который в данный момент указывает итератор.

  • Оператор ++ перемещает итератор к следующему элементу контейнера. Большинство итераторов также предоставляют оператор −− для перехода к предыдущему элементу.

  • Операторы == и != используются для определения того, указывают ли два итератора на один и тот же элемент или нет. Для сравнения значений, на которые указывают два итератора, нужно сначала разыменовать эти итераторы, а затем использовать оператор == или !=.

  • Оператор = присваивает итератору новую позицию (обычно начало или конец элементов контейнера). Чтобы присвоить значение элемента, на который указывает итератор, другому объекту, нужно сначала разыменовать итератор, а затем использовать оператор =.

Каждый контейнерный класс имеет 4 основных метода для работы с оператором =:

  • begin() возвращает итератор, представляющий начало элементов контейнера.

  • end() возвращает итератор, представляющий элемент, который находится после последнего элемента в контейнере.

  • cbegin() возвращает константный (только для чтения) итератор, представляющий начало элементов контейнера.

  • cend() возвращает константный (только для чтения) итератор, представляющий элемент, который находится после последнего элемента в контейнере.

Преимущества

В процедурных языках программирования широко используется индексация, основанная на счётчике цикла, для перебора всех элементов последовательности (например, массива). Хотя индексация может использоваться совместно с некоторыми объектно-ориентированными контейнерами, использование итераторов даёт свои преимущества:

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

Итераторы могут предоставлять дополнительные возможности при навигации по элементам. Например, проверку отсутствия пропусков элементов или защиту от повторного перебора одного и того же элемента. Некоторые контейнеры могут предоставлять возможность модифицировать свои объекты без влияния на сам итератор. Например, после того, как итератор уже «прошёл» первый элемент, можно вставить дополнительные элементы в начало контейнера без каких-либо нежелательных последствий. При использовании индексации это проблематично из-за смены номеров индексов.

Типы итераторов

Итератор ввода

Input iterator предназначен только для однократного чтения (ввода) последовательности значений.

Value value = *it++; // прочитать следующее значение, it - итератор

Итератор можно передвигать на одну позицию вперед (инкремент) и разыменовывать (операции * и ->), получая доступ к текущему значению. Итераторы можно сравнивать между собой на равенство и неравенство.

Итератор вывода

Output iterator предназначен только для однократной записи (вывода) последовательности. В остальном аналогичен итератору ввода.

*it++ = value;
Однонаправленный итератор

Forward iterator является расширением концепции “итератор ввода”, т.е. предоставляет возможности итератора ввода (и, возможно, но не гарантированно, итератора вывода). Кроме того, однонаправленный итератор допускает многократное чтение и запись линейной последовательности, по которой можно двигаться только в одну сторону (как по односвязному списку — “вперёд” с помощью операции ++).

Двунаправленный итератор

bidirectional iterator является расширением концепции “однонаправленный итератор”. Двунаправленный итератор допускает движение в двух направлениях: вперед (с помощью ++) и назад (с помощью операции --).

Итератор произвольного доступа

random access iterator является расширением концепции “двунаправленный итератор” и наиболее похож по своему поведению на обычный указатель на элемент массива (который является частным случаем итератора произвольного доступа).

Итератор произвольного доступа допускает адресацию по индексу (оператор []), сдвиг в обе стороны на некоторое количество позиций (добавление и вычитание целого числа), вычисление расстояния с помощью вычитания и сравнение на “меньше” и “больше” (согласованное с расстоянием, которое имеет знак).

Современный C++

auto

До С++11, ключевое слово auto использовалось как спецификатор хранения переменной (как, например, register, static, extern). В С++11 auto позволяет не указывать тип переменной явно, говоря компилятору, чтобы он сам определил фактический тип переменной, на основе типа инициализируемого значения. Это может использоваться при объявлении переменных в различных областях видимости, как, например, пространство имен, блоки, инициализация в цикле и т.п. Например, объявление итераторов становится гораздо короче и проще, что позволяет сократить код

decltype

Decltype позволяет статически определить тип по типу другой переменной.

int x = 5;
double y = 5.1;

decltype(x) foo;    // int
decltype(y) bar;    // double
decltype(x+y) baz;  // double

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

Range based loop

Range-Based for — это цикл по контейнеру.

for (int& x : foo)
    x *= 2;

for (const int& x : foo)
    std::cout << x << std::endl;

nullptr

Раньше, для обнуления указателей использовался макрос NULL, являющийся нулем — целым типом, что, естественно, вызывало проблемы (например, при перегрузке функций). Ключевое слово nullptr имеет свой собственный тип std::nullptr_t, что избавляет нас от бывших проблем. Существуют неявные преобразования nullptr к нулевому указателю любого типа и к bool (как false), но преобразования к целочисленных типам нет.

void foo(int* p) {}

void bar(std::shared_ptr<int> p) {}

int* p1 = NULL;
int* p2 = nullptr;   

if(p1 == p2)
{}

foo(nullptr);
bar(nullptr);

bool f = nullptr;
int i = nullptr; // ошибка: для преобразования в int надо использовать reinterpret_cast

Constexpr

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

constexpr-функция

Ключевое слово constexpr, добавленное в C++11, перед функцией означает, что если значения параметров возможно посчитать на этапе компиляции, то возвращаемое значение также должно посчитаться на этапе компиляции. Если значение хотя бы одного параметра будет неизвестно на этапе компиляции, то функция будет запущена в runtime (а не будет выведена ошибка компиляции).

constexpr-переменная

Ключевое слово в данном случае означает создание константы. Причем expression должно быть известно на этапе компиляции.

int sum (int a, int b)
{
	return a + b;
}

constexpr int new_sum (int a, int b)
{
	return a + b;
}

void func()
{
	constexpr int a1 = new_sum (5, 12); // ОК: constexpr-переменная
	constexpr int a2 = sum (5, 12); // ошибка: функция sum не является constexp-выражением
	int a3 = new_sum (5, 12); // ОК: функция будет вызвана на этапе компиляции
	int a4 = sum (5, 12); // ОК
}

enum-class

Перечисление представляет собой особый тип, значение которой ограничивается одним из нескольких явно именованных констант ("счетчики"). Значения констант - это значения целого типа, известного также как базовый тип перечисления.

enum color {
    red,
    yellow,
    green = 20,
    blue
};  

Современный C++(2)

static_assert

В C++11 добавили ещё один тип assert-а — static_assert. В отличие от assert, который выполняется во время выполнения, static_assert выполняется во время компиляции, вызывая ошибку компилятора, если условие не является истинным. Если условие ложное, то выводится диагностическое сообщение.

static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) == 4, "int must be 4 bytes");

Поскольку static_assert обрабатывается компилятором, то условная часть static_assert также должна обрабатываться во время компиляции. Поскольку static_assert не обрабатывается во время выполнения, то стейтменты static_assert могут быть размещены в любом месте кода (даже в глобальном пространстве). В C++11 диагностическое сообщение должно быть обязательно предоставлено в качестве второго параметра. В C++17 предоставление диагностического сообщения является необязательным.

default

Суть его заключается в том, что пользователь может указать компилятору реализовать ту или иную функцию-член класса по-умолчанию.

class Foo
{
public:
    Foo(int x) {/* ... */}
}

Необходимо реализовать конструкор по умолчанию. Вместо определения конструктора без параметров, в C++11 появилась возможность просто указать компилятору сгенерировать его по-умолчанию. Достигается это, как я сказал выше, при помощи спецификатора default.

class Foo
{
public:
    Foo() = default;
    Foo(int x) {/* ... */}
};

Final и override описано выше

using vs typedef

С появлением шаблонов в C++, добавление синонимов стало достаточно трудным с использованием typedef, соответсвенно для решения этой проблемы, а также упрощения кода в стандарт был добавлена using.

Конструкторы принимающие initializer_list

Конструкторы, походящие на конструкторы контейнеры, принимающие значения в угловых скобках. Реализованны данные конструкторы посредством класса std::initializer_list, что позволяет упростить множественное присваивание.

Современный C++(3)

optional

Класс, который хранит определнное значение и сообщает о том, инициализированно оно или нет. Классическое использование - в функции, которая может и не вернуть значение.

variant

Шаблон класса, который в качестве шаблонных параметров принимает типы, которые он может содержать. Далее используя функцию std::visit можно узнать хранимый типа

variant<string, int, bool> mySetting = string("Hello!"); // или
mySetting = 42; // или
mySetting = false;

any

Тут хранится что угодно, что потом можно достать

std::any a = 1;
    std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';

a = 1;
int* i = std::any_cast<int>(&a);
std::cout << *i << "\n";    

string_view

Позволяет лишить права владения определенной строкой, оставив право просмотра

void get_vendor_from_id(std::string_view id) { // не аллоцирует память, работает с `const char*`, `char*`, `const std::string&` и т.д.
    std::cout <<
        id.substr(0, id.find_last_of(':')); // не аллоцирует память для подстрок
}

Лямбда-функции

И так всем известно =)

Функторы

Функтор == функциональный объект. Для определения функтора достаточно описать класс, в котором переопределен оператор (). Особо широкое применение функторы приобрели в алгоритмах STL, рассмотренных ранее, когда они передаются в вызов в качестве параметра, вместо функции, определяющей действие или предикат алгоритма.

functional

?

Rvalue ссылки. Move-семантика

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

Функция move в действительности выполняет весьма скромную работу. Её задача состоит в том, чтобы принять либо lvalue, либо rvalue параметр, и вернуть его как rvalue без вызова конструктора копирования. Теперь всё зависит от клиентского кода, где должны быть перегружены ключевые функции (например, конструктор копирования и оператор присваивания), определяющие будет ли параметр lvalue или rvalue. Если параметр lvalue, то необходимо выполнить копирование. Если rvalue, то можно безопасно выполнить перемещение.

std::forward

std::forward - вспомогательная функция, которая позволяет выполнить идеальную передачу аргументов, принимаемых в качестве rvalue-ссылок.

Потребность в этой функции вытекает из того факта, что все названные значения (такие как параметры функции) всегда оцениваются, как lvalues (даже тех, которые объявлены как rvalue-ссылки), и это создает трудности в сохранении семантики перемещения внутри функций, которые переадресуют аргументы для другим функциям

Таким образом, std::forward помогает в промежуточной передачи ссылок с сохранением их типа.

struct smth : base {
  template <typename ...Args> smth(Args &&...args) : base(std::forward<Args>(args)...) {}
};

std::unique_ptr<int> p;
smth s1(p);            // OK, будет вызван base(std::unique_ptr<int>&);
smth s2(st::move(p));  // OK, будет вызван base(std::unique_ptr<int>&&);

Алгоритмы STL

cpp refference и вперед к покорению STL =)

Обработка ошибок

try {
  throw 1;
//  throw 'a';
}
catch (long b) {
  cout << "пойман тип long:  " << b << endl;
}
catch (char b) {
  cout << "пойман тип char:  " << b << endl;
}

RAII

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

Классы использующие RAII

template <class T>
struct ScopedPtr {
  T* ptr_;

  ScopedPtr(T* ptr) {
    ptr_ = ptr;
  }

  ~ScopedPtr() {
    delete ptr_;
  }
};


ScopedPtr<Unit> guard(new Knight);

Стандартными классами являются, например "умные" указатели, а также std::lock_guard, std::unique_lock, std::shared_lock

noexcept

Ключевое слово сообщает компилятору о том, что исключений в данной функции выброшено не будет.

class my_class {
    int i_;

public:
    explicit my_class (int i) noexcept
        : i_(i)
    {}

    int get() const noexcept {
        return i_;
    }
};

inline int operator+(const my_class& v1, const my_class& v2) noexcept {
    return v1.get() + v2.get();
}

int main() {
    // std::terminate при исключении
    my_class var0(10);

    // std::terminate при исключении
    my_class var1(100);

    // std::terminate при исключении
    int res = (var1 + var0 > 0 ? 0 : 1);

    // Вызов деструкторов var1 и var0
    return res;
}

Выше приведен пример использование данного ключевого слова. Стоит отметить, что noexcept позволяет уменьшить размер бинарного файла, генерируемого компилятором.

"Умные" указатели

Благодаря наличию исключений, язык C++ позволяет разделить основную логику приложения и обработку ошибок, не мешая их в одну кучу. Что есть очень хорошо. Однако теперь по коду нельзя с уверенностью сказать, где может быть прервано его исполнение. Отсюда возникает опасность утечки ресурсов. Проблема эта решается при помощи деструкторов и идиомы RAII. Впрочем, придерживаться этой идиомы становится проблематично при использовании указателей. Особенно при использовании их не как членов класса, а просто как переменных в методах.

Данную проблему решает стандартная библиотека memory, содержащая так называемые "умные" указатели.

shared_ptr

Преимущества:

  • совместное управление объектом
  • копируемый и перемещаемый
  • можно работать в условиях многопоточности

Указатель std::shared_ptr используется для управления ресурсами путем совместноrо владения, т.е. объект, на который указывает shared_ptr, уничтожится только после того, как не останется ни одного shared_ptr, ссылающегося на него.

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

std::shared_ptr ptr(new Image("~/photo.png"));

// Copy:
std::shared_ptr another_ptr = ptr;
assert(ptr != nullptr);
assert(another_ptr != nullptr);

// Move:
std::shared_ptr yet_another_ptr = std::move(ptr);
assert(ptr == nullptr);
assert(yet_another_ptr != nullptr);
assert(another_ptr != nullptr);

Weak_ptr

Интересные грабли при использовании shared_ptr заключается в том, что с его помощью можно создать циклические ссылки. Например, есть два объекта. Первый ссылается при помощи shared_ptr на второй, а второй — на первый. Даже если ни на один из объектов нет других ссылок, счетчики ссылок никогда не обнулятся, и объекты никогда не будут уничтожены.

Эта проблема обходится при помощи weak_ptr, так называемого слабого указателя. Класс weak_ptr похож на shared_ptr, но не участвует в подсчете ссылок. Также у weak_ptr есть метод lock(), возвращающий временный shared_ptr на объект.

Пример использования:

class SomeClass {
public:
    void sayHello() {
        std::cout << "Hello!" << std::endl;
    }

    ~SomeClass() {
        std::cout << "~SomeClass" << std::endl;
    }
};

int main() {
    std::weak_ptr<SomeClass> wptr;

    {
        auto ptr = std::make_shared<SomeClass>();
        wptr = ptr;

        if(auto tptr = wptr.lock()) {
            tptr->sayHello();
        } else {
            std::cout << "lock() failed" << std::endl;
        }
    }

    if(auto tptr = wptr.lock()) {
        tptr->sayHello();
    } else {
        std::cout << "lock() failed" << std::endl;
    }
}

Unique_ptr

Шаблонный класс unique_ptr представляет собой уникальный указатель на объект. Указатель нельзя копировать, но можно передавать владение им с помощью std::move. При уничтожении указателя автоматически вызывается деструктор объекта, на который он указывает.

auto unq = std::make_unique<SomeClass>(/* ctor args */);
unq->sayHello();
auto mov = std::move(unq);
SomeClass* ptr = mov.get();
ptr->sayHello();

Шаблоны проектирования. Фабрика

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

Достоинства:

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

Недостатки:

  • необходимость создавать наследника Creator для каждого нового типа продукта (ConcreteProduct).

Пример:

#
class abstractFooCreator
{
public:
	virtual fooCreator() {}
	virtual Foo * create() const = 0;
};

template <class C>
class fooCreator : public abstractFooCreator
{
public:
	virtual Foo * create() const { 
    return new C(); }
};

сlass FooFactory
{
protected:
	typedef std::map<std::string, abstractFooCreator*> FactoryMap;
	FactoryMap factory;

public:
	FooFactory();
	~virtual FooFactory();

	template <class C>
	void add(const std::string & id)
	{
		typename FactoryMap::iterator it = factory.find(id);
		if (it == factory.end())
			factory[id] = new fooCreator<C>();
	}

	Foo * create(const std::string & id)
	{
		typename FactoryMap::iterator it = factory.find(id);
		if (it != factory.end())
			return it->second->create();
		return 0;
	}
};

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

FooFactory factory;
factory.add<MyFoo>("MyFoo");
factory.add<MyFoo2>("MyFoo2");
factory.add<ImprovedFoo>("ImprovedFoo");
Foo * p = factory.create("MyFoo2");

Шаблоны проектирования. Синглтон. Pimpl.

Singleton

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

Singleton решает несколько проблем:

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

Все реализации одиночки сводятся к тому, чтобы скрыть конструктор по умолчанию и создать публичный статический метод, который и будет контролировать жизненный цикл объекта-одиночки. Если у вас есть доступ к классу одиночки, значит, будет доступ и к этому статическому методу. Из какой точки кода вы бы его ни вызвали, он всегда будет отдавать один и тот же объект.

Достоинства:

  • Гарантирует наличие единственного экземпляра класса.
  • Предоставляет к нему глобальную точку доступа.
  • Реализует отложенную инициализацию объекта-одиночки.

Недостатки:

  • Нарушает принцип единственной ответственности класса.
  • Маскирует плохой дизайн.
  • Проблемы мультипоточности.
  • Требует постоянного создания Mock-объектов при юнит-тестировании.
class Singleton {
  static Singleton* instance;
  Singleton() {}
  Singleton(const Singleton&) = delete;
  Singleton& operator=(Singleton&) = delete;
public:
  static Singleton * getInstance() {
    if(!instance)
      instance = new Singleton();
    return instance;
  }
};

Singleton* Singleton::instance = nullptr;

Pimpl

Управление потоками

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

std::future и std::async

std::async позволяет выполнить функцию асинхронно и вернуть результат как std::future, стоит отметить, что функция может быть выполнена и синхронно.

Шаблонный класс std::future обеспечивает механизм доступа к результатам асинхронных операций:

  • Асинхронные операции (созданные с помощью std::async, std::packaged_task, или std::promise) могут вернуть объект типа std::future создателю этой операции.
  • Создатель асинхронной операции может использовать различные методы запроса, ожидания или получения значения из std::future. Этим методы могут заблокировать выполнение до получения результата асинхронной операции.
  • Когда асинхронная операция готова к отправке результата её создателю, она может сделать это, изменив shared state (например, std::promise::set_value), которое связано с std::future создателя.

Пример:

   // future from an async()
   std::future<int> f2 = std::async(std::launch::async, [](){ return 8; });

   // future from a promise
   std::promise<int> p;
   std::future<int> f3 = p.get_future();
   std::thread( [](std::promise<int>& p){ p.set_value(9); },
                std::ref(p) ).detach();
 dwd
   std::cout << "Waiting...";
   f1.wait();
   f2.wait();
   f3.wait();
   std::cout << "Done!\nResults are: "
             << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';

Переключение контекста потоков

Чтобы операционная система поддерживала многозадачность, каждый выполняемый поток должен обладать своим контекстом исполнения. Этот контекст используется для хранения данных о текущем состоянии потока: значения регистров процессора, указателя на стек данных, указатель на текущую выполняемую команду.

Переключение контекста:

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

std::thread

Создание объекта типа std::thread запускает новый поток.

#include <iostream>
#include <thread>
 
void hello() {
  std::cout << "Hello, World!";
}
 
int main() {
  std::thread th(hello);
  th.join();
}

До вызова деструктора объекта типа std::thread необходимо вызвать или метод join(), или метод detach(). Вызов метода join приведет к ожиданию завершения потока. Это значит, что до тех пор пока поток не завершит своё выполнение, основной поток не будет выполнять код находящийся после вызова метода join(). Этот метод необходимо использовать, если основному потоку необходим и важен результат выполнения дочернего потока. Например, когда необходимо дождаться загрузки данных для дальнейшей обработки этих данных. Вызов функции detach оставляет поток работать в фоновом режиме. Это значит, что код находящийся после вызова метода detach() может выполняться пока выполняется запущенный поток. Этот метод необходимо использовать, если основному потоку не важен результат выполнения дочернего потока. Например, отправка пользовательской статистики. Класс std::thread является перемещающимся типом со всеми вытекающими последствиями.

thread_local

?

Синхронизация потоков

Для избежания состояния гонки следует синхронизировать потоки. Простейшим способом синхронизации потоков является взаимоисключающая блокировка. Класс mutex является примитивом синхронизации, который может использоваться для защиты разделяемых данных от одновременного доступа нескольких потоков. Mutex предлагает эксклюзивую, нерекурсивную семантику владения:

  • Вызывающий поток владеет мьютексом со времени успешного вызова lock или try_lock, и до момента вызова unlock.
  • Пока поток владеет мьютексом, все остальные потоки при попытке завладения им блокируются на вызове lock или получают значение false при вызове try_lock.
  • Вызывающий поток не должен владеть мьютексом до вызова lock или try_lock.

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

lock_guard

std::lock_guard класс. является оболочкой для mutex и позволяет реализовать идиому RAII в его отношении. Когда создается объект lock_guard, он завладевает мьютексом и исвобождает его либо при вызове декструктора, либо при вызове unlock()

void safe_increment()
{
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
 
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
 
    // g_i_mutex is automatically released when lock
    // goes out of scope
}

unique_lock

Является аналогом lock_guard, но при этом является перемещаемым и обладает набором вспомогательных метдов. Например try_lock

Пример:

struct Box {
    explicit Box(int num) : num_things{num} {}
 
    int num_things;
    std::mutex m;
};
 
void transfer(Box &from, Box &to, int num)
{
    // don't actually take the locks yet
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
 
    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);
 
    from.num_things -= num;
    to.num_things += num;
 
    // 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
}
 
int main()
{
    Box acc1(100);
    Box acc2(50);
 
    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
 
    t1.join();
    t2.join();
}

recursive_mutex

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

template <typename T>
class container 
{
     std::mutex _lock;
     std::vector<T> _elements;
public:
     void add(T element) 
     {
          _lock.lock();
          _elements.push_back(element);
          _lock.unlock();
     } 
     void addrange(int num, ...)
     {
          va_list arguments;
          va_start(arguments, num);
          for (int i = 0; i < num; i++)
          {
               _lock.lock();
               add(va_arg(arguments, T));
               _lock.unlock();
          }
          va_end(arguments); 
     }
     void dump()
     {
          _lock.lock();
          for(auto e: _elements)
          std::cout << e << std::endl;
          _lock.unlock();
     }
};
 
void threadFunction(container<int> &c)
{
     c.addrange(3, rand(), rand(), rand());
}
 
int main()
{
     srand((unsigned int)time(0));
     container<int> cntr;
     std::thread t1(threadFunction, std::ref(cntr));
     std::thread t2(threadFunction, std::ref(cntr));
     std::thread t3(threadFunction, std::ref(cntr));
     t1.join();
     t2.join();
     t3.join();
     cntr.dump();
     return 0;
}

shared_mutex

shared_mutex – это мьютекс позволяющий одновременно многим потокам читать одни и те же данные, если в этот момент нет потоков изменяющих эти данные.

condition_variable

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

Основные методы:

  • notify_one уведомляет один ожидающий поток
  • notify_all - уведомляет все ожидающие потоки
  • wait - блокирует текущий поток до тех пор, пока переменная не будет пробужена
  • wait_for - блокирует текущий поток до тех пор пока переменная условия проснулась или после указанного периода тайм-аута

Их работу удобно рассмотреть на примере шаблона паралелльного программирования Producer-Consumer. Producer, или “поставщик”, — это некоторый поток, который генерирует “задания” и складывает их в очередь. Consumer, или “потребитель”, — это поток, который обрабатывает “задачи” из очереди.

Пример:

std::mutex m;
std::queue<std::string> queue;
std::condition_variable cv;
void Producer() {
  std::lock_guard<std::mutex> lk(m);
  queue.push(ReadMessageFromNetwork());
  cv.notify_one();
}
void Consumer() {
  std::unique_lock<std::mutex> lk(m);

  while(queue.empty()) {
    cv.wait(lk);
  }
  // Используем очередь.
  lk.unlock();
  // Продолжаем выполнения потока.
}

Пулл потоков

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

  • метод возвращает future на результат выполнения функции
  • принимает функцию func, которую необходимо выполнить и принимает аргументы args, которые необходимо передать в функцию func во время выполнения Аргументы повторяют аргументы функции std::async (за исключением первого параметра std::launch policy).

По факту, std::async может не создавать новый поток для исполнения задачи, а использовать поток из встроенного пула, но это зависит от конкретной реализации компилятора.

По сути, это применение шаблона producer-consumer. В качестве consumer’ов выступают потоки, которые выполняют поставленные в очередь задачи. В качестве producer’ов – пользовательский код.

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

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

Вывод:

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

Атомарные операции

Операция называется атомарной, если она выполняется как единое целое, либо не выполняется вовсе. Т.е. она не может быть частично выполнена или частично не выполнена. Если один поток выполняет атомарную операцию, то другие потоки не могут “вмешаться” в выполнение этой операции (например, получить её промежуточное значение).

std::atomic

Каждая специализация шаблона std::atomic определяет атомарный тип. Только объекты атомарных С++ типов могут безопасно использоваться в нескольких потоках одновременно. Когда один поток сохраняет данные в объекте атомарного типа, а другой хочет их прочитать, поведение программы определено стандартом.

Доступ к атомарным переменным может использоваться для синхронизации потоков и для упорядочивания доступа к неатомарным переменным

  • load() - получить текущее значение
  • store() - присвоить новое значение
  • is_lock_free() - возвращает true, если операции на данном типе неблокирующие
  • operator++ - инкремент
  • exchange() - установить новое значение и вернуть предыдущее
  • compare_exchange_strong() - аналог CAS
  • compare_exchange_weak() - аналог CAS

CAS (Compare and Swap) Операция атомарно сравнивает значение одного объекта с другим и при равенстве измениет значение объекта.

std::atomic<int>integer(0);
std::atomic<int> otherInteger(0);
integer++;//Атомарно
otherInteger += integer++;//Не атомарно!

athomic_flag

std::atomic_flag. Причина, по которой этот тип стоит особняком, – проста, это тип является простейшим атомарным объектом и представляет собой булев флаг. Он содержит свой, уникальный набор операций и, самое главное, он единственный гарантированно является свободным от блокировок! Т.е. по стандарту все операции над объектом типа std::atomic_flag являются “чисто” атомарными, без каких либо условностей. Исходя из вышесказанного можно предположить, что остальные атомарные типы, для которых не существует свободной от блокировок версии на той или иной архитектуре, будут реализованы посредством atomic_flag.

atomic_flag содержит всего две операции: test_and_set и clear, чего, собственно говоря, вполне достаточно для флага, ведь он может быть либо поднятым, либо опущенным. Но отсутствует операция проверки значения флага, без модификации оного, что сильно ограничивает сферы его использования. Так же есть набор свободных функций, которые могут оперировать флагом:

  • std::atomic_flag_test_and_set
  • atomic_flag_test_and_set_explicit
  • atomic_flag_clear

Еще одним важным свойством atomic_flag, которое необходимо упомянуть, является его неопределенность при создании. Т.е. стандарт не оговаривает в каком состоянии находится флаг, ежели он не инициализирован. Поэтому, для получения предсказуемого результата есть смысл всегда инициализировать флаг; для этих целей существует специальный макрос ATOMIC_FLAG_INIT. Для инициализации флага, просто присвойте этот макрос вашему флагу, и, тогда, флаг инициализируется и гарантированно становится сброшенным:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

Перегрузка new и delete

Имеется возможность перегрузить new и delete. Это можно сделать в том случае, если необходимо использовать какой-то особый способ выделения памяти. Например, может понадобиться процедура выделения памяти, автоматически использующая дисковый файл в качестве виртуальной памяти в том случае, когда куча оказывается исчерпанной. Какой бы ни была причина, осуществить такую перегрузку очень просто.

void *operator new(size_t размер)
{
// выполнение выделения
return указатель_на_память;
}
void operator delete(void *p)
{
// освобождение памяти, на которую указывает р
}

Параметр размер будет содержать число в байтах, которое необходимо выделить для размещения объекта. Это значение будет сформировано автоматически. Перегруженная функция new должна возвращать указатель на выделенную память. За исключением этих ограничений перегруженная функция new может выполнять все, что необходимо.

Функция delete получает указатель на область памяти, которую необходимо освободить. Она обязана освободить эту память.

Аллокаторы

Аллокатор (англ. Allocator) или распределитель памяти в языке программирования C++ — специализированный класс, реализующий и инкапсулирующий малозначимые (с прикладной точки зрения) детали распределения и освобождения ресурсов компьютерной памяти. Все классы стандартной библиотеки шаблонов STL управляют памятью с помощью встроенных аллокаторов. Явное задание аллокатора не является обязательным требованием классов-контейнеров библиотеки, однако их можно передавать в конструкторы в качестве параметров шаблона[1]. Причиной внедрения в библиотеку STL механизма аллокаторов стала необходимость абстрагироваться при проектировании шаблонов от ограничений модели памяти вычислительной техники[2].

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

TCMalloc

tcmalloc — очень быстрая реализация malloc (быстрее чем malloc в glibc 2.3). С помощью данной библиотеки можно анализировать выделение памяти в программе, а также производить поиск утечек памяти

Поиск утечек памяти с помощью tcmalloc очень прост — надо слинковать программу с этой библиотекой, и запустить ее вот так: # HEAPCHECK=normal ./your-program

или вот так (без линковки): # LD_PRELOAD=/usr/lib/libtcmalloc.so.0.0.0 HEAPCHECK=normal ./your-program

и после выполнения программы, она выдаст отчет о найденных утечках памяти, например вот так: # LD_PRELOAD=/usr/lib/libtcmalloc.so.0.0.0 HEAPCHECK=normal ./test-hashes 1000000

Пространства имен namespace

Пространство имен — это декларативная область, в рамках которой определяются различные идентификаторы (имена типов, функций, переменных, и т. д.). Пространства имен используются для организации кода в виде логических групп и с целью избежания конфликтов имен, которые могут возникнуть, особенно в таких случаях, когда база кода включает несколько библиотек. Все идентификаторы в пределах пространства имен доступны друг другу без уточнения. Идентификаторы за пределами пространства имен могут обращаться к членам с помощью полного имени для каждого идентификатора, std::vectorstd::string vec;например или с помощью объявления using для одного идентификатора (using std::string) или директивы using для ALL идентификаторы в пространстве имен (using namespace std;). Код в файлах заголовков всегда должен содержать полное имя в пространстве имен.

Анонимные или безымянные пространства имен

Возможно создать явное пространство имен, но не присвоить ему имя.

namespace
{
    int MyFunc(){}
}

Это называется безымянным или анонимным пространством имен, и его можно использовать, если нужно сделать объявления переменных невидимыми для кода в других файлах (т. е. обеспечить их внутреннюю компоновку) без создания именованного пространства имен. Весь код, находящийся в том же файле, может видеть идентификаторы в безымянном пространстве имен, но эти идентификаторы, а также само пространство имен, будет невидимым за пределами этого файла или, точнее, вне блока перевода.

Функции для работы с файловой системой

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

  • path (C++17) представляет собой путь
  • directory_iterator (C++17) итератор содержимого каталога

Функции:

  • exists - проверяет, ссылается ли путь на существующий объект файловой системы
  • is_regular_file - проверяет, ссылается ли аргумент на обычный файл
  • is_directory - проверяет, ссылается ли данный путь на каталог
  • is_symlink - проверяет, ссылается ли аргумент на символическую ссылку
@exeynod
Copy link
Author

exeynod commented Jan 6, 2020

std::vector ведет себя аналогично std::vector

Что ты имел в виду?

Ну и где ты это нашел, как мне понять?? ахах

@govyadkin
Copy link

image
Там почему-то bool не отображается

@exeynod
Copy link
Author

exeynod commented Jan 7, 2020

image
Там почему-то bool не отображается

Спасибо. Поправлю

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment