libnumerixpp  0.1.3
A Powerful C++ Library for High-Performance Numerical Computing
Создаем свою простую (C++) библиотеку с документацией, CMake и блекджеком
  • краткое описание

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

Если вы ловили себя на мысли: "А почему мне бы не создать свою полноценную библиотеку?", то я рекомендую прочитать вам мою статью.

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

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


Сразу говорю - я не профессионал в C++, и вы всегда можете поправить меня в комментариях. Благодарю!

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

Своими корнями он уходит в язык Си, который был разработан в 1969—1973 годах в компании Bell Labs программистом Деннисом Ритчи (Dennis Ritchie). В начале 1980-х годов датский программист Бьерн Страуструп (Bjarne Stroustrup), который в то время работал в компании Bell Labs, разработал С++ как расширение к языку Си. Фактически вначале C++ просто дополнял язык Си некоторыми возможностями объектно-ориентированного программирования. И поэтому сам Страуструп вначале называл его как "C with classes" ("Си с классами"). Но называть его так - значит упускать много конструкций из виду. Помимо ООП, есть пространства имен, шаблоны, огромная стандартная библиотека и так далее. В принципе, я думаю, не надо углубляться в историю языка. Но стоит знать, что с 1998 до 2011 года C++ развивался довольно медленно, но в 2011 вышел стандарт C++11, в котором было реализовано множество нового функционала.

С++ повсюду. Код, написанный на C++, можно найти в вашем телефоне, в вашей стиральной машине, в вашем автомобиле, в самолетах, в банках и вообще везде, где только можно представить.

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

А также C++ невероятно экономичен и быстрый. Посмотрите на довольный известный график снизу:

Ну а теперь к минусам. Один из главных минусов, который влияет на популярность C++ - плохая реклама. Многие люди настроены негативно по отношению к C++, из-за его сложности. Сколько раз вы слышали "с C легко выстрелить себе в ногу. В C++ это сложнее, но зато вы отстреливаете всю ногу сразу"?

Также довольно большим минусом является отсутствие экосистемы стандартизированных инструментов. Например, доставка и использование библиотек. В JavaScript для этого используется npm, в Python pip, в Haskell есть stack и cabal. То есть у этих языков есть экосистема, которая позволяет без сложностей использовать и публиковать пакеты, и в принципе работать с языком. А у C++ нет таких инструментов-стандартов. Есть конечно Conan, vcpkg, но они мало развиты, и разобщены.

Тем не менее, язык и экосистема растут, сообщество очень большое, а C++ неизбежно повсеместен. Так или иначе, его хотя бы частично можно найти почти в каждом написанном на сегодня программном обеспечении. Я не говорю, что C++ — это молоток, который должен превратить все вокруг вас в гвозди, но его все же стоит изучить и освоить.

Прошу не начинать холивары в комментариях насчет C++, эта статья туториал, а не обзор.

Особенности C++

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

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

Начнем с относительно легкой вещи - а именно пространства имен. Я не буду сильно углубляться в темы, просто расскажу минимальную информацию. Мы же все таки айтишники, а не сосиски в тесте.

Пространство имен позволяет сгруппировать функционал в отдельные контейнеры. Пространство имен представляет блок кода, который содержит набор компонентов (функций, классов и т.д.) и имеет некоторое имя, которое прикрепляется к каждому компоненту из этого пространства имен. Полное имя каждого компонента — это имя пространства имен, за которым следует оператор :: (оператор области видимости или scope operator) и имя компонента. Примером может служить оператор cout, который предназначен для вывода строки на консоль и который определен в пространстве имен std. Соответственно чтобы обратиться к этому оператору, применяется выражение std::cout.

#include <iostream>
int main() {
std::cout << "Hello, Habr!" << std::endl;
return 0;
}

Но также можно создавать и свои пространства:

#include <iostream>
namespace hello {
const std::string message{"hello work"};
void print(const std::string& text) {
std::cout << text << std::endl;
}
}
int main() {
hello::print(hello::message); // hello work
}

Директива using позволяет ссылаться на любой компонент пространства имен без использования его имени:

#include <iostream>
namespace console
{
const std::string message{"hello"};
void print(const std::string& text)
{
std::cout << text << std::endl;
}
}
using namespace console; // подключаем все компоненты пространства console
int main()
{
print(message); // указывать пространство имен не требуется
}

