В мире программирования создание собственных библиотек — это не просто возможность пополнения своего портфолио или способ структурировать код, а настоящий акт творческого самовыражения (и иногда велосипедостроения). Каждый разработчик иногда использовал в нескольких своих проектах однообразный код, который приходилось каждый раз перемещать. Да и хотя бы как упаковать свои идеи и знания в удобный и доступный формат, которым можно будет поделиться с сообществом.
Если вы ловили себя на мысли: «А почему мне бы не создать свою полноценную библиотеку?», то я рекомендую прочитать вам мою статью.
Эту статью вы можете использовать как шпаргалку для создания проектов, и не только библиотек.
Некоторые из вас могут подумать что мы изобретаем велосипед. А я в ответ скажу — сможете ли вы прямо сейчас, без подсказок, только по памяти, нарисовать велосипед без ошибок?
Сразу говорю - я не профессионал в 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(...) {} // сколько угодно аргументов
Создаем репозиторий
Каждая уважающая себя библиотека должна иметь три вещи:
Репозиторий
Документацию
Относительно понятный код
Не буду расписывать создание репозитория, и почему нужно использовать 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 моего проекта. Рассмотрим его:
cmake_minimum_required
— минимальная версия CMakeproject(libnumerixpp VERSION 0.1.0)
— название и версия проектаset(CMAKE_CXX_STANDARD 17)
— устанавливаем стандарт C++17set(CMAKE_CXX_STANDARD_REQUIRED ON)
— требуем чтобы компилятор поддерживал выбранный стандарт C++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)
— находим все C++ файлы в директории srcfile(GLOB_RECURSE EXAMPLE_SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/examples/*.cpp)
— находит все C++ файлы в директории examplesadd_library(libnumerixpp SHARED ${SOURCE_FILES})
— создаем динамическую библиотеку с именем libnumerixppset_target_properties(libnumerixpp PROPERTIES OUTPUT_NAME "numerixpp")
— задаем имя выходного файла на numerixpp (CMake автоматически прибавит префикс lib к названию файла библиотеки)target_include_directories(...)
— добавляем каталог include в качестве общедоступного каталога.foreach(EXAMPLE_SOURCE_FILE ${EXAMPLE_SOURCE_FILES}) ... endforeach()
— перебираем все исходные файлы из директории examples и подключаем к ним библиотекуinstall(...)
— установка библиотеки в систему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 правильно создал документацию, стоит следовать стилистике написания комментариев. Рассмотрим пример:
/**
* @brief Calculates the roots by discriminant.
*
* @details Calculate the roots by discriminant \f$\frac{-b +-
* \sqrt{D}}{2a}\f$. D > 0 = 2 roots, D == 0 = 1 root, D < 0 = 0
* roots.
*
* @todo Example Todo
* @warning This is a test
* @see Ref
*
* @param[in] discriminant The discriminant
* @param[in] a a
* @param[in] b b
*
* @return The roots by discriminant.
*/
std::vector<double> calculateRootsByDiscriminant(double discriminant, double a, double b);
@brief — краткое описание
@details — детали, подробное описание
@todo — что-то нужно доделать. Doxygen генерирует отдельную страницу со списком всех @todo
@warning — предупреждение
@ref — ссылка на связанный класс или метод
@param — передаваемый параметр, имеет направление ([in], [out], [in,out])
@return — возвращаемое значение
Также мы используем latex-формулу: чтобы обозначить ее, надо в начале и в конце вставить \f$
. Для создания latex-формул можно использовать онлайн редактор latex.
Также существуют следующие метки:
@authors — автор/ы
@version — версия
@date — дата
@bug — известные ошибки
@copyright — лицензия
@example — файл примера работы
@throws — исключение во время работы
@mainpage Title — комментарий содержит текст для титульного листа документации
@file fname — описание конкретного файла
@deprecated — помечает класс или метод устаревшим. Как и с @todo, Doxygen генерирует отдельную страницу со списком всех устаревших классов и методов
В действительности, Doxygen поддерживает куда больше команд. Например, он позволяет писать многостраничную @pagee) документацию с разделами @sectionn) и подразделами @subsectionn), указывать версии методов и классов @versionn), и не только.
Больше можно прочитать здесь.
Деплой документации на github-pages
Для начала создадим репозиторий на GitHub. Откройте главную вкладку репозитория.
Перейдите на вкладку Settings и откройте раздел Pages:
В этом разделе выберете Static HTML и нажмите кнопку Configure.
Откроется файл конфигурации задачи для CI/CD пайплайна. Все настройки хранятся в static.yaml файле. С помощью пайплайна можно вызывать системные команды. Вызов всех команд описывается с помощью шагов. Описание шагов начинается после строки steps. Путь к этой строке: jobs -> deploy -> steps.
Первые два шага по умолчанию выглядят так:
При обновлении GitHub Pages может что-то поменяться, но я не думаю, что будет сложно понять, где надо писать свои шаги.
Теперь требуется добавить шаги с установкой Doxygen. В качестве системы используется Ubuntu, а значит пакеты устанавливаются через apt.
# Install Doxygen
- name: Install Doxygen
run: sudo apt install doxygen && doxygen --version
# Create documentation
- name: Create documentation
run: doxygen
Документация создается, но надо ее развернуть. Для этого необходимо указать путь к папке с index.html в шаге Upload artifact. Путь к главной странице сайта: ./html/index.html. Тогда этот шаг будет выглядеть так:
Я же указал в конфигурации Doxyfile, что документация сохраняется в docs, поэтому я указываю путь ./docs/html/index.html.
На этом настройка закончена. Ссылка на документацию находится в раздел Settings -> Pages. То есть <username>.github.io/<reponame>
.
P.S. инструкция взята с этой статье
...
❯ Пишем код
Итак, у вас должна быть следующая структура:
.
├── include
│ └── libnumerixpp
│ ├── core
│ │ └── common.hpp
│ ├── libnumerixpp.hpp
│ ├── mathematics
│ │ ├── core.hpp
│ │ └── quadratic_equations.hpp
│ └── physics
│ ├── core.hpp
│ └── kinematics.hpp
└── src
├── core
│ └── common.cpp
├── libnumerixpp.cpp
├── mathematics
│ ├── core.cpp
│ └── quadratic_equations.cpp
└── physics
├── core.cpp
└── kinematics.cpp
Я буду писать библиотеку для вычислений по физике и математике (называться она будет libnumerixpp). Исходный код будет храниться в моем репозитории
Модульная и расширяемая структура позволит в последствии добавлять больше функционала. В директории include существует поддиректория libnumerixpp, именно там и хранятся основные заголовочные файлы.
Начнем с примера — файл libnumerixpp.cpp:
/**
* @file
* @brief A Powerful C++ Library for High-Performance Numerical Computing
* @version 0.1.0
* @date 2024-09-21
* @authors alxvdev
* @copyright Apache 2.0 License
*/
#include <iostream>
void println(std::string string) {
std::cout << string << std::endl;
}
И его заголовочного файла libnumerixpp.hpp:
#ifndef LIBNUMERIXPP_HPP
#define LIBNUMERIXPP_HPP
#include <string>
#pragma once
/**
* @brief Print string with new line
*
* @param[in] string The string
*/
void println(std::string string);
#endif // LIBNUMERIXPP_HPP
Комментарии-документацию я вывожу именно в include-файлах.
❯ Блок ядра библиотеки
Директория core/ — это базовый модуль, в котором будут содержаться некоторые основные функции нашей библиотеки.
Итак, core/common.cpp:
/**
* @file
* @brief Core common utils for libnumerixpp
* @authors alxvdev
*/
#include <chrono>
#include <functional>
#include <iostream>
void credits(void) {
std::cout << "libnumerixpp v0.1.0 - A powerful C++ Library for High-Performance Numerical Computing" << std::endl;
std::cout << "Licensed by Apache License" << std::endl;
std::cout << "Developed&maintained by @alxvdev" << std::endl;
std::cout << std::endl;
}
А теперь его заголовочный файл:
/**
* @file
* @brief Core common utils for libnumerixpp
* @authors alxvdev
*/
#ifndef LIBNUMERIXPP_COMMON_HPP
#define LIBNUMERIXPP_COMMON_HPP
#pragma once
/**
* @brief print credits
*/
void credits(void);
#endif // LIBNUMERIXPP_COMMON_HPP
Физика
Т.к. модуль физики на данный момент меньше чем модуль математики, начнем именно с него.
Создадим базовый файл physics/core.cpp:
/**
* @file
* @brief Core utils for physics
* @authors alxvdev
*/
namespace physics {
}
Здесь ничего на данный момент нету, просто пустой namespace physics. Позже мы его дополним.
И по традиции, physics/core.hpp:
/**
* @file
* @brief Core utils for physics
* @authors alxvdev
*/
#ifndef LIBNUMERIXPP_PHYSICS_CORE_HPP
#define LIBNUMERIXPP_PHYSICS_CORE_HPP
#pragma once
/**
* @brief Basic physics utils
*
* @include example-1.cpp
*/
namespace physics {
}
#endif // LIBNUMERIXPP_PHYSICS_CORE_HPP
Следующий шаг — уже настоящий функционал модуля. Для примера я взял базовую кинематику. Рассмотрим файл physics/kinematics.cpp:
/**
* @file
* @brief Physics utils for kinematics
* @authors alxvdev
*/
#include "libnumerixpp/physics/core.hpp"
namespace physics::kinematics {
double calculatePath(double speed, double time) {
return speed * time;
}
double calculateSpeed(double path, double time) {
return path / time;
}
double calculateTime(double path, double speed) {
return path / speed;
}
double calculateFinalVelocity(double initial_velocity, double acceleration, double time) {
return initial_velocity + acceleration * time;
}
double calculateFinalPosition(double initial_position, double initial_velocity, double acceleration, double time) {
return initial_position + initial_velocity * time + 0.5 * acceleration * time * time;
}
}
Здесь вы видите 5 функций. Первые три являются разными подвидами одной формулы: S = v * t (путь равен скорости умноженной на время). А вот две оставшиеся поинтереснее:
calculateFinalVelocity высчитывает конечную скорость объекта, используя формулу: v = u + at, где:
v — конечная скорость (м/c)
u — начальная скорость (м/c)
a — ускорение (м/с)
t — время, в течение которого действовало ускорение (с)
calculateFinalPosition высчитывает конечную позицию объекта, используя формулу: s = s0 + v0 * t + 0.5 * a * t^2, где:
s — конечное положение
s0 — начальное положение
v0 — начальная скорость
a — ускорение
t — время
Код простой, но база сделана. Если вам понравится данная статья, я могу сделать 2 часть, где мы добавим больше функционала.
И не забудем про файл physics/kinematics.hpp:
/**
* @file
* @brief Physics utils for kinematics
* @authors alxvdev
*/
#ifndef LIBNUMERIXPP_PHYSICS_KINEMATICS_HPP
#define LIBNUMERIXPP_PHYSICS_KINEMATICS_HPP
#pragma once
/**
* @brief Namespace of kinematics (physics)
*
* @todo Add more kinematics equations
*/
namespace physics::kinematics {
/**
* @brief Calculates the path.
*
* This function is based on basic formule: S = v * t
* where:
* + S - path (m)
* + v - speed (m/s)
* + t - time (s)
*
* @param[in] speed The speed
* @param[in] time The time
*
* @return The path.
*/
double calculatePath(double speed, double time);
/**
* @brief Calculates the speed.
*
* This function is based on basic formule: S = v * t
* where:
* + S - path (m)
* + v - speed (m/s)
* + t - time (s)
*
* @param[in] path The path
* @param[in] time The time
*
* @return The speed.
*/
double calculateSpeed(double path, double time);
/**
* @brief Calculates the time.
*
* This function is based on basic formule: S = v * t
* where:
* + S - path (m)
* + v - speed (m/s)
* + t - time (s)
*
* @param[in] path The path
* @param[in] speed The speed
*
* @return The time.
*/
double calculateTime(double path, double speed);
/**
* @brief Calculates the final velocity.
*
* This function is based on the basic kinematics equation: v = u + at
* where:
* + v - final velocity (m/s)
* + u - start speed (m/s)
* + a - accelaration (m/s^2)
* + t - time (s)
*
* @param[in] initial_velocity The initial velocity
* @param[in] acceleration The acceleration
* @param[in] time The time
*
* @return The final velocity.
*/
double calculateFinalVelocity(double initial_velocity, double acceleration, double time);
/**
* @brief Calculates the final position.
*
* This function is based on the kinematics equation: s = u*t + 0.5 * a * t^2
* where:
* + s - final position (m)
* + u - start speed (m/s)
* + a - acceleration (m/s^2)
* + t - time (s)
*
* @param[in] initial_position The initial position
* @param[in] initial_velocity The initial velocity
* @param[in] acceleration The acceleration
* @param[in] time The time
*
* @return The final position.
*/
double calculateFinalPosition(double initial_position, double initial_velocity, double acceleration, double time);
}
#endif // LIBNUMERIXPP_PHYSICS_KINEMATICS_HPP
Перейдем к следующему модулю.
Математика
Итак, начнем с mathematics/core.cpp:
/**
* @file
* @brief Core utils for mathematics
* @authors alxvdev
*/
#include <math.h>
namespace mathematics {
double oldApproximatePower(double base, double exponent) {
union {
double d;
long long i;
} u = { base };
long long magicNum = 4606853616395542500L;
u.i = (long long)(magicNum + exponent * (u.i - magicNum));
return u.d;
}
double binaryPower(double base, unsigned long long exponent) {
double v = 1.0;
while(exponent != 0) {
if((exponent & 1) != 0)
v *= base;
base *= base;
exponent >>= 1;
}
return v;
}
double fastPowerDividing(double base, double exponent) {
if(base == 1.0 || exponent == 0.0)
return 1.0;
double eAbs = fabs(exponent);
double el = ceil(eAbs);
double basePart = oldApproximatePower(base, eAbs / el);
double result = binaryPower(basePart, (unsigned long long)el);
if(exponent < 0.0) {
return 1.0 / result;
}
return result;
}
double anotherApproximatePower(double base, double exponent) {
union {
double d;
int x[2];
} u = { base };
u.x[1] = (int)(exponent * (u.x[1] - 1072632447) + 1072632447);
u.x[0] = 0;
return u.d;
}
double fastPowerFractional(double base, double exponent) {
if(base == 1.0 || exponent == 0.0)
return 1.0;
double absExp = fabs(exponent);
unsigned long long eIntPart = (long long)absExp;
double eFractPart = absExp - eIntPart;
double result = oldApproximatePower(base, eFractPart) * binaryPower(base, eIntPart);
if(exponent < 0.0)
return 1.0 / result;
return result;
}
double add_percent_to_number(double number, double percentage) {
double oneperc = number / 100;
double result = number + (oneperc * percentage);
return result;
}
double square_it_up(double num) {
return num * num;
}
double get_square_root(double num) {
if (num <= 0)
return 0;
int exp = 0;
num = frexp(num, &exp);
if (exp & 1) {
exp--;
num *= 2;
}
double y = (1 + num) / 2;
double z = 0;
while (y != z) {
z = y;
y = (y + num / y) / 2;
}
return ldexp(y, exp / 2);
}
}
В начале вы можете увидеть разные виды возведения числа в степень, мы их рассмотрим ниже. А вот в конце находятся более простые функции — для добавления процента к числу, возведения в квадрат, и получения квадратного корня. Функция получения квадратного корня работает так: если входное число меньше или ровно 0, возвращаем 0. Далее используем frexp() для выделения мантиссы и порядка числа num. Мантиссу помещаем в переменную num, а порядок числа — в exp. Если порядок числа exp является нечетным, то его уменьшают на 1 и мантиссу num умножают на 2. Это необходимо для того, чтобы привести число к виду, когда мантисса находится в диапазоне [0.5, 1) (от 0.5 включая до 1 не включая). Затем инициализируются две перемнные: y=начальное значение, равно среднему между 1 и мантиссой num, и z=0. Далее выполняется итеративный цикл вычисления квадратного корня. После возвращаем результат, умноженный но соответствующую степень 2, вычисленную ранее на основе порядке exp.
А теперь, чтобы перейти к методам возведения в степень, создадим mathematics/core.hpp:
/**
* @file
* @brief Core utils for mathematics
* @authors alxvdev
*/
#ifndef LIBNUMERIXPP_MATHEMATICS_CORE_HPP
#define LIBNUMERIXPP_MATHEMATICS_CORE_HPP
#pragma once
/**
* @brief Basic mathematics utils
* @include example-2.cpp
*/
namespace mathematics {
/**
* @brief Algorithm for fast exponentiation "'Old' approximation"
*
* @details If accuracy is not important to you and the degrees are in
* the range from -1 to 1, you can use this method (see also
* 'another' approximation: anotherApproximatePower()). This
* method is based on the algorithm used in the 2005 game Quake
* III Arena. He raised the number x to the power -0.5, i.e.
* found the value: \f$\frac{1}{\sqrt{x}}\f$
*
* @param[in] base The base
* @param[in] exponent The exponent
*
* @return raised value
*/
double oldApproximatePower(double base, double exponent);
/**
* @brief Algorithm for fast exponentiation "'Another' approximation"
*
* @details Speed increase: ~9 times. Accuracy: <1.5%. Limitations:
* accuracy drops rapidly as the absolute value of the degree
* increases and remains acceptable in the range [-10, 10] (see
* also 'old' approximation: oldApproximatePower()).
*
* @param[in] base The base
* @param[in] exponent The exponent
*
* @return raised value
*/
double anotherApproximatePower(double base, double exponent);
/**
* @brief Algorithm: Binary exponentiation
* @details Speed increase: ~7.5 times on average, the advantage remains
* until numbers are raised to a power of 134217728, Speed
* increase: ~7.5 times on average, the advantage remains until
* the numbers are raised to the power of 134217728. Error:
* none, but it is worth noting that the multiplication
* operation is not associative for floating point numbers, i.e.
* 1.21 * 1.21 is not the same as 1.1 * 1.1 * 1.1 * 1.1,
* however, when compared with standard functions, errors do not
* arise, as mentioned earlier. Restrictions: the degree must be
* an integer not less than 0
*
* @param[in] base base
* @param[in] exponent exponent
*
* @return raised value
*/
double binaryPower(double b, unsigned long long e);
/**
* @brief Algorithm: "Dividing fast power"
*
* @details Speed increase: ~3.5 times. Accuracy: ~13%. The code below
* contains checks for special input data. Without them, the
* code runs only 10% faster, but the error increases tenfold
* (especially when using negative powers).
*
* @param[in] base The base
* @param[in] exponent The exponent
*
* @return raised value
*/
double fastPowerDividing(double base, double exponent);
/**
* @brief Algorithm: "Fractional fast power"
* @details Speed increase: ~4.4 times. Accuracy: ~0.7%
*
* @param[in] base The base
* @param[in] exponent The exponent
*
* @return raised value
*/
double fastPowerFractional(double base, double exponent);
/**
* @brief Adds a percent to number.
*
* @param[in] number The number
* @param[in] percentage The percentage
*
* @return number
*/
double add_percent_to_number(double number, double percentage);
/**
* @brief Gets the number square (N^2).
*
* @param[in] num The number
*
* @return The number square.
*/
double square_it_up(double num);
/**
* @brief Gets the square root.
*
* @param[in] num The number
*
* @return The square root.
*/
double get_square_root(double num);
}
#endif // LIBNUMERIXPP_MATHEMATICS_CORE_HPP
Заново изобретаем std::pow
В этом разделе я покажу реализации нескольких нестандартных алгоритмов для возведения числа в степень. Реализация и описание способ взяты из этой статьи на хабре.
Скорость алгоритмов сравнивается с std::pow.
Алгоритм: «Старая аппроксимация»
Этот метод основан на алгоритме, использованном в игре Quake III Arena 2005 года. Он возводил число x в степень -0.5.
Увеличение скорости: в ~11 раз.
Погрешность: <2%.
Ограничения: приемлемая точность только для степеней от -1 до 1.
double oldApproximatePower(double base, double exponent) {
union {
double d;
long long i;
} u = { base };
long long magicNum = 4606853616395542500L;
u.i = (long long)(magicNum + exponent * (u.i - magicNum));
return u.d;
}
magicNum = это магическое число. Вы можете узнать о нем подробнее в этой статье.
Алгоритм: Бинарное возведение в степень
Увеличение скорости: в среднем в ~7.5 раз, преимущество сохраняется до возведения чисел в степень 134217728.
Погрешность: нет, но стоит отметить, что операция умножения не ассоциативна для чисел с плавающей точкой, т.е. 1.21 * 1.21 не то же самое, что 1.1 * 1.1 * 1.1 * 1.1, однако при сравнении со стандартными функциями погрешности, как уже сказано ранее, не возникает.
Ограничения: степень должна быть целым числом не меньше 0
double binaryPower(double base, unsigned long long exponent) {
double v = 1.0;
while(exponent != 0) {
if((exponent & 1) != 0)
v *= base;
base *= base;
exponent >>= 1;
}
return v;
}
Широко известный алгоритм для возведения любого числа в целую степень с абсолютной точностью. Принцип действия прост: есть целая степень e, чтобы получить число b в этой степени нужно возвести это число во все степени 1, 2, 4, … 2n (в коде этому соответствует base *= base
), каждый раз сдвигая биты e вправо (e >>= 1) пока оно не равно 0 и тогда, когда последний бит e не равен нулю ((exponent & 1) != 0), домножать результат v на полученное base.
Алгоритм: «Делящая быстрая степень»
Увеличение скорости: в ~3.5 раз
Погрешность: ~13%
Примечание: в коде ниже присутствуют проверки для особых входных данных. Без них код работает всего на 10% быстрее, но погрешность возрастает в десятки раз (особенно при использовании отрицательных степеней).
double fastPowerDividing(double base, double exponent) {
if(base == 1.0 || exponent == 0.0)
return 1.0;
double eAbs = fabs(exponent);
double el = ceil(eAbs);
double basePart = oldApproximatePower(base, eAbs / el);
double result = binaryPower(basePart, (unsigned long long)el);
if(exponent < 0.0) {
return 1.0 / result;
}
return result;
}
Алгоритм: «Дробная быстрая степень»
Увеличение скорости: в ~4.4 раза
Погрешность: ~0.7%
double fastPowerFractional(double base, double exponent) {
if(base == 1.0 || exponent == 0.0)
return 1.0;
double absExp = fabs(exponent);
unsigned long long eIntPart = (long long)absExp;
double eFractPart = absExp - eIntPart;
double result = oldApproximatePower(base, eFractPart) * binaryPower(base, eIntPart);
if(exponent < 0.0)
return 1.0 / result;
return result;
}
По сути, любое число состоит из суммы двух частей: целой и дробной. Целую можно использовать для возведения основания в степень при помощи бинарного возведения, а дробную — при помощи «старой» аппроксимации.
Алгоритм: «Другая аппроксимация»
Увеличение скорости: в ~9 раз
Погрешность: <1.5%
Ограничения: точность стремительно падает при повышении абсолютного значения степени и остается приемлемой в промежутке [-10, 10]
double anotherApproximatePower(double base, double exponent) {
union {
double d;
int x[2];
} u = { base };
u.x[1] = (int)(exponent * (u.x[1] - 1072632447) + 1072632447);
u.x[0] = 0;
return u.d;
}
Сравнение
Каждый из перечисленных методов дает различную точность и скорость. Поэтому, прежде чем использовать стандартную функцию возведения в степень, стоит проанализировать какие у вас будут входные данные, и насколько вам важна точность. Выбор правильного метода может существенно ускорить работу вашей программы.
Квадратные уравнения
Продолжим улучшение модуля математики — на этот раз займемся квадратными уравнениями.
Создадим файл mathematics/quadratic_equations.cpp:
/**
* @file
* @brief Math utils for quadratic
* @authors alxvdev
*/
#include <vector>
#include "libnumerixpp/mathematics/core.hpp"
namespace mathematics::quadratic {
double calculateDiscriminant(double a, double b, double c) {
double d = mathematics::square_it_up(b) - 4 * a * c;
return d;
}
std::vector<double> calculateRootsByDiscriminant(double discriminant, double a, double b) {
std::vector<double> roots;
if (discriminant > 0) {
double x1 = (-b + get_square_root(discriminant)) / (2 * a);
double x2 = (-b - get_square_root(discriminant)) / (2 * a);
roots.push_back(x1);
roots.push_back(x2);
} else if (discriminant == 0) {
double x1 = -b + get_square_root(discriminant) / (2 * a);
roots.push_back(x1);
}
return roots;
}
std::vector<double> getRootsByVietaTheorem(double a, double b, double c) {
std::vector<double> roots;
if (a == 0)
return roots;
double roots_sum = -(b / a);
double roots_mul = c / a;
roots.push_back(roots_sum);
roots.push_back(roots_mul);
return roots;
}
}
Функция calculateDiscriminant высчитывает дискриминант по формуле D = b^2 - 4ac.
Функция calculateRootsByDiscriminant высчитывает корни уравнения. На вход принимает дискриминант, переменные a и b. На выходе возвращает vector (массив с неясным количеством элементов, проще говоря) типа double. Если дискриминант больше 0, то корня два, высчитываем их по формуле: -b +- квадратный корень из дискриминанта / 2a. Если дискриминант равен 0, то корень 1. А если дискриминант равен нулю — то корней нет.
Функция getRootsByVietaTheorem «получает корни» по теореме Виета. Теорема Виета гласит, если a != 0, значит: x1 + x2 = - b / a И x1 * x2 = c / a. В данной функции мы как раз и получаем их. А вот реализацию поиска корней я решил пока не делать.
И следующим шагом мы создаем файл mathematics/quadratic_equations.hpp:
/**
* @file
* @brief Quadratic utils for mathematics
* @authors alxvdev
*/
#ifndef LIBNUMERIXPP_MATHEMATICS_QUADRATIC_EQUATIONS_HPP
#define LIBNUMERIXPP_MATHEMATICS_QUADRATIC_EQUATIONS_HPP
#pragma once
#include <vector>
#include "libnumerixpp/mathematics/core.hpp"
/**
* mathematics utils for quadratic equations and other
*
* @include example-2.cpp
*/
namespace mathematics::quadratic {
/**
* @brief Calculates the discriminant.
*
* @details Based on discriminant formula: \f$b^{2} - 4ac\f$
*
* @param[in] a a
* @param[in] b b
* @param[in] c c
*
* @return The discriminant.
*/
double calculateDiscriminant(double a, double b, double c);
/**
* @brief Calculates the roots by discriminant.
*
* @details Calculate the roots by discriminant \f$\frac{-b +-
* \sqrt{D}}{2a}\f$. D > 0 = 2 roots, D == 0 = 1 root, D < 0 = 0
* roots.
*
* @param[in] discriminant The discriminant
* @param[in] a a
* @param[in] b b
*
* @return The roots by discriminant.
*/
std::vector<double> calculateRootsByDiscriminant(double discriminant, double a, double b);
/**
* @brief Gets the roots by vieta theorem.
*
* @param[in] a a
* @param[in] b b
* @param[in] c c
*
* @return The roots by vieta theorem.
*/
std::vector<double> getRootsByVietaTheorem(double a, double b, double c);
}
#endif // LIBNUMERIXPP_MATHEMATICS_QUADRATIC_EQUATIONS_HPP
Мы закончили разработку модулей. Пора перейти к примерам.
Примеры
В данной части статьи мы создадим файлы example-1.cpp и example-2.cpp в директории examples. В этих файлах будут содержаться примеры работы библиотеки.
Напишем example-1.cpp, где мы будем тестировать модуль физики:
#include <iostream>
#include "libnumerixpp/libnumerixpp.hpp"
#include "libnumerixpp/core/common.hpp"
#include "libnumerixpp/physics/core.hpp"
#include "libnumerixpp/physics/kinematics.hpp"
int main() {
credits();
say_hello("LIBNUMERIXPP");
double speed = 10.0;
double time = 5.0;
double path = physics::kinematics::calculatePath(speed, time);
speed = physics::kinematics::calculateSpeed(path, time);
time = physics::kinematics::calculateSpeed(path, speed);
std::cout << "Calculate: speed=" << speed << "m/s" << "; time=" << time << "s" << "; path=" << path << "m" << std::endl;
double finalVelocity = physics::kinematics::calculateFinalVelocity(10.0, 10.0, 10.0);
std::cout << "final velocity (10.0, 10.0, 10.0) = " << finalVelocity << std::endl;
double finalPosition = physics::kinematics::calculateFinalPosition(10.0, 10.0, 10.0, 10.0);
std::cout << "final position (10.0, 10.0, 10.0, 10.0) = " << finalVelocity << std::endl;
return 0;
}
При сборке данный файл выведет:
libnumerixpp v0.1.0 - A powerful C++ Library for High-Performance Numerical Computing
Licensed by Apache License
Developed&maintained by @alxvdev
LIBNUMERIXPP
Calculate: speed=10m/s; time=5s; path=50m
final velocity (10.0, 10.0, 10.0) = 110
final position (10.0, 10.0, 10.0, 10.0) = 110
А теперь второй файл - example-2.cpp, где будут примеры использования модуля математики:
#include <iostream>
#include <vector>
#include "libnumerixpp/libnumerixpp.hpp"
#include "libnumerixpp/core/common.hpp"
#include "libnumerixpp/mathematics/core.hpp"
#include "libnumerixpp/mathematics/quadratic_equations.hpp"
int main() {
credits();
println("LIBNUMERIXPP");
// SQUARE AND SQR //
double num = 100.0;
double num_sq = mathematics::square_it_up(num);
double num_sqr = mathematics::get_square_root(num);
std::cout << "Square " << num << ": " << num_sq << std::endl;
std::cout << "Square root " << num << ": " << num_sqr << std::endl;
std::cout << std::endl;
// CALCULATE QUADRATIC EQUATION BY DISCRIMINANT //
double a = -2;
double b = 5;
double c = 5;
double d = mathematics::quadratic::calculateDiscriminant(a, b, c);
std::vector<double> roots = mathematics::quadratic::calculateRootsByDiscriminant(d, a, b);
std::cout << "Quadratic Equation: a=" << a << "; b=" << b << "; c=" << c << std::endl;
std::cout << "D=" << d << std::endl;
std::cout << "Roots:" << std::endl;
for (double root : roots) {
std::cout << root << std::endl;
}
std::cout << std::endl;
// PERCENTAGE //
double nump = mathematics::add_percent_to_number(100.0, 10.0);
std::cout << "100+10%: " << nump << std::endl;
std::cout << std::endl;
// POWER / Algorithms for fast exponentiation //
double bestPowVal = 100;
double pow_results[5] = {
mathematics::oldApproximatePower(10.0, 2.0),
mathematics::anotherApproximatePower(10.0, 2.0),
mathematics::binaryPower(10.0, 2),
mathematics::fastPowerDividing(10.0, 2.0),
mathematics::fastPowerFractional(10.0, 2.0)
};
std::cout << "0 oldApproximatePower : base 10 exponent 2: " << pow_results[0] << std::endl;
std::cout << "1 anotherApproximatePower: base 10 exponent 2: " << pow_results[1] << std::endl;
std::cout << "2 binaryPower : base 10 exponent 2: " << pow_results[2] << std::endl;
std::cout << "3 fastPowerDividing : base 10 exponent 2: " << pow_results[3] << std::endl;
std::cout << "4 fastPowerFractional : base 10 exponent 2: " << pow_results[4] << std::endl;
for (int i = 0; i < sizeof(pow_results) / sizeof(pow_results[0]); i++) {
double error = bestPowVal - pow_results[i];
std::cout << "POW Algorithm #" << i << ": error=" << error << std::endl;
}
return 0;
}
При компиляции данный файл выведет:
libnumerixpp v0.1.0 - A powerful C++ Library for High-Performance Numerical Computing
Licensed by Apache License
Developed&maintained by @alxvdev
LIBNUMERIXPP
Square 100: 10000
Square root 100: 10
Quadratic Equation: a=-2; b=5; c=5
D=65
Roots:
-0.765564
3.26556
100+10%: 110
0 oldApproximatePower : base 10 exponent 2: 100.673
1 anotherApproximatePower: base 10 exponent 2: 99.711
2 binaryPower : base 10 exponent 2: 100
3 fastPowerDividing : base 10 exponent 2: 100
4 fastPowerFractional : base 10 exponent 2: 96.3496
POW Algorithm #0: error=-0.672563
POW Algorithm #1: error=0.289001
POW Algorithm #2: error=0
POW Algorithm #3: error=0
POW Algorithm #4: error=3.65044
Вот и все. С программированием мы закончили - настало очередь украшательств.
❯ Украшаем репозиторий
Итак, любой проект будет привлекательнее с грамотно оформленным README. У меня в проекте он выглядит так.
Документация поможет пользователям и разработчикам. Да, если им сильно нужно - они могут и просто скачать или почитать код, но многих отсутствие нормальной документации или хотя бы нормального README файла отпугивает. Что вы бы выбрали - хорошо заполненный и документированный репозиторий или такой же репозиторий, но без какой либо информации? Я думаю, ответ очевиден.
Давайте разберем README поэтапно. Начнем с шапки:
# libnumerixpp
<p align="center">A Powerful C++ Library for High-Performance Numerical Computing</p>
<br>
<p align="center">
<img src="https://img.shields.io/github/languages/top/alxvdev/libnumerixpp?style=for-the-badge">
<img src="https://img.shields.io/github/languages/count/alxvdev/libnumerixpp?style=for-the-badge">
<img src="https://img.shields.io/github/license/alxvdev/libnumerixpp?style=for-the-badge">
<img src="https://img.shields.io/github/stars/alxvdev/libnumerixpp?style=for-the-badge">
<img src="https://img.shields.io/github/issues/alxvdev/libnumerixpp?style=for-the-badge">
<img src="https://img.shields.io/github/last-commit/alxvdev/libnumerixpp?style=for-the-badge">
</p>
> [!CAUTION]
> At the moment, libnumerixpp is under active development (alpha), many things may not work, and this version is not recommended for use (all at your own risk).
libnumerixpp is a powerful, cross-platofrm C++ library designed for high-performance numerical computing in the domains of physics, mathematics, and computer science.
You can join to our [small russian telegram blog](https://t.me/hex_warehouse).
You can view docs for libnumerixpp [here](https://alxvdev.github.io/libnumerixpp).
Вверху мы указываем заголовок, название проекта. Ниже описание и бейджи. Бейджи нужны для общей статистики проекта — лицензия, язык, звезды и т.д. Замените /alxvdev/libnumerixpp
на свой юзернейм и репозиторий.
После мы вставляем предупреждение в виде цитаты, что проект в стадии разработки и может быть не стабилен.
Ниже также детали, и другие важные вещи, на которые сразу стоит обратить внимания. У меня это ссылка на телеграм-канал и на документацию.
Идем дальше:
## Key Features
- **Extensive Functionality**: libnumerixpp provides a wide range of functions coverint the core areas of mathematics, physics, and computer science, including:
- Mathematics: linear algebra, calculus, geometry
- Physics: kinematics, mechanics, thermodynamics, electronics
- Computer Science: algorithms, numerical methods, data processing
- **High Performance**: The library is optimized for maximum performance, leveraging modern techniques such as parallel computing and vectorization
- **Cross-platform Support**: libnumerixpp supports major operating systems (Windows, Linux, macOS) and can be compiled using various compilers (GCC, CLANG, MSVC).
- **Ease of Use**: A simple and intuitive API, comprehensive documentation, and numerous examples facilitate the integration of the library into your projects.
- **Modular Architecture**: libnumerixpp is designed with a modular structure, allowing selective compilation of only the required components.
- **Extensibility**: The library is open to the developer community, who can contribute improvements and additions.
Основные возможности. Здесь можно перечислить, что может ваш проект, какие у него преимущества.
Далее вы можете также создавать разные блоки, рассказывающие, например, о структуре.
Желательно еще дать инструкции по установке и сборке. Посмотрите, как сделано в моем репозитории.
Следующим шагом я бы сделал ссылки на примеры использование, зависимости и инструменты, а также раздел документации и поддержки:
## Examples
Below you can see examples of using libnumerixpp in your project. Also, you can view [examples dir](./examples).
### example-1 Speed, Time, Path
Source code: [example-1.cpp](./examples/example-1.cpp)
This example shows how to calculate the *speed, path, and time* using the `physics::kinematics`.
### example-2 Math, Quadratic and math
Source code: [example-2.cpp](./examples/example-2.cpp)
This example shows how to calculate the *quadratic equations, discriminant, squares* using the `mathematics` and `mathematics::quadratic`.
## Tools and Dependencies
linumerixpp utilizes the following tools and libraries:
- **CMake**: Cross-platform build system
- **Doxygen**: Documentation generation
## Documentation
Detailed documentation, including user guides, API reference, and code examples, is available in the [docs](https://alxvdev.github.io/libnumerixpp/). Or you can see articles or additional info in [en docs dir](./docs/en/index.md) or [ru docs dir](./docs/ru/index.md).
If you have any questions, suggestions, or encounter issues, please create a new [issue](https://github.com/alxvdev/libnumerixpp/issues/new) in the repository. We'll be happy to assist you and improve the library.
You can also write to me on Telegram: [@alexeev_dev](https://t.me/alexeev_dev)
libnumerixpp is an Open Source project, and it only survives due to your feedback and support!
Project releases are available at [this link](https://github.com/alxvdev/libnumerixpp/releases).
И в самом конце можно указать копирайты:
## Copyright
libnumerixpp is released under the [Apache License 2.0](LICENSE).
Copyright © 2024 Alexeev Bronislav. All rights reversed.
Я хочу сказать, что это не требования, а мои рекомендации. Вы можете сделать по другому.
И небольшое наставление по коммитам. По некоторым правилам, коммит должен быть в повелительном наклонении, потому что это, как бы указание разработчику, что надо делать. Но это не указание, это также рекомендации, вы вольны использовать что хотите (или что захочет ваша команда).
Описание коммитов
Название |
Описание |
---|---|
build |
Сборка проекта или изменения внешних зависимостей |
sec |
Безопасность, уязвимости |
ci |
Настройка CI и работа со скриптами |
docs |
Обновление документации |
feat |
Добавление нового функционала |
fix |
Исправление ошибок |
perf |
Изменения направленные на улучшение производительности |
refactor |
Правки кода без исправления ошибок или добавления новых функций |
revert |
Откат на предыдущие коммиты |
style |
Правки по кодстайлу (табы, отступы, точки, запятые и т.д.) |
test |
Добавление тестов |
Пример:
git commit -m "feat: add line output" # en, добавьте вывод строки
git commit -m "feat: добавьте вывод строки" # ru
Больше информации об оформлении репозитория вы можете увидеть в моей старой статье.
Вот и все. Минимальный комплект минимальной библиотеки. Надеюсь, я сподвиг вас к чему-то большему. Пишите, творите, публикуйте!
❯ Заключение
Спасибо за внимание! Это был довольно интересный опыт для меня, т.к. это мой первый большой проект на языке C++, где я попытался его изучить более подробно.
Если у вас есть замечания по статье или по коду — пишите, наверняка есть более опытный и профессиональный программист на C++, который может помочь как и читателям статьи, так и мне.
Ссылка на мой репозиторий библиотеки здесь.
Буду рад, если вы присоединитесь к моему небольшому телеграм-блогу. Анонсы статей, новости из мира IT и полезные материалы для изучения программирования и смежных областей.
P.S. вы можете прорекламировать свою библиотеку в комментариях, если она интересная и может кому-то пригодиться.
? Читайте также:
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале ↩
Перейти ↩
Источники
Комментарии (54)
LesleySin
26.09.2024 08:19+8Если смотреть на статью, как на туториал (что указано), то получился хороший get started гайд. Мне, как человеку далекому от C++, зачастую непонятно, как было бы "правильно" организовать исходник и собрать их с помощью сmake.
Другая сторона медали - обилие примеров кода библиотеки, которые напрямую не относятся к теме статьи. Можно ведь было сделать гораздо короче, и читабельность повысилась бы (многое я проскролил, мог упустить что-то важное).
И тем не менее спасибо за статью, давно хотел поэкспериментировать с плюсами и теперь есть отправная точка для этого :)DrArgentum Автор
26.09.2024 08:19+4Благодарствую! Вторая часть будет тоже иметь примеры кода, но в первую очередь я напишу самое важное.
MonkeyWatchingYou
26.09.2024 08:19Код не помешает :) Спасибо за статью!
Редко нынче утреннее чтиво приятно иполезно, а тут всё есть.
aax
26.09.2024 08:19вначале C++ просто дополнял язык Си некоторыми возможностями объектно-ориентированного программирования. И поэтому сам Страуструп вначале называл его как «C with classes».
Сколько себя помню Си всегда был со структурами, от классов в С++ они отличаются названием(в т.ч. и в синтаксисе языка) и противоположными дефолтами их содержимого по части public и private.
Возможностей для более изощренного ООП С++ да заметно больше, но тут как всегда достоинства и недостатки - это разные стороны одной и той же медали - С++ медленнее чистого Си, особенно С++ с применением продвинутого ООП(в котором к слову легче наделать трудно отлавливаемые ошибки), что и определяет сферы применения обеих ЯП, каждый из которых активно используется по настоящее время, и имеет свои предпочтительные ниши.
DrArgentum Автор
26.09.2024 08:19+2Насчет медленности согласен, сам Торвальдс говорил что "ядро линукс скомпилируется быстрее чем программа на плюсах"
Nipheris
26.09.2024 08:19+2Многие скажут, что это вкусовщина, но мне кажется нет смысла индексный хедер класть внутрь header namespace (т.е. внутрь поддиректории). Хочется подключать как
#include <libnumerixpp.hpp>
ну или#include <libnumerixpp/all.hpp>
, но не#include <libnumerixpp/libnumerixpp.hpp>
.И кстати да, стандартные и библиотечные инклуды должны использовать угловые скобки
#include <>
, т.к. кавычки на большинстве компиляторов подразумевают поиск сначала в директории проекта, а инклуд с угловыми скобками ищет только по дирам из списка include paths. Из этого кстати следует, что в публичных хедерах могут быть только инклуды с угловыми скобками, а инклуды с кавычками могут быть только в приватных хедерах и файлах реализации.Спасибо за статью!
voldemar_d
26.09.2024 08:19в публичных хедерах могут быть только инклуды с угловыми скобками
А это точно может быть реализовано в 100% случаев?
А если в публичном хедере класс использует, например, нестандартные структуры, которые в другом хедере описаны?
Melirius
26.09.2024 08:19+2Он должен тоже лежать в стандартных папках для поиска - иначе вы перепутываете интерфейс с реализацией, что не очень хорошо.
voldemar_d
26.09.2024 08:19Рекомендация понятна. Предлагаете каждую мою нестандартную структуру оформлять в отдельную библиотеку и инсталлировать её в стандартную папку?
Jogker
26.09.2024 08:19Хорошо бы во второй части таблица не была графиком,
чтобы (ЧТОБЫ чтоБЫ ЧТОбы) совсем всё было хорошо )).
bogolt
26.09.2024 08:19+4Вы приводите очень плохие примеры Си++ кода, я не осилил всю статью но вот это ужасно:
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; } }
Итак у нас есть класс, в котором три переменные. При этом переменная password будет инициализированна своим дефолтным конструктором, то есть получит пустое значение, а вот что мы будем делать с userid и cash? Мда, боюсь внутрь будет записан какой-то мусор из оперативной памяти. Но постойте, ведь у нас есть целых два метода с помощью которых можно менять эти переменные? Ну да можно менять, но повляять на исходные значения никак нельзя.
Тут как в шуточной задачке: "Буратино дали три яблока, два он съел. Сколько яблок осталось у Буратино?
Ответ: неизвестно, потому что мы не знаем сколько у него их было до этого."
А вот собственно что выводит ваш класс ( если открыть доступ к переменным )
User u; cout << "user: " << u.userid << ",cash: " << u.cash << endl;
user: 1061167616,cash: 6.9526e-310
Решение:
используйте значения по умолчанию, благо на дворе давно не 2003 год, новые стандарты позволяют писать намного более безопасный си++
class User { private: int userid = 0; std::string password; double cash = 0.0;
И теперь все становится гораздо лучше. Теперь даже если вы не определите конструктор класса все переменные получат разумные начальные значения.
bogolt
26.09.2024 08:19Ладно, пошел
дальше, попробовал вашим методом посчитать квадратаное уравнение.
// x*x -3x - 4 = 0 ( roots: 4, -1 ) auto a = 1.0; auto b = -3.0; auto c = 4.0; auto disc = calculateDiscriminant(a, b, c); auto r = calculateRootsByDiscriminant(disc, a, b);
Увы ни одного из корней ваша функция найти не смогла, хотя они там есть.
Считаю что если уж вы выкладываете код, он должен быть протестирован, тем более что подобные функции тестировать это же счастье - скачать задачник, напихать в таблицу примеров и ответов, и убедится что ваш код считает все что записано в таблице.
DrArgentum Автор
26.09.2024 08:19+1Буду исправляться. А вот код уравнений я тестировал, правда не так сильно, как вы описали в комментарии ниже. Но все равно, я пересмотрю подход. Спасибо!
dv0ich
26.09.2024 08:19+2По последним стандартам С++ надо писать так :)
class User { private: int userid {}; std::string password {}; double cash {};
belav
26.09.2024 08:19В хэдерах получается "масло масляное": Вы используете #ifndef...#end вместе с #pragma once. Удобней использовать последнее.
klirichek
26.09.2024 08:19+3Звать вручную make, make install - это, конечно, дело вкуса... Особенно если только-только из автотулзов перешёл на cmake...
но если вместоmake
вы позовёте `cmake --build .` - это чуть больше букв печатать, зато нет привязки к make. cmake сам позовёт нужную тулзу (это может быть ninja, а под виндой вообще msbuild). А вам не нужно помнить детали.
То же самое про установку. Это всего лишь одна из целей. `cmake --build . --target install`, возможно с sudo (если ставите в системные пути). И ещё это обычно можно делать без предварительной сборки (поскольку эта одна из целей проекта - она сама подтянет сборку перед установкой как зависимость).Использовать glob в cmake для добавления исходников в общем случае не стоит. Один-два раза собрать пойдёт, но лучше в конце концов заменить на явное перечисление файлов. Причину расскажет любой поисковик по фразе "cmake glob evil"
Вы всего лишь использовали cmake как систему сборки; он остался сугубо внутренней деталью (так же это мог быть обычный makefile или какой-нибудь meson). Нынче под "библиотекой с cmake" подразумевается чуть другое: это библиотека, вместе с которой идёт модуль Cmake для её использования в других проектах. Т.е. скачали исходники библиотеки, добавили к себе через add_subdirectory - и вуаля, у вас появился таргет yourlib::yourlib. Который я могу прилинковать к себе, и всё просто заработает. Ключевая фраза для гугла "cmake export".
DrArgentum Автор
26.09.2024 08:19Учту обязательно! Не знал про глобы. Во второй части обязательно исправлю. Спасибо!
dv0ich
26.09.2024 08:19В JavaScript для этого используется npm, в Python pip, в Haskell есть stack и cabal. То есть у этих языков есть экосистема, которая позволяет без сложностей использовать и публиковать пакеты, и в принципе работать с языком. А у C++ нет таких инструментов-стандартов.
Лично для меня это плюс, что у С++ нет централизованной экосистемы. Бойтесь данайцев, дары приносящих.
UFO_01
26.09.2024 08:19+1ИМХО конечно, но использование glob в cmake это плохая практика, особенно если создавать библиотеку. Лучше использовать просто перечисление а-ля
set(HEADER "${CMAKE_CURRENT_LIST_DIR}/examples/mainwindow.h" ) set(SOURCES "${CMAKE_CURRENT_LIST_DIR}/examples/mainwindow.cpp" "${CMAKE_CURRENT_LIST_DIR}/examples/main.cpp" ) add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})
Или
add_library(mycoollib SHARED "${CMAKE_CURRENT_LIST_DIR}/mycoollib.cpp" "${CMAKE_CURRENT_LIST_DIR}/mycoollib.h" )
Если проект чисто для себя, можете использовать glob, но если это библиотека, лучше перечислять вручную, если либа большая, сделать несколько поддиректорий cmake.
Скрипты на bash тоже лучше не использовать, для этого есть command line tool
В данном случае нас интересует
cmake -E <command> [<options>]
Ибо не линуксом единым жив программист. Особенно если он делает библиотеку.
DrArgentum Автор
26.09.2024 08:19Дело в том, что во второй части будет немного больше функционала у скрипта, больше для удобства разработчика.
UFO_01
26.09.2024 08:19Тогда лучше сделать CmakeLists.txt для каждой директории, как вам уже сказали выше. И добавить cmake модули. Это файлы .cmake если что, их потом можно будет включать-выключать при необходимости прямо из ide.
option(${PROJECT_NAME}_SANITIZE "Сборка приложения в режиме поиска утечек памяти" OFF) if (${PROJECT_NAME}_SANITIZE) include(Sanitize) endif()
Директории в cmake можно создавать так
make_directory("тут пишешь полный путь")
А печатать сообщения
message("обычное сообщение") message(FATAL_ERROR "ошибка") message(STATUS "сообщение статуса")
Cmake для того и создан, чтобы проект можно было собрать где угодно и под что угодно. Рад что кто-то решил сделать туториал для новичков, мне бы это на первых порах здорово бы помогло разобраться.
AnonimYYYs
26.09.2024 08:19Вроде бы неплохой туториал, подробный и с хорошими примерами. Но тема именно заголовка раскрыта не очень, зато подробно описана разница между алгоритмами степени (в контексте статьи - зочем?)
Сам я буквально три дня назад делал аналогично - создавал библиотеку через CMake. Однако основная проблема с которой я столкнулся, которую было относительно трудно решить, и которая не описана в статье - это разница между генерацией статических и динамических библиотек.
Я, буду честен, не до конца разобрался. Когда CMake генерирует екзешник для примеров - он использует .lib, соответсвенно надо генерить .lib. Но, кроме этого, мне была нужна .dll для использования потом через питон. И генерить надо обе штуки, а для этого мне потребовалось в хедеры добавлять дополнительные директивы DLLEXPORT, а тело кода обрамлять в неймспейс extern "c". Как я понял, потому что плюсовый компилятор при компиляции названия функций под капотом изменяет, но с данным экстерном - не изменяет.
И, вот я на самом деле мало разобрался с этим, что и почему и как. Работает и хорошо, типа того. Но с радостью почитал бы про такие вот нюансы, что как куда зачем и главное - почему, а не тысячную статью сравнения алгоритмов.
Tujh
26.09.2024 08:19Это называется манглинг - https://en.wikipedia.org/wiki/Name_mangling
Оно ещё и от компилятора к компилятору отличается.
webhamster
26.09.2024 08:19Я не говорю, что C++ — это молоток, который должен превратить все вокруг вас в гвозди, но его все же стоит изучить и освоить.
Как по мне, так это самый бессмысленный совет. C++ настолько сложен и упорот, что на его изучение и освоение уходят десятилетия. Изучать и осваивать этот язык имеет смысл только в одном случае - вы собираетесь заниматься разработкой на этом языке и готовы посвещать ему все свое время.
Если же в работе будут перерывы, например на другие технологии, то возвращаться на плюсы максимально мучительно. Несколько месяцев будет уходить только на то, чтобы восстановить свои скиллы хоть до какого-то приемлемого уровня.
tixo
26.09.2024 08:19Респект за статью!
Но IMHO:
Для туториала по созданию библиотеки не хватает раздела с Автотестами.
Кроме того от библиотеки на GitHub-е, да ещё и с документацией и примерами, ожидаешь переносимости и возможности настраивать разные виды сборки (динамическая, статическая и т.п.). А это может повлиять и на структуру файлов и на сами исходники.DrArgentum Автор
26.09.2024 08:19Как раз планирую добавить в вторую часть раздал с тестированием и улучшением сборки). Спасибо!
tixo
26.09.2024 08:19И ещё. Мне, как читателю, было бы удобнее если бы статья была разделена на две:
1) туториал по созданию библиотеки;
2) сами алгоритмы на CPP (математика, кинематика и т.п.).
Ещё раз спасибо за статью )DrArgentum Автор
26.09.2024 08:19Как раз так и планирую, вначале все организованные моменты, улучшение структуры сборки, всякая теория и т.д., а вот в конце уже будет сам код библиотеки. Всегда пожалуйста!
abagnale
26.09.2024 08:19Я прокомментирую часть про CMake.
cmake_minimum_required(VERSION 3.10)
Если это новый проект, то лучше конечно ограничить более новой версией, например
3.22
(идёт из коробки в Ubuntu22.04
, хотя уже есть24.04
, где в стандартных пакетах3.28
).set(EXECUTABLE_OUTPUT_DIRECTORY ...
set(LIBRARY_OUTPUT_DIRECTORY ...
Обычно у проекта есть процедура установки, и там все какие надо пути уже прописаны по умолчанию. И хотя у вас там далее есть
install()
, но он несколько недоделанный.file(GLOB_RECURSE ...
Про это уже написали, могу только подтвердить, что глоббинг не особо рекомендуется.
add_library(libnumerixpp SHARED ...
Не нужно хардкодить тип библиотеки, это контролируется с BUILD_SHARED_LIBS. Если только это у вас не header-only библиотека, которой нужно указать INTERFACE.
Помимо этого, вы заявляете
SHARED
и поддержку Windows, однако я не вижу у вас __declspec(dllexport), и в комментариях тоже уже указали, что пары.dll
/.lib
у вас на выходе не будет.set_target_properties(libnumerixpp PROPERTIES OUTPUT_NAME "numerixpp")
Редко когда требуется манипулировать финальным именем бинарника. И кстати добавлять
lib
в имя таргета не рекомендуется (ни сзади, ни спереди).├── include
│ └── libnumerixpp
│ ├── core
│ │ └── common.hpp
│ ├── libnumerixpp.hpp
Здесь могу только сказать плюсадин, я тоже предпочитаю такую структуру с вложенной папкой для публичных заголовочных файлов, а не гадить прямо в корень
include
.target_include_directories(libnumerixpp
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
Вот тут почти всё хорошо, только вместо
include
лучше было бы${CMAKE_INSTALL_INCLUDEDIR}
, хотя это конечно то же самое.set_target_properties(libnumerixpp ...
target_include_directories(libnumerixpp ...
install(TARGETS libnumerixpp ...
Раз у вас имя таргета совпадает с именем проекта, то наверное лучше использовать
${PROJECT_NAME}
ну или отдельную переменную, а не копировать везде одну и ту же строку.install(DIRECTORY include/
DESTINATION include
)
Так конечно тоже работает, но лучше было бы устанавливать публичные заголовочные файлы через
PUBLIC_HEADER
свойство таргета и одноимённый параметр команды install().Кроме того, почему я назвал вашу процедуру установки недоделанной: у вас тут отсутствует создание конфига/пакета для CMake, который облегчил бы другим проектам процесс использования вашей библиотеки.
cmake ..
make
sudo make install
Вообще, обычно вот так делают:
$ cmake .. # -G Ninja -DCMAKE_BUILD_TYPE=Release .. $ cmake --build . --target install # --config Release # если генератор мультиконфиговый
Или если надо собрать пакет для дальнейшего распространения, а не поставить его в систему, то добавить ещё
-DCMAKE_INSTALL_PREFIX="../install"
в первую строку конфигурации.для облегчения этой задачи напишем простой bash-скрипт
Из четырёх строк стало сто :)
Но вообще, в целом, это достаточно хороший уже пример проекта, мне по работе приходится иметь дело с библиотеками в гораздо более плачевном состоянии, хотя их и сопровождают с позволения сказать достаточно серьёзные разработчики с казалось бы опытом.
DrArgentum Автор
26.09.2024 08:19CMake да, я уже изменяю. Решил воспользоваться этим автоматическим инструментом.
Спасибо за комментарий! Во второй части все исправлю, улучшу.
Playa
26.09.2024 08:19+1лучше было бы устанавливать публичные заголовочные файлы через PUBLIC_HEADER свойство таргета и одноимённый параметр команды install().
https://cmake.org/cmake/help/latest/command/target_sources.html#file-sets
haqreu
26.09.2024 08:19Стартовый гайд симпатичный, но добавлю чуть-чуть про сборку. На мой вкус, не хватает двух вещей: внешний пример того, как другие могут включать вашу библиотеку, и (важно) механизма включения ваших собственных зависимостей.
Ну и лично мне не нравятся кастомные шелл-скрипты, они и от ОС зависят, да и вообще нестандартно, вряд ли мне захочется разбираться с чужим build.sh
DrArgentum Автор
26.09.2024 08:19Спасибо! Напишу об этом во второй части. Под механизмом включения зависимостей имеется Dependency Injector?
haqreu
26.09.2024 08:19Ой, я таких слов не знаю. Я про то, что если ваша библиотека зависит от другой, как вы её включите? git submodule? cmake fetch? просто поставите шелл скриптом?
Кстати, ещё не хватает юнит тестов в проекте :)
Какой-нибудь catch2 отлично работает в связке с cmake
haqreu
26.09.2024 08:19А, да, ещё удобно в тех же самых github actions, которые у вас есть, делать CI, нехай компилит и гоняет тесты само на всех платформах!
DrArgentum Автор
26.09.2024 08:19Я делал, но потом отключил, винда не хотела работать. Попробую снова. Спасибо!
DrArgentum Автор
26.09.2024 08:19Вы предсказали будущее! Статью я уже написал, но я использовал cmake init для генерации cmake части, и как раз там используется catch2 для тестов)
XViivi
26.09.2024 08:19От человека, только учащегося создавать проекты — спасибо за статью. :)
Про doxygen — узнал впервые, и хорошо, что на раннем этапе). И за это отдельное спасибо.
DrArgentum Автор
26.09.2024 08:19Всегда пожалуйста! Кстати, вторая часть выйдет на следующей неделе. Решил просто сделать большую статью, которую я сам хотел увидеть, со всеми нужными материалами. Иногда проблема не в изучении, а в том что ты даже не знаешь что инструмент существует.
Спасибо!
Tujh
Для CMake существует стандартное расширение для работы с Doxygen
https://cmake.org/cmake/help/latest/module/FindDoxygen.html
и всё, после этого можно вызывать
после чего будет создана папка docs со всей сгенерированной документацией.
Tujh
Если хочется использовать собственные стили, то вот пример как это делается
https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/blob/main/examples/ch13/02-doxygen-nice/cmake/Doxygen.cmake
DrArgentum Автор
Спасибо большое! Во второй части напишу об этом