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

Коллекции универсальных библиотек C++

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

iu5edu.ru/wiki/cpp2

План

  1. Длительность хранения
  2. Идиома RAII
  3. Умные указатели
  4. Лямда-выражения
  5. Основанное на диапазоне выражение for
  6. Потрясающие проекты
  7. GoogleTest
  8. Boost
  9. SFML
  10. GTK

Не тот герой те герои, которого мы заслуживаем, но тот герой те герои, в котором которых мы нуждаемся!

Основано на материале от proglib.io, metanit.com и хендбук Яндекса.

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

Длительность хранения

Длительность хранения (англ. storage duration) – это свойство объекта, которое описывает, когда тот попадает в память и когда её освобождает.

Четыре вида: автоматическая, статическая, потока и динамическая.

1. Автоматическая длительность хранения

  • Когда управление входит в область видимости объекта (также известную как scope), он размещается в автоматической памяти, зачастую реализованной в виде стека;
  • Когда управление покидает эту область, вызывается деструктор и память освобождается.
#include <iostream>

class Awesome {
  int id;
  static inline int count{0};
  Awesome() { Awesome::count++; id = Awesome::count; }
  ~Awesome() {
    std::cout << "EXTERMINATE! #" << id << std::endl;
    Awesome::count--;
  }
  void Print() { std::cout << "My id is #" << id << std::endl; }
};

int main() {
  Awesome aw1;
  { Awesome aw2; }
}
// EXTERMINATE! #2
// EXTERMINATE! #1

2. Статическая длительность хранения

Статическая связана с использованием спецификаторов static и extern. Объекты со статической длительности хранения создаются при запуске программы и удаляются при её завершении.

...
static Awesome aw1;

int main() {}
// EXTERMINATE! #1

3. Потоковая длительность хранения

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

При thread_local каждый поток также получит отдельную копию этих глобальных переменных:

...

thread_local static Awesome aw1;

int main() { aw1.Print(); }
// My id is #1
// EXTERMINATE! #1

4. Динамическая длительность хранения

Динамическая неразрывно связана с использованием ключевых слов new и delete.

...

int main() {
  Awesome *aw1 = nullptr;
  {
    Awesome *aw2 = new Awesome();
    aw1 = aw2;
  }
  aw1->Print();
  delete aw1;
}
// My id is #1
// EXTERMINATE! #1

Может использовать автоматическую длительность хранения?

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

center

Что может пойти не так?

...

Awesome *aw1 = new Awesome();
if (func()) {
  func2();
  return;
}
delete aw1;

...

P.S. Всё! 😈

Утечки памяти поджидают:

  • Если func() выбросит исключение, то управление не дойдёт до delete и память не освободится.
  • Если func() вернёт true, то после выполнения func2() управление покинет функцию, но память не освободится, т.к. автор кода забыл добавить delete внутрь условия.
  • Если бы автор забыл delete, память тоже не освободилась бы.

center

Идиома RAII

Идиома RAII (resource aquitization is initialization) переводится как "захват ресурса должен быть инициализацией объекта".

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

На этой идее построены стандартные контейнеры и так называемые "умные указатели".

Название идиомы, как замечает сам Бьярне Страуструп (автор языка программирования C++), выбрано неудачно. Лучше отражают её смысл альтернативные названия:

  • CADR (Constructor Acquires, Destructor Releases) — в конструкторе захватываем ресурс, в деструкторе — освобождаем;
  • SBRM (Scope-Bound Resource Management) — управление ресурсами с привязкой к области видимости.

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

Умные указатели (англ. smart pointer) — это объект, работать с которым можно как с обычным указателем, но при этом, в отличии от последнего, он предоставляет некоторый дополнительный функционал (например, автоматическое освобождение закрепленной за указателем области памяти).
Три вида: unique_ptr, shared_ptr и weak_ptr.

P.S. Ссылка на карту со знаком

Умный указатель std::unique_ptr

#include <memory>  // все умные указатели объявлены здесь

...

int main() {
  // Умный указатель
  std::unique_ptr<Awesome> smart = std::unique_ptr<Awesome>(new Awesome());

  // Он притворяется обычным указателем — 
  // у него перегружены соответствующие операторы:
  std::cout << smart->count << "\n";  // 17
   // Вызывать delete не надо, выделенная память освободится 
   // при выходе из блока
}
// 1
// EXTERMINATE! #1

Умный указатель "владеет" ресурсом: память будет освобождена в его деструкторе. Слово unique в его названии подчёркивает, что это единственный владелец ресурса.

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