А также можно делать псевдонимы:

namespace fs = boost::filesystem;
// fs::path ...

ООП

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

У нас статья не об ООП, так что сильно углубляться не будем. Вот пример класса:

class User {
private:
int userid;
std::string password;
double cash;
public:
void top_up_account(double sum) {
cash += sum;
}
double withdraw_funds(double sum) {
if (cash < sum)
return 0;
cash -= sum;
return cash;
}
}

Перегрузка функций

C++ позволяет определять функции с одним и тем же именем, но разным набором параметров. Подобная возможность и называется function overloading. Компилятор уже сам выбирает нужный тип функции.

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

Простейший пример:

#include <iostream>
int max(int, int);
double max(double, double, double);
int main() {
int result1 = {max(1, 3)};
double result2 = {max(3.0000001, 3.000001)};
std::cout << result1 << std::endl;
std::cout << result2 << std::endl;
return 0;
}
int max(int a, int b) {
return (a >= b ? a : b);
}
double sum(double a, double b) {
return (a >= b ? a : b);
}

Функции могут отличаться и количеством аргументом, и их типом и так далее.

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

Шаблоны

Концепция шаблонов возникла из известного принципа программирования DRY (Dont't repeat yourself, не повторяйся). Шаблоны позволяют определить конструкции, которые используют определенные типы, но на момент написания кода точно не известно, что это будут за типы. То есть, шаблоны позволяют определить универсальные конструкции, которые не зависят от конкретных типов.

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

Количество аргументов

C++ имеет следующий вид функции с принятием аргументов:

int example_function() {} // функция не принимает аргументов
int example_function(void) {} // функция тоже не принимает аргументов. Это создано для обратной совместимости с C, т.к. в C код `int example_function() {}` означает что она принимает сколько угодно значений, поэтому для пустых функций принято было использовать void
int example_function(...) {} // сколько угодно аргументов

Создаем репозиторий

Каждая уважающая себя библиотека должна иметь три вещи:

  1. Репозиторий
  2. Документацию
  3. Относительно понятный код

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

Один из самых важных файлов - .gitignore. Этот файл содержит в себе список директорий и файлов, которые следует игнорировать. Вот мой .gitignore из моего репозитория:

# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Cmake build
build/

А также нам нужно будет создать файл .github/workflows/static.yml, но это будет позже, когда мы будем создавать документацию.

Сама структура проекта такова:

├── build.sh
├── CHANGELOG.md
├── CMakeLists.txt
├── docs
│ ├── doxygen-styles.css
│ ├── en
│ │ └── index.md
│ └── ru
│ ├── article.md
│ └── index.md
├── Doxyfile
├── examples
│ ├── example-1.cpp
│ └── example-2.cpp
├── include
│ └── libnumerixpp
│ ├── core
│ │ └── common.hpp
│ ├── libnumerixpp.hpp
│ ├── mathematics
│ │ ├── core.hpp
│ │ └── quadratic_equations.hpp
│ └── physics
│ ├── core.hpp
│ └── kinematics.hpp
├── LICENSE
├── README.md
└── src
├── core
│ └── common.cpp
├── libnumerixpp.cpp
├── mathematics
│ ├── core.cpp
│ └── quadratic_equations.cpp
└── physics
├── core.cpp
└── kinematics.cpp

Рассмотрим ее:

  • build.sh - простой баш-скрипт для сборки проекта
  • CMakeLists.txt - файл сборки CMake
  • docs - директория с документацией. doxygen-styles.css это кастомные css-стили для doxygen, а директории en и ru нужны для разных переводов.
  • Doxyfile - файл конфигурации Doxygen
  • examples - директория с примерами работы библиотеки
  • include - директория с заголовочными файлами библиотеки
  • src - директория с исходным кодом: core - базовый функционал, mathematics - математика, physics - физика.

Система сборки CMake

CMake — кроcсплатформенная утилита для автоматической сборки программы из исходного кода. При этом сама CMake непосредственно сборкой не занимается, а представляет из себя front-end. В качестве back-end'a могут выступать различные версии make и Ninja. Так же CMake позволяет создавать проекты для CodeBlocks, Eclipse, KDevelop3, MS VC++ и Xcode.

Для того что бы собрать проект средствами CMake, необходимо в корне дерева исходников разместить файл CMakeLists.txt, хранящий правила и цели сборки, и произвести несколько простых шагов.

Больше о CMake вы можете прочитать в этой статье.

Инструкция по сборке cmake хранится в файле CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(libnumerixpp VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set (EXECUTABLE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin)
file(GLOB_RECURSE SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB_RECURSE EXAMPLE_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/examples/*.cpp)
add_library(libnumerixpp SHARED ${SOURCE_FILES})
set_target_properties(libnumerixpp PROPERTIES OUTPUT_NAME "numerixpp")
target_include_directories(libnumerixpp
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
foreach(EXAMPLE_SOURCE_FILE ${EXAMPLE_SOURCE_FILES})
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SOURCE_FILE} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SOURCE_FILE})
target_link_libraries(${EXAMPLE_NAME} PRIVATE libnumerixpp)
endforeach()
install(TARGETS libnumerixpp
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include
)

Код выше - это и есть файл сборки CMake моего проекта. Рассмотрим его:

  1. cmake_minimum_required - минимальная версия CMake
  2. project(libnumerixpp VERSION 0.1.0) - название и версия проекта
  3. set(CMAKE_CXX_STANDARD 17) - устанавливаем стандарт C++17
  4. set(CMAKE_CXX_STANDARD_REQUIRED ON) - требуем чтобы компилятор поддерживал выбранный стандарт C++
  5. set (EXECUTABLE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) - устанавливаем каталог вывода для исполняемых файлов
  6. set(LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) - устанавливаем каталог вывода для библиотек
  7. file(GLOB_RECURSE SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) - находим все C++ файлы в директории src
  8. file(GLOB_RECURSE EXAMPLE_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/examples/*.cpp) - находит все C++ файлы в директории examples
  9. add_library(libnumerixpp SHARED ${SOURCE_FILES}) - создаем динамическую библиотеку с именем libnumerixpp
  10. set_target_properties(libnumerixpp PROPERTIES OUTPUT_NAME "numerixpp") - задаем имя выходного файла на numerixpp (CMake автоматически прибавит префикс lib к названию файла библиотеки)
  11. target_include_directories(...) - добавляем каталог include в качестве общедоступного каталога.
  12. foreach(EXAMPLE_SOURCE_FILE ${EXAMPLE_SOURCE_FILES}) ... endforeach() - перебираем все исходные файлы из директории examples и подключаем к ним библиотеку
  13. install(...) - установка библиотеки в систему
  14. install(DIRECTORY include/ - настраиваем установку заголовочных файлов библиотеки

Итак, для сборки надо использовать команду:

cd build
cmake ..
make
sudo make install

Но для облегчения этой задачи напишем простой bash-скрипт:

#!/bin/bash
# Define colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Project information
PROJECT_NAME="libnumerixpp"
BUILD_DIR="build"
# Functions
function print_header() {
echo -e "${YELLOW}#######################################################################${NC}"
echo -e "${YELLOW}### ${1}${NC}"
echo -e "${YELLOW}#######################################################################${NC}"
}
function print_step() {
currdate=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${currdate} ${BLUE}[ * ] ${1}${NC}"
}
function print_debug() {
currdate=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${currdate} ${PURPLE}[ * ] ${1}${NC}"
}
function print_success() {
currdate=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${currdate} ${GREEN}[ ✓ ] ${1}${NC}"
}
function print_error() {
currdate=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${currdate} ${RED}[ ✗ ] ${1}${NC}"
}
currdate=$(date +"%Y-%m-%d %H:%M:%S")
clear
echo -e "libnumerixpp build @ ${currdate}\n"
if [ "$1" == "clean" ]; then
# Clean up previous build
print_header "Cleaning up previous build"
if [ -d "$BUILD_DIR" ]; then
print_step "Removing $BUILD_DIR directory..."
rm -rf "$BUILD_DIR"
print_success "Removed $BUILD_DIR directory."
else
print_success "No previous build found."
fi
fi
# Create build directory
print_header "Creating build directory"
print_step "Creating $BUILD_DIR directory..."
mkdir -p "$BUILD_DIR"
print_success "Created $BUILD_DIR directory."
# Configure the project
print_header "Configuring the project"
print_step "Running CMake in $BUILD_DIR..."
cd "$BUILD_DIR"
cmake ..
if [ $? -eq 0 ]; then
print_success "CMake configuration completed successfully."
else
print_error "CMake configuration failed."
exit 1
fi
# Build the project
print_header "Building the project"
print_step "Building the project in $BUILD_DIR..."
make
if [ $? -eq 0 ]; then
print_success "Project build completed successfully."
else
print_error "Project build failed."
exit 1
fi
# Install the project
print_header "Installing the project"
print_step "Installing the project..."
sudo make install
if [ $? -eq 0 ]; then
print_success "Project installation completed successfully."
else
print_error "Project installation failed."
exit 1
fi
print_header "Build completed successfully"
echo -e "${CYAN}The ${PROJECT_NAME} library has been built and installed.${NC}"
echo "Build dir: build/"

Не волнуйтесь, весь код мы напишем позже.

Создание документации при помощи Doxygen

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

Устанавливается Doxygen просто:

sudo pacman -S doxygen # Arch
sudo apt install doxygen # Ubuntu/Debian

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

Рассматриваемая система Doxygen как раз и выполняет эту задачу: она позволяет генерировать на основе исходного кода, содержащего комментарии специального вида, красивую и удобную документацию, содержащую в себе ссылки, диаграммы классов, вызовов и т.п. в различных форматах: HTML, LaTeX, CHM, RTF, PostScript, PDF, man-страницы.

В большинстве случаев Doxygen используется для документации программного обеспечения, написанного на языке C++, однако на самом деле данная система поддерживает гораздо большое число других языков: C, Objective-C, C#, PHP, Java, Python, IDL, Fortran, VHDL, Tcl, и частично D.

Итак, сначала нам нужно будет перейти в рабочую директорию и создать Doxyfile - файл конфигурации:

doxygen -g

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

Вот основные значения:

PROJECT_NAME = "Project Name" # Имя проекта
PROJECT_NUMBER = 0.1.0 # Версия проекта
PROJECT_BRIEF = "Yet another project" # Краткое описание проекта
OUTPUT_DIRECTORY = docs # Куда складывать сгенерированную документацию
OUTPUT_LANGUAGE = English # Язык документации
GENERATE_LATEX = YES # Генерация LaTeX
INPUT = src include # Директории, где искать файлы
RECURSIVE = YES # Рекурсивный обход директорий
USE_MATHJAX = YES # Использование mathjax (для latex в html)
  • PROJECT_NAME - название проекта.
  • PROJECT_NUMBER - версия проекта. Я придерживаюсь схемы "major.minor.patch".
  • PROJECT_BRIEF - краткое описание проекта.
  • OUTPUT_DIRECTORY - директория, куда будет записываться созданная документация.
  • OUTPUT_LANGUAGE - язык документации (доступные значения: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, Ukrainian and Vietnamese).
  • GENERATE_LATEX - позволяет генерировать LaTeX.
  • INPUT - директории, откуда будет браться исходный код. Разделяются пробелами.
  • RECURSIVE - рекурсивный обход директорий.
  • USE_MATHJAX - для использования latex-формул в html.

Больше настроек вы можете посмотреть в этой статье.

Кастомизация

Дефолтный стиль, мягко говоря, некрасивый. Поэтому мы будем использовать кастомную css-тему:

HTML_STYLESHEET = ./docs/doxygen-styles.css # путь до css стилей

Данный файл стилей вы можете скачать отсюда.

Посмотреть, что получилось у меня, вы можете по ссылке. А мой Doxyfile здесь.

Форма написания комментариев

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

Для того, чтобы doxygen правильно создал документацию, стоит следовать стилистике написания комментариев. Рассмотрим пример:

std::vector<double> calculateRootsByDiscriminant(double discriminant, double a, double b);
  • +

    • детали, подробное описание
  • Todo:
    Warning
    - предупреждение
  • - ссылка на связанный класс или метод
  • Parameters
    -передаваемый параметр, имеет направление ([in], [out], [in,out])
  • Returns
    - возвращаемое значение
    Также мы используем latex-формулу: чтобы обозначить ее, надо в начале и в конце вставить \f$. Для создания latex-формул можно использовать онлайн редактор latex.

Также существуют следующие метки:

  • Authors
    - автор/ы
  • Version
    - версия
  • Date
    - дата
  • Bug: