Featured image of post Come strutturare un progetto in Cpp

Come strutturare un progetto in Cpp

Abstract

Sono sicuro che ogni programmatore C++ ha, ad un certo punto, dovuto scontrarsi con CMake. Mi è capitato spesso, nei primi progetti, dilungarmi per troppo tempo all’organizzazione della struttura del progetto. È una gran seccatura dover scrivere e tenere seotto controllo ogni file CMakeList.txt sparso nelle directories, e non scordiamoci di tutte le librerie e moduli che possono creare errori nel processo di compilazione se male impostati.

Dopo aver analizzato varie pratiche personali di altri sviluppatori, sono finalmente riuscito a trovare una struttura che mi compiace, per semplicità e chiarezza.

Ci tengo a precisare che Le informazioni che seguono sono di parte. La è concepita per:

  • Evitare schemi che causano conflitti.
  • Evitare di complicare la compilazione.
  • Semplificare la lettura.

Cosa ci serve?

Per strutturare il progetto abbiamo necessità di tre tools molto importanti.

CMake

CMake è un software libero multipiattaforma per l’automazione dello sviluppo il cui nome è un’abbreviazione di cross platform make. Questo software nasce per rimpiazzare Automake nella generazione dei Makefile, cercando di essere più semplice da usare. Infatti, nella maggior parte dei progetti, non esiste un Makefile incluso nei sorgenti, dato che questo non è portabile.

– Wikipedia

Ci servirà per la generazione degli script, della build e la compilazione del progetto.

Non fornirò una guida completa su CMake, perchè l’articolo uscirebbe troppo complesso. Tuttavia se siete interessati allo strumento cliccando qui verrete indirizzati al sito di CMake.

Conan

Conan is a dependency and package manager for C and C++ languages. It is free and open-source, works in all platforms ( Windows, Linux, OSX, FreeBSD, Solaris, etc.), and can be used to develop for all targets including embedded, mobile (iOS, Android), and bare metal. It also integrates with all build systems like CMake, Visual Studio (MSBuild), Makefiles, SCons, etc., including proprietary ones.

– Conan

Ci servirà per la gestione delle dipendenze e dei pacchetti.

Se vi interessa lo strumento cliccando qui verrete indirizzati al sito di Conan.

Prima di procedere vorrei illustrarvi i passi fondamentali per utilizzare Conan.

Si inizia generando un profilo Conan, il quale consente di definire un insieme di configurazioni per elementi quali il compilatore, la configurazione di compilazione, l’architettura, ecc. Lo facciamo con il comando successivo.

1
conan profile detect --force

Al termine dell’esecuzione, se siete su Unix, troverete nella home directory una cartella .conan2 che conterrà i files sopracitati.

Per installare le dipendenze ustiamo la seguente istruzione.

1
conan install . --output-folder=build --build=missing

Conan quindi esegue due operazioni fondamentali:

  • Installa le librerie specificate nel conanfile.txt dal server remoto, che dovrebbe essere il server Conan Center, se disponibili. Questo server memorizza sia le Conan recipies, che definiscono come devono essere costruite le librerie, sia i binaries che possono essere riutilizzati in modo da non doverli ogni volta ricompilarli.
  • Genera diversi files nella directory build.
    • CMakeDeps genera i file necassari per far si che CMake trovi le librerie che abbiamo scaricato.
    • CMakeToolchain genera un file toolchain per CMake per poter costruire il nostro progetto con CMake.

Doxygen

Doxygen è una applicazione per la generazione automatica della documentazione a partire dal codice sorgente di un generico software. È un progetto open source disponibile sotto licenza GPL, scritto per la maggior parte da Dimitri van Heesch a partire dal 1997.

– Wikipedia

Per la generazione della documentazione del codice. Documentate sempre mi raccomando!

Come per gli strumenti precedenti non fornirò una guida completa su Doxygen, cliccando qui verrete indirizzati al sito di Doxygen.

Struttura

La struttura del progetto ha la seguente forma.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.
├── CMakeLists.txt
├── conanfile.txt
├── conan_provider.cmake
├── libfoo
│   ├── CMakeLists.txt
│   ├── docs
│   │   └── CMakeLists.txt
│   ├── include
│   │   └── libfoo
│   │       └── foo.hpp
│   ├── src
│   │   └── foo.cpp
│   └── tests
│       ├── foo.test.cpp
│       └── main.cpp
└── standalone
    ├── CMakeLists.txt
    └── main.cpp

Cerchiamo però di esporla meglio.

L’idea di fondo è quella di separare per directory i vari componenti del progetto. Ogni directory contiene un eseguibile o una libreria. In questo modo andremo a separare o meglio effettuare una compartimentazione dei vari target.

Standalone, un target eseguibile. Nel caso in questione utilizza la libreria libfoo. Esso è concepito con l’idea di essere un applicazione che utilizza una o più librerie “core” (che gli permettono di funzionare).

Libfoo in questo caso è una libreria “core” per standalone, all’occhio una semplice libreria statica, ma più attentamente si vede come sia stata costrutita come un progetto a se stante. Ogni libreria dovrà:

  • Seguire lo standard canonico della struttura di una libreria standard.
    • Directory include per le dichiarazioni pubbliche, interfaccia della libreria.
    • Directory src per le definizioni e per headers privati.
  • Garantire la generazione della documentazione con Doxygen.
  • Fornire un ambiente di testing con doctest (o similari).

Per comprendere meglio utilizziamo l’immagine sottostante come esempio. Si noti come gli eseguibili “app” utilizzino delle librerie “core”.

TopLevel CMakeLists.txt

Il file CMakeLists.txt nella root contiene la configurazione toplevel del nostro progetto di cui il contenuto è il seguente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
cmake_minimum_required(VERSION 3.27)

### Project
project(cpp_project_structure VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)


### Packages
find_package(fmt REQUIRED)
find_package(doctest REQUIRED)
find_package(Doxygen REQUIRED)


### Subdirectories (the order is important)
add_subdirectory(libfoo)
add_subdirectory(standalone)

Abbiamo un solo progetto e specifichiamo a CMake di ricercare altri file di configurazione nelle sottocartelle libfoo e standalone, rispettivamente “core” e “app”. Specifichiamo poi delle semplici variabili, ed impartiamo dei comandi find_package per ricercare le dipendenze necessarie al processo di compilazione.

Gestione dipendenze

Il file conanfile.txt specifica i pacchetti che serviranno ai vari target del nostro progetto. Nel nostro caso fmt viene usato sia da libfoo che da standalone, doctest viene richiesto da libfoo per gli unit tests. Il contenuto di seguito.

1
2
3
4
5
6
[requires]
fmt/10.2.1
doctest/2.4.11

[layout]
cmake_layout

Per usufruire meglio di conan utilizziamo il wrapper cmake-conan, in particolare siamo interessati al file conan_provider.cmake che salviamo nella root del progetto. Questo file ci tornerà utile in seguito.

Target libfoo

Il target libfoo che nel caso in analisi si presenta come una libreria statica, segue la forma canonica di una libreria standard. Ritornando al concetto di un target per subdirectory il nostro CMakeLists.txt è il sottostante.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
### Library libfoo
add_library(libfoo STATIC src/foo.cpp
        include/libfoo/foo.hpp)
add_library(libfoo::libfoo ALIAS libfoo)
set_target_properties(libfoo PROPERTIES VERSION 0.0)
target_include_directories(libfoo PUBLIC include PRIVATE src)
target_link_libraries(libfoo PRIVATE fmt::fmt)
target_compile_options(libfoo PRIVATE -Wall -Wextra -pedantic -Werror)
target_compile_features(libfoo PRIVATE cxx_std_17)


### Testing libfoo
add_executable(libfoo_tests tests/main.cpp)
target_link_libraries(libfoo_tests PRIVATE doctest::doctest)
target_compile_options(libfoo_tests PRIVATE -Wall -Wextra -pedantic -Werror)
target_compile_features(libfoo_tests PRIVATE cxx_std_17)


### Subdirectories
add_subdirectory(docs)

Abbiamo tre sezioni:

  • Definizione della libreria e della sua configurazione.
  • Impostazione e creazione del target per gli unit tests.
  • Abilitazione e configurazione di Doxygen per la generazione della documentazione.

Per chiarezza riporto il conenuto del CMakeLists.txt nella cartella docs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
set(DOXYGEN_ALPHABETICAL_INDEX NO)
set(DOXYGEN_BUILTIN_STL_SUPPORT YES)
set(DOXYGEN_CASE_SENSE_NAMES NO)
set(DOXYGEN_CLASS_DIAGRAMS NO)
set(DOXYGEN_DISTRIBUTE_GROUP_DOC YES)
# set(DOXYGEN_EXAMPLE_PATH "")
set(DOXYGEN_EXCLUDE bin)
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_EXTRACT_LOCAL_CLASSES NO)
set(DOXYGEN_FILE_PATTERNS *.hpp)
set(DOXYGEN_GENERATE_TREEVIEW YES)
set(DOXYGEN_HIDE_FRIEND_COMPOUNDS YES)
set(DOXYGEN_HIDE_IN_BODY_DOCS YES)
set(DOXYGEN_HIDE_UNDOC_CLASSES YES)
set(DOXYGEN_HIDE_UNDOC_MEMBERS YES)
set(DOXYGEN_JAVADOC_AUTOBRIEF YES)
set(DOXYGEN_QT_AUTOBRIEF YES)
set(DOXYGEN_QUIET YES)
set(DOXYGEN_RECURSIVE YES)
set(DOXYGEN_REFERENCED_BY_RELATION YES)
set(DOXYGEN_REFERENCES_RELATION YES)
set(DOXYGEN_SORT_BY_SCOPE_NAME YES)
set(DOXYGEN_SORT_MEMBER_DOCS NO)
set(DOXYGEN_SOURCE_BROWSER YES)
set(DOXYGEN_STRIP_CODE_COMMENTS NO)

doxygen_add_docs(
        libfoo_docs
        "../include/"
        ALL
        COMMENT "Generate HTML documentation for libfoo"
)

Target standalone

A differenza del target libfoo, questo risulta molto più banale di seguito la configurazione.

1
2
3
4
add_executable(standalone main.cpp)
target_link_libraries(standalone PRIVATE fmt::fmt libfoo::libfoo)
target_compile_options(standalone PRIVATE -Wall -Wextra -pedantic -Werror)
target_compile_features(standalone PRIVATE cxx_std_17)

Compilazione

1
2
cmake -B build -S . -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug

Con il primo comando eseguiamo la fase di configurazione out-of-tree, generando un sistema di compilazione. Con il secondo costruiamo il progetto chiamando lo strumento di compilazione di sistema, make su Unix.

Ricordate il file conan_provider.cmake enunciato in precedenza? Ebbene utilizzando l’impostazione -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake nel processo di configurazione di cmake, questo invoca automaticamente il comando conan install semplificandoci la gestione del progetto.

Conclusione

Ora nella cartella build sotto la root del progetto troveremo i file binari e le librerie che abbiamo compilato, oltre che alla documentazione doxygen e all’eseguibile degli unit tests.

Questa struttura può facilmente essere apliate aggiungendo il pipeline CI/CD e altro.

Spero di esservi stato utile, a questo link trovate la repository del progetto discusso in questo articolo in maniera che possiate analizzarlo.

Realizzato con Hugo
Tema Stack realizzato da Jimmy