Теоретические сведения по C++ для студентов курса “Программирование на основе классов и шабллонов” кафедры ИУ5 МГТУ им. Н.Э. Баумана.

Содержание

Разработка кроссплатформенных приложений

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

CMake облегчает работу, объединяя настройку, сборку, тестирование и упаковку в едином инструменте. Он генерирует файлы проекта для выбранной платформы и инструмента сборки, а встроенные CTest и CPack позволяют автоматизировать тестирование и создание пакетов. Все этапы управления сборкой можно выполнять напрямую через CMake.

Перед использованием следует установить актуальную версию CMake — многие платформы поставляют устаревшие версии через менеджеры пакетов.

1. Что такое CMake

CMake — это генератор систем сборки, а не компилятор.

CMakeLists.txt
   ↓ (configure)
CMake
   ↓ (generate)
Makefiles / Ninja / Visual Studio / Xcode
   ↓
Компилятор (gcc / clang / MSVC)

CMake позволяет описать проект один раз и собирать его на разных платформах.

2. In-source и Out-of-source сборка

cmake .
make

bad

cmake -S . -B build
cmake --build build

ok

Преимущества out-of-source:

3. Язык CMake. Синтаксис

command(ARG1 ARG2 ...)

Особенности:

4. Переменные

set(MY_VAR "hello")
message(STATUS "${MY_VAR}")

Проверка:

if(DEFINED MY_VAR)
endif()

5. Списки

set(SOURCES a.cpp b.cpp c.cpp)
add_executable(app ${SOURCES})

6. Targets — основа CMake

Target — это логическая единица сборки (цель):

add_executable(app main.cpp)
add_library(mylib src.cpp)

7. Области видимости

target_include_directories(mylib
    PUBLIC include
    PRIVATE src
)
Ключ Назначение
PRIVATE только цель
PUBLIC цель + потребители
INTERFACE только потребители

8. Стандарт C++

set(CMAKE_CXX_STANDARD 20)

bad

target_compile_features(app PRIVATE cxx_std_20)

ok

9. Библиотеки

add_library(static_lib STATIC src.cpp)
add_library(shared_lib SHARED src.cpp)
add_library(header_only INTERFACE)

10. Линковка

target_link_libraries(app PRIVATE mylib)

CMake автоматически передаёт include-директории, флаги и определения.

11. Структура проекта

project/
├── CMakeLists.txt
├── src/
│   ├── CMakeLists.txt
│   └── main.cpp
├── include/
├── tests/
│   └── CMakeLists.txt

Корневой CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

add_subdirectory(src)
add_subdirectory(tests)

12. Внешние зависимости FetchContent

include(FetchContent)

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.2.1
)

FetchContent_MakeAvailable(fmt)

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

target_link_libraries(app PRIVATE fmt::fmt)

13. Тестирование CTest

enable_testing()
add_executable(test1 test.cpp)
add_test(NAME unit COMMAND test1)

Запуск:

ctest --test-dir build

14. Пользовательские опции

option(ENABLE_WARNINGS "Enable warnings" ON)

if(ENABLE_WARNINGS)
    target_compile_options(app PRIVATE -Wall -Wextra)
endif()

15. install и find_package

install(TARGETS mylib EXPORT MyLibTargets)
install(DIRECTORY include/ DESTINATION include)

16. CMakePresets.json

{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ]
}

17. Минимальный шаблон

cmake_minimum_required(VERSION 3.20)
project(App LANGUAGES CXX)

add_executable(app src/main.cpp)

target_compile_features(app PRIVATE cxx_std_23)
target_compile_options(app PRIVATE -Wall -Wextra)

18. Четыре примера

От простого к сложному

example1

В проекте есть два файла main.cpp:

#include <iostream>

int main(int, char**){

   std::cout << "hello, this is first CMake example!\n";
   return 0;
}

и CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(example1)

add_executable(hello main.cpp)

set_target_properties(
    hello PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED ON
)

для запуска CMake, обычно, создают дополнительную директорию (build или temp или с другим именем, куда CMake будет генерировать файлы системы сборки (например, Makefile или файлы для Ninja), которые содержат информацию про ОС, компиляторы, библиотеки на устройстве с которого был запущенн CMake, иначе они будут лежать в корне вашего проекта).
Для запуска cmake из этой директории необходимо перейти в директорию “выше” (..) и в директории еxample1 обратиться к файлу CMakeLists.txt

mkdir build
cd build
cmake ../еxample1/

После того как CMake сгенерирует файлы-системы сборки, можно запустить сборку вашего проекта в соответствии с target-целью поставленной перед CMake командой:

cmake --build .

в результате появиться исполняемый файл hello (в директории build) в соответствии с add_executable(hello main.cpp).

example2

много-файловый проект:

//main.cpp
#include "version.h"

#include <iostream>

int main(int, char**){
   std::cout << "hello from the second Cmake example! \n";
   std::cout << "Version = " << examples::getVersion() <<  '\n';
   return 0;
}

// version.h
#pragma once

namespace examples{

    int getVersion();

}

//version.cpp
#include "version.h"

int examples::getVersion(){
    return 0;
}

и CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(example2)

#add_executable(version main.cpp version.cpp version.h)
#add_executable(version main.cpp version.cpp)

set(SOURCES
        main.cpp
        version.cpp
 )
 set(HEADERS
        version.h
)

add_executable(version ${SOURCES} ${HEADERS})

set_target_properties(
    version PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED ON
)

example3

также много-файловый проект, плюс генерация файла через configure_file

//main.cpp
#include "version.h"

#include <iostream>

int main(int, char**){
   std::cout << "hello from the third Cmake example! \n";
   std::cout << "Version = " << examples::getVersion() <<  '\n';
   return 0;
}
//__________
// version.h
namespace examples{
    int getVersion();
}
//__________
//version.cpp
#include "version.h"
#include "config.h"

namespace examples {
    int getVersion(){
        return (PROJECT_VERSION_PATCH);
    }
}
//__________
//config.h.in
#pragma once

#cmakedefine PROJECT_VERSION @PROJECT_VERSION@

config.h.in - Это шаблон файла, который будет обработан CMake.

если переменная определена — будет создан define - #cmakedefine PROJECT_VERSION @PROJECT_VERSION@

но есть ньюанс #cmakedefine PROJECT_VERSION @PROJECT_VERSION_PATCH@ возвращает "0.0.1" , а это строка и поэтому программа выведет Version = 1, можно получить каждую цифру #cmakedefine PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@, #cmakedefine PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@,#cmakedefine PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@, можно придумать и по другому - :)

После обработки configure_file() CMake создаст файл:

build/config.h

Пример того, что может получиться:

#define PROJECT_VERSION 1

и CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(example3 VERSION 0.0.1)

configure_file(
    config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

set(SOURCES
        main.cpp
        version.cpp
 )
 set(HEADERS
        version.h
)

add_executable(configure ${SOURCES} ${HEADERS})

set_target_properties(
    configure PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED ON
)

target_include_directories(
    configure
    PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}
)

Этот пример показывает три важные вещи CMake:

1 - Переменные проекта

project(example3 VERSION 0.0.1)

создаёт переменные:

2️ - Генерацию файлов

configure_file(config.h.in config.h)

CMake подставляет значения переменных в шаблон.

3️ - Работа с include директориями

target_include_directories(...)

нужна потому что config.h лежит в build-директории, а не в исходниках.

example4

использование структуры много-модульного проекта и создание статической библиотеки

Структура проекта

example4
│
├── CMakeLists.txt
│
├── lib
│   ├── CMakeLists.txt
│   ├── version.cpp
│   └── version.h
│
└── src
    ├── CMakeLists.txt
    └── main.cpp

главный CMakeLists.txt объединяет всё вместе.

Итак, общий CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(example4 VERSION 0.0.2)

add_subdirectory(lib)
add_subdirectory(src)

add_subdirectory(lib) говорит CMake: перейти в папку lib и выполни файл lib/CMakeLists.txt, добавь созданные targets (например библиотеку) в общий проект.
То же самое происходит с src.

Создание библиотеки

//lib.cpp
#include "lib.h"
#include "config.h"

#include <iostream>

namespace lib {
    int makeSameSuperJob(){
        std::cout << "Hello from lib\n";
        return 42;
    }
    int getVersion(){
        return (PROJECT_VERSION);
    }
}
//__________
// lib.h
#pragma once
namespace lib {
    int makeSameSuperJob();
    int getVersion();
}
//__________
//config.h.in
#pragma once

#cmakedefine PROJECT_VERSION @PROJECT_VERSION@
cmake_minimum_required(VERSION 3.5)

project(library VERSION 0.0.2)

configure_file(
    config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

add_library(lib STATIC lib.cpp lib.h)

set_target_properties(
    lib PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED
)

target_include_directories(
    lib
    PUBLIC
    ${CMAKE_CURRENT_BINARY_DIR}
)

target_include_directories(lib PUBLIC ...) говорит компилятору, где искать заголовки. PUBLIC означает: библиотека сама использует этот include и передаёт его всем target’ам, которые будут линковаться с lib.

создание приложения, использующее написанную библиотеку

//main.cpp
#include "config.h"
#include "lib.h"

#include <iostream>

int main(int, char**)
{
   std::cout << "hello from main! \n";
   lib::makeSameSupperJob();
   std::cout << "Lib version: " << lib::getVersion() <<  '\n';
   return 0;

}
//__________
//config.h.in
#pragma once

#cmakedefine PROJECT_VERSION @PROJECT_VERSION@
cmake_minimum_required(VERSION 3.5)

project(Main VERSION 0.0.5)

configure_file(
    config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

add_executable(mainLib main.cpp)

set_target_properties(
    mainLib PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED ON
)

target_include_directories(
    mainLib
    PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}
)

target_include_directories(
    mainLib
    PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/../lib
)

target_link_libraries(
    mainLib lib
)

Рекомендации по изучению

  1. Всегда думать в терминах targets
  2. Использовать CMake
  3. Читать чужие проекты
  4. Использовать CMakePresets
  5. Не копировать устаревшие примеры