Важно понять, что внутри умные указатели всё равно используют new/delete, они лишь позволяют программисту не делать этого и, как следствие, защищают его от ошибок.

Умный указатель std::shared_ptr

std::shared_ptr - умный указатель с подсчётом ссылок на объект. Такой указатель можно копировать. При копировании увеличивается счётчик созданных копий. В деструкторе этот счётчик уменьшается

Объект удаляется последним владельцем, когда счётчик дойдёт до нуля. Этот счётчик хранится в отдельной ячейке динамической памяти.

На неё ссылаются все объекты shared_ptr, которые разделяют владение одним и тем же объектом в динамической памяти. Текущее значение счётчика можно узнать с помощью функции use_count.

#include <iostream>
#include <memory>

int main() {
  std::shared_ptr<int> ptr1 = std::make_shared<int>(17);
  std::cout << *ptr1 << "\n";             // 17
  std::cout << ptr1.use_count() << "\n";  // 1

  auto ptr2 = ptr1;            // копирование разрешено!
  std::cout << *ptr1 << "\n";  // 17
  std::cout << *ptr2 << "\n";  // 17 — это всё тот же объект
  std::cout << ptr1.use_count() << "\n";  // 2
  std::cout << ptr2.use_count() << "\n";  // 2

  std::shared_ptr<int> ptr3;
  std::cout << ptr3.use_count() << "\n";  // 0

  ptr3 = ptr1;  // присваивание тоже разрешено!
  std::cout << *ptr3 << "\n";             // 17
  std::cout << ptr1.use_count() << "\n";  // 3
  std::cout << ptr2.use_count() << "\n";  // 3
  std::cout << ptr3.use_count() << "\n";  // 3
}
...

int main() {
  std::shared_ptr<Awesome> ptr1 = std::shared_ptr<Awesome>(new Awesome());
  std::cout << ptr1.use_count() << "\n"; // 1
  {
    auto ptr2 = ptr1;
    std::cout << ptr2.use_count() << "\n"; // 2
  }
  std::cout << ptr1.use_count() << "\n"; // 1
}

Существуют ситуации, когда объект A должен ссылаться на B, а B - на A. Это называется циклической ссылкой (cyclic reference/circular dependency). В таком случае оба объекта никогда не будут выгружены из памяти.

Чтобы разорвать цикличность, необходимо использовать std::weak_ptr.

Лямбда-выражения

Лямбда-выражения представляют более краткий компактный синтаксис для определения объектов-функций. Формальный синтаксис лямбда-выражения: [] (параметры) { действия }.

#include <iostream>

int main() {
  std::function<void(void)> f = []() { std::cout << "Hello" << std::endl; };
  f();
}

Тоже самое, только используя auto:

auto f = []() { std::cout << "Hello" << std::endl; };

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

class __Lambda1234 {
 public:
  auto operator()() const { std::cout << "Hello" << std::endl; }
};

Лямбда-выражение формы []() {...} может быть дополнительно сокращено до [] {...}:

[]{ std::cout << "Hello" << std::endl; }

Вызов лямбда-выражения

Безымянный вызов:

[]() { std::cout << "Hello" << std::endl; }();
// или так
[] { std::cout << "Hello" << std::endl; }();

Именнованные лямбда-выражения:

// переменная hello представляет лямбда-выражение
auto hello{[]() { std::cout << "Hello" << std::endl; }};

// через переменную вызываем лямбда-выражение
hello();  // Hello

Параметры лямбда-выражений

auto print{
  [](const std::string& text) { 
    std::cout << text << std::endl; 
}};

// вызываем лямбда-выражение
print("Hello World!");
print("Good bye, World...");

Возвращение значений из лямбда-выражений

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

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

// вызываем лямбда-выражение
std::cout << sum(10, 23) << std::endl;  // 33

// присваиваем его результат переменной
int result{sum(1, 4)};
std::cout << result << std::endl;  // 5

Можем явным образом указать возвращаемый тип:

auto sum{[](int a, int b) -> double { return a + b; }};

Лямбда-выражения как параметры функций

#include <iostream>

void do_operation(int a, int b, int (*op)(int, int)) {
  std::cout << op(a, b) << std::endl;
}

int main() {
  auto sum{[](int a, int b) { return a + b; }};
  auto subtract{[](int a, int b) { return a - b; }};

  do_operation(10, 4, sum);       // 14
  do_operation(10, 4, subtract);  // 6
}

Универсальные лямбда-выражения

Универсальное лямбда-выражение (англ. generic lambda) — это лямбда-выражение, в котором как минимум для одного параметра в качестве типа указано слово auto или выражения auto& или const auto&:

auto add = [](auto a, auto b) { return a + b; };
auto print = [](const auto& value) {std::cout << value << std::endl; };

std::cout << add(2, 3) << std::endl;  // 5 - складываем числа int
std::cout << add(2.2, 3.4) << std::endl;  // 5.6 - складываем числа double

print("Hello");
print(4);

Захват внешних значений в лямбда-выражениях

Получение данных по значению

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

#include <iostream>

int main() {
  int n{10};
  auto add = [=](int x) { std::cout << x + n << std::endl; };
  add(4);  // 14
}

Для подобного лямбда-выражения компилятор будет генерировать класс наподобие:

class __Lambda1c8 {
  int n;
 public:
  __Lambda1c8(const int& arg1) : n(arg1) {}
  auto operator()(int x) const { std::cout << x + n << std::endl; }
};
  • Значение внешней переменной передается через параметр, который представляет константную ссылку, и сохраняется в приватную переменную.
  • Значение приватной переменной мы изменить не можем.

Получение данных по ссылке

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

#include <iostream>

int main() {
  int n{10};
  auto increment = [&]() {
    n++;  // увеличиваем значение внешней переменной n
    std::cout << n << std::endl; // 11
  };
  increment();
  std::cout << n << std::endl; // 11
}

Для подобного лямбда-выражения компилятор будет генерировать класс наподобие:

class __Lambda1c8 {
  int& n;
 public:
  __Lambda1c8(int& arg1) : n(arg1) {}
  auto operator()() const {
    n++;
    std::cout << "n inside lambda: " << n << std::endl;
  }
};

mutable-параметры

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

#include <iostream>

int main() {
  int n{10};
  auto increment = [=]() mutable {
    n++;  // увеличиваем значение внешней переменной n
    std::cout << n << std::endl;  // 11
  };

  increment();
  std::cout << n << std::endl;  // 10
}

Получение определенных переменных

#include <iostream>

int main() {
  int n{10};
  auto b = [n]() { std::cout << "n: " << n << std::endl; };
  b();  // n = 10

  auto a = [&n]() { n++; };
  a();
  std::cout << "n: " << n << std::endl;  // n = 11
}
[&k, l, &m, n] // k и m - по ссылке, l и n - по значению
[=, &m, &n]    // все по значению, а m и n - по ссылке
[&, m, n]      // все по по ссылке, а m и n - по значению

Основанное на диапазоне выражение for

Цикл for имеет другой синтаксис, который используется исключительно с диапазонами:

for ( declaration : range ) statement;
// range-based for loop
#include <iostream>
#include <string>
using namespace std;

int main() {
  string str{"Hello!"};
  for (char c : str) { // for (auto c : str)
    cout << "[" << c << "]";
  }
  cout << '\n';
}
// [H][e][l][l][o][!]

А теперь про потрясающее!

Потрясающие проекты: https://github.com/sindresorhus/awesome

Потрясающие проекты C++: https://github.com/fffaraz/awesome-cpp

GoogleTest

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

А какие виды тесты бывают?

Для интересующихся подробнее почитать тут.

Что делает тест хорошим?

Тесты на GoogleTest полностью удовлетворяют следующим критериям:

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

Основные понятия GoogleTest

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

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

Набор тестов содержит один или несколько тестов. Вы должны сгруппировать свои тесты в наборы тестов, которые отражают структуру тестируемого кода.

Тестовая программа может содержать несколько наборов тестов.

Утверждения GoogleTest

Утверждения GoogleTest - это макросы, которые напоминают вызовы функций:

ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";

for (int i = 0; i < x.size(); ++i) {
  EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
  • ASSERT_* версии генерируют фатальные сбои, когда их проверка не проходит, и прерывают исполнение текущей функции.
  • EXPECT_* версии генерируют нефатальные сбои, которые не прерывают текущую функцию.

Простые тесты GoogleTest

Используйте TEST() макрос для определения и присвоения имени тестовой функции. Это обычные функции C ++, которые не возвращают значение:

TEST(TestSuiteName, TestName) {
  ... test body ...
}

TEST() аргументы переходят от общего к конкретному. Первый аргумент - это имя набора тестов и второй аргумент - это название теста внутри теста набор. Оба имени должны быть допустимыми идентификаторами C++, и они не должны содержать никаких подчеркивания (_).

Набор тестов GoogleTest

Дана сигнатура функции:

int Factorial(int n);  // Returns the factorial of n

Набор тестов для этой функции может выглядеть следующим образом:

// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
  EXPECT_EQ(Factorial(0), 1);
}

// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
  EXPECT_EQ(Factorial(1), 1);
  EXPECT_EQ(Factorial(2), 2);
  EXPECT_EQ(Factorial(3), 6);
  EXPECT_EQ(Factorial(8), 40320);
}

Пример проекта, использующего GoogleTest

Можете найти в материалах лекции. На что обратить внимание:

  • Декомпозиция проекта и использование cmake для обмена заголовочными файлами. Основа взята из репозитория.
  • Использование CMake для загрузки GoogleTest из репозитория. Полезная информация: документация и статья.

Boost

Boost — собрание библиотек классов, использующих функциональность языка C++ и предоставляющих удобный кроссплатформенный высокоуровневый интерфейс для лаконичного кодирования различных повседневных подзадач программирования (работа с данными, алгоритмами, файлами, потоками, регулярными выражениями, линейная алгебра, генерация псевдослучайных чисел, обработка изображений, модульное тестирование и т. п.). Версия 1.82 содержит 170 отдельных библиотек!!!

center

О Boost даже книги пишут...

center

А есть ли варианты не использовать Boost?

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

Охват библиотек Boost

  • Алгоритмы
  • Обход ошибок в компиляторах, не соответствующих стандарту
  • Многопоточное программирование
  • Контейнеры
  • Юнит-тестирование
  • Структуры данных
  • Функциональные объекты
  • Обобщённое программирование
  • Графы
  • Работа с геометрическими данными
  • Ввод-вывод
  • Межъязыковая поддержка
  • Итераторы
  • Математические и числовые алгоритмы
  • Работа с памятью
  • Синтаксический и лексический разбор
  • Метапрограммирование на основе препроцессора
  • "Умные указатели"
  • Обработка строк и текста
  • Метапрограммирование на основе шаблонов

Пример проекта, использующего Boost

Можете найти в материалах лекции. На что обратить внимание:

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

SFML

SFML (англ. Simple and Fast Multimedia Library — простая и быстрая мультимедийная библиотека) — свободная кроссплатформенная мультимедийная библиотека. Написана на C++, но доступна также для C, C#, .Net, D, Java, Python, Ruby, Go и Rust.

SFML содержит ряд модулей для простого программирования игр и мультимедиа приложений.

Модули SFML

  • System — управление временем и потоками, он является обязательным, так как все модули зависят от него.
  • Window — управление окнами и взаимодействием с пользователем.
  • Graphics — делает простым отображение графических примитивов и изображений.
  • Audio — предоставляет интерфейс для управления звуком.
  • Network — для сетевых приложений.

Привет, SFML!

#include <SFML/Graphics.hpp>

int main() {
  sf::RenderWindow window(sf::VideoMode(1024, 768), "Hello, World!",
                          sf::Style::Close);  // Создать окно
  // Ограничить частоту кадров в секунду до 60
  window.setFramerateLimit(60);
  // Основной цикл
  while (window.isOpen()) {
    sf::Event event;  // События
    // Обработка событий (нажатие кнопок, закрытие окна и т.д.)
    while (window.pollEvent(event)) {
      // Закрыть окно если нажата кнопка "Закрыть"
      if (event.type == sf::Event::Closed) window.close();
    }
    window.clear(sf::Color::Black);  // Очистить окно и залить его черным цветом
    window.display();  // Отобразить
  }
}

Пример проекта, использующего SFML

Можете найти в репозитории CMake SFML Project Template. На что обратить внимание:

  • Использование CMake для загрузки Boost из репозитория. Полезная информация: документация.
  • Возможность использования MinGW на Windows.
  • Предлагаемая структура проекта.

GTK

GTK (ранее GTK+; сокращение от GIMP ToolKit) — кроссплатформенная библиотека элементов интерфейса (фреймворк), имеет простой в использовании API, наряду с Qt является одной из двух наиболее популярных на сегодняшний день библиотек UI на Linux.

Причины выбрать GTK

GTKсвободное ПО, распространяемое на условиях GNU LGPL, позволяющей создавать как свободное, так и проприетарное программное обеспечение с использованием библиотеки. GTK является официальной библиотекой для создания графического интерфейса проекта GNU.

Пример проекта на GTK

Описание возможной структуры представлено здесь. Обратите внимание, что:

  • Через CMake FetchContent_Declare GTK не установить. Требуется устанавливать зависимости в операционной системе.
  • Пример CMake файла для GTK 4 находится тут.
  • Отдельно существует вики по разработке приложений под GNOME.

Мы чуть-чуть капнули...

center

Вопросы?

center

If not, just clap your hands!