1. Introduction au C++



sommaire chapitre 1

1.10. Compilation, édition de liens, makefile

processus de compilation

1.10.1. Compilation

Le processus de compilation consiste à transformer un fichier source écrit dans un langage A en un fichier cible écrit dans un langage B.

Dans le cas présent, on transforme un fichier C ou C++ en un fichier objet (object file) d'extension .o

gcc -c source.c  
ou
g++ -c source.cpp  

Des options peuvent être ajoutées à cette commande de base :

Voir également man gcc pour plus d'options.

1.10.2. Edition de liens (link)

Le processus d'édition de liens consiste à transformer un ensemble de fichiers objets en un exécutable (binaire).

gcc -o monexe  a.o b.o ...
ou
g++ -o monexe *.o  

On peut également réaliser la compilation suivie de l'édition de liens si les fichiers sources sont les fichiers C ou C++ :

gcc -o monexe  a.cpp b.cpp ...
ou
g++ -o monexe *.cpp  

1.10.3. Problèmatique

Le problème qui se pose consiste, si on modifie un fichier, à ne recompiler que ce qui est nécessaire.

Par exemple, si on modifie le fichier file2.c alors on ne doit recompiler file2.c, puis réaliser l'édition de liens.

1.10.4. Automatisation du processus de compilation

Sur l'image suivante on voit un comparatif schématisé de l'automatisation de la compilation avec trois outils différents.

comparaison autotools make cmake
Extrait des transparents de Rodolfo Lima

1.10.5. Make

Le processus de compilation et d'édition de liens peut être automatisé grâce à l'utilitaire make. Il suffit pour cela de créer un fichier nommé makefile ou Makefile dans le répertoire ou on désire compiler. Il est préférable d'organiser les répertoires ainsi:

 Répertoire   Description 
 src   l'ensemble de fichiers sources (.h .cpp) 
 obj   l'ensemble des fichiers objets (.o) 
 lib   l'ensemble des librairies utilisateur (.a ou .so) 
 bin   l'ensemble des exécutables / binaires (.exe) 
 data   l'ensemble des fichiers de données utilisés par les exécutables 
Organisation des répertoires d'un projet

ou alors de créer un répertoire build qui regroupera bin, lib, obj.

Le makefile comporte des règles qui indiquent quelles actions déclencher pour obtenir une cible (fichier ou autre cible) en fonction d'une liste de de cibles existantes ou de fichiers sources :

cible : sources
<tabulation>actions

Dans la partie actions on mettra des commandes shells, on peut également laisser cette partie vide.

Voici des raccourcis de variables prédéfinies liées aux règles que l'on peut utiliser dans la partie actions :

 Variable   Role 
 $@   cible 
 $%   nom de la cible 
 $<   première source 
 $?   toutes les sources plus récentes que la cible 
 $^   toutes les sources 
variables d'une règle

Par exemple pour obtenir l'exécutable main.exe en fonction des fichiers objets a.o, b.o, c.o :

main.exe : a.o b.o c.o
	g++ -O2 -o \$@ \$^

# or
	
main.exe : a.o b.o c.o
	g++ -O2 -o main.exe a.o b.o c.o

On peut également définir des règles génériques comme :

obj/%.o : src/%.cpp
	g++ -c -O2 -o \$@ \$<

qui se lit : pour obtenir tout fichier d'extension .o dans le répertoire obj, il faut qu'il existe un fichier d'extension .cpp de même nom dans le répertoire src, pour cela on compilera le fichier source en fichier objet (paramètre -c de g++).

Les règles génériques permettent de simplifier l'écriture du makefile. Sans elles on serait contraint d'écrire :

obj/a.o : src/a.cpp
	g++ -c -O2 -o \$@ \$<

obj/b.o : src/b.cpp
	g++ -c -O2 -o \$@ \$<

obj/c.o : src/c.cpp
	g++ -c -O2 -o \$@ \$<

Attnetion : lorsque l'on tape make dans le terminal pour lancer l'exécution des règles, le programme make exécutera la première règle qu'il trouve dans le fichier makefile. Il est préférable de faire en sorte que la règle all soit la première rencontrée. Sinon il faudra explicitement saisir dans le terminal la commande make all

  1. CFLAGS=-Wall
  2. OFLAGS=-O2
  3.  
  4. all: bin/monexe.exe
  5.  
  6. SRCS=$(ls src/*.cpp)
  7. OBJS=$(subst src/,obj/,$(subst .cpp,.o,$(SRCS)))
  8.  
  9. info:
  10.     @echo "==================" \
  11.     echo "   COMPILATION    " \
  12.     echo "=================="
  13.    
  14. bin/monexe.exe: info $(OBJS)
  15.     g++ -o $@ $(OBJS) $(OFLAGS)
  16.  
  17. obj/%.o : src/%.cpp
  18.     g++ -c $< $(CFLAGS) $(OFLAGS)
  19.  
  20. clean:
  21.     rm bin/*.exe obj/*.o
  22.  
  23.  
  1. CC=g++
  2.  
  3. ifeq "$(distrib)" ""
  4.     distrib=debug
  5. endif
  6.  
  7. ifeq "$(distrib)" "debug"
  8. CFLAGS=-Wall -DDEBUG -fmax-errors=2 -std=c++11
  9. OFLAGS=-ggdb -fno-inline
  10. endif
  11.  
  12. ifeq "$(distrib)" "release"
  13. CFLAGS=-Wall -std=c++11
  14. OFLAGS=-O2 -msse4.2 -funroll-loops --param max-unroll-times=8
  15. endif
  16.  
  17. ifeq "$(distrib)" "profile"
  18. CFLAGS=-Wall
  19. OFLAGS=-pg -msse4.2 -funroll-loops --param max-unroll-times=8
  20. endif
  21.  
  22.  
  23. SRCS=$(shell ls src/*.cpp)
  24. OBJS=$(subst .cpp,.o,$(subst src/,obj/,$(SRCS)))
  25. EXE=bin/main_$(distrib).exe
  26.  
  27. all: $(EXE)
  28.  
  29. $(EXE): $(OBJS)
  30.     $(CC) -o $@ $(OBJS) $(OFLAGS)
  31.  
  32. obj/%.o: src/%.cpp
  33.     $(CC) -c -o $@ $< $(CFLAGS) $(OFLAGS)
  34.  
  35. clean:
  36.     rm obj/*.o bin/*.exe
  37.  
  38. release:
  39.     make --no-print-directory distrib=release
  40.  
  41. profile:
  42.     make --no-print-directory distrib=profile
  43.     bin/main_profile.exe
  44.     gprof bin/main_profile.exe >profile.txt
  45.  

Voici un exemple plus complexe qui utilise le répertoire build pour stocker fichiers objets, librairies, exécutables et tests unitaires. On prend en compte :

Voici l'organisation des répertoires du projet :


.
├── include
│   └── v1_0
│       ├── base
│       │   ├── base.h
│       │   ├── command_line.h
│       │   ├── exception.h
│       │   ├── strings.h
│       │   ├── terminal.h
│       │   └── types.h
│       └── maths
│           └── statistics.h
├── makefile
├── makefile.distrib
├── makefile.libraries
├── makefile.modules
├── src
│   └── v1_0
│       ├── base
│       │   ├── base.h
│       │   ├── command_line.cpp
│       │   ├── command_line.h
│       │   ├── exception.cpp
│       │   ├── exception.h
│       │   ├── makefile.lib
│       │   ├── strings.cpp
│       │   ├── strings.h
│       │   ├── terminal.cpp
│       │   ├── terminal.h
│       │   └── types.h
│       ├── maths
│       │   ├── makefile.lib
│       │   ├── statistics.cpp
│       │   └── statistics.h
│       └── prog
│           ├── makefile.bin
│           ├── prog1.cpp
│           └── prog2.cpp
└── tst
    └── v1_0
        ├── base
        │   ├── makefile.tst
        │   ├── test_strings.cpp
        │   └── test_strings.h
        └── maths

On utilise plusieurs makefiles :

Dans les sous-répertoires on trouve :

1.10.6. CMake

CMake (abréviation de cross platform make) est qualifié de moteur de production multiplate-forme contrôlé par des fichiers de configuration (appelés CMakeLists.txt) ce qui lui permet de produire des makefile sous Unix ou des fichiers de projet Visual Studio sous Window par exemple.

cmake possède son propre langage de programmation basé sur des variables prédéfinies, des conventions de nommage et des règles implicites.

Sous Ubuntu 14.04, installer le package cmake. On pourra également installer le package cmake-qt-gui qui est une interface graphique mais qui n'est pas très intuitive.

Dans la partie qui suit on s'intéresse à la génération d'un makefile.

1.10.6.a  Exemple simple avec un fichier

On désire à partir d'un fichier situé dans le répertoire courant main.cpp générer l'exécutable main.exe.

La première étape consiste à écrire un fichier CMakeLists.txt qui décrit les étapes à réaliser pour aller des sources à l'exécutable.

  1.  
  2. project(MyProject)
  3.  
  4. # will generate the binary ./main.exe
  5.     # binary
  6.     main.exe
  7.     # fichiers sources
  8.     main.cpp
  9. )
  10.  
  11.  

Pour obtenir un makefile, exécuter la commande suivante :

\$ cmake -G "Unix Makefiles"

-- The C compiler identification is Clang 3.6.0
-- The CXX compiler identification is Clang 3.6.0
-- Check for working C compiler: /usr/bin/clang-3.6
-- Check for working C compiler: /usr/bin/clang-3.6 -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/clang++-3.6
-- Check for working CXX compiler: /usr/bin/clang++-3.6 -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: [...]/cmake1

Durant l'exécution de la commande, plusieurs fichiers sont générés :

Pour obtenir l'exécutable, il suffit de taper :

\$make

Scanning dependencies of target main.exe
[100%] Building CXX object CMakeFiles/main.exe.dir/main.cpp.o
Linking CXX executable main.exe
[100%] Built target main.exe

On rencontre cependant deux problèmes:

Pour répondre au premier problème, il suffit d'ajouter les lignes suivantes au fichier CMakeLists.txt :

set(
	CMAKE_C_COMPILER 
	"/usr/bin/gcc"
)
set(
	CMAKE_CXX_COMPILER 
	"/usr/bin/g++"
)

La commande set permet d'attribuer une valeur à une variable en l'occurrence les variables prédéfinies CMAKE_C_COMPILER et CMAKE_CXX_COMPILER.

Il faut relancer cmake, mais parfois cela n'est pas suffisant, il faut supprimer le fichier CMakeCache.txt et le sous-répertoire CMakeFiles (a priori il n'existe pas de moyen simple de supprimer ces fichiers si ce n'est un rm):

\$ rm -rf CMakeFiles CMakeCache.txt
\$ cmake -G "Unix Makefiles"

-- The C compiler identification is GNU 4.9.2
-- The CXX compiler identification is GNU 4.9.2
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: [...]/cmake1_1

Pour répondre au second problème, il suffit de lancer make avec l'option VERBOSE=1 :

make VERBOSE=1
...
[100%] Building CXX object CMakeFiles/main.exe.dir/main.cpp.o
/usr/bin/g++     -o CMakeFiles/main.exe.dir/main.cpp.o -c /home/richer/dev/cpp/cmake1_1/main.cpp
Linking CXX executable main.exe
/usr/bin/cmake -E cmake_link_script CMakeFiles/main.exe.dir/link.txt --verbose=1
/usr/bin/g++       CMakeFiles/main.exe.dir/main.cpp.o  -o main.exe -rdynamic 
...

On s'aperçoit qu'aucune option de compilation (optimisation ou débugage) n'est utilisée.

Il faut choisir un "type de build" (build type en anglais) que l'on peut traduire en français par le mot distribution, on modifie CMakeLists.txt en conséquence :

set(
	CMAKE_BUILD_TYPE 
	Release
)

On a le choix entre :

Par exemple pour définir sa propre distribution :

project(MyProject)

set(
	CMAKE_BUILD_TYPE 
	ma_distribution
)
set(
	CMAKE_CXX_FLAGS_MA_DISTRIBUTION 
	"-O3 -ftree-vectorize -msse2"
)
set(
	CMAKE_C_FLAGS_MA_DISTRIBUTION 
	"-O3 -ftree-vectorize -msse2"
)

Attention les trois dernières instructions doivent être placées après la directive project, sinon elles ne seront pas prises en compte.

On peut également ajouter des flags à une distribution existante :

project(MyProject)

set(
	CMAKE_BUILD_TYPE 
	release
)
set(
	CMAKE_CXX_FLAGS_RELEASE 
	"\${CMAKE_CXX_FLAGS_RELEASE} -Wall -ftree-vectorize -msse2 -std=c++11"
)

On compilera alors avec -O3 -DNDEBUG et -Wall -ftree-vectorize -msse2 -std=c++11.

1.10.6.b  Séparation src et bin

Généralement on sépare les fichiers sources des exécutables en plaçant les premiers dans un sous-répertoire src et les seconds dans un répertoire bin ou build.

  1.  
  2. # define compiler in order not to take clang
  3. # and clang++ as default compilers
  4. # do rm -rf CMakeFiles/* CMakeCache.txt
  5. # if it is not working
  6.     CMAKE_C_COMPILER
  7.     "/usr/bin/gcc"
  8. )
  9.     CMAKE_CXX_COMPILER
  10.     "/usr/bin/g++"
  11. )
  12.  
  13.  
  14. project(MyProject)
  15.  
  16.     CMAKE_BUILD_TYPE
  17.     release
  18. )
  19.     EXECUTABLE_OUTPUT_PATH
  20.     bin/${CMAKE_BUILD_TYPE}
  21. )
  22.  
  23.     GLOB_RECURSE
  24.     source_files
  25.     src/*.cpp
  26. )
  27.  
  28. # will generate the binary ./main.exe
  29.     main.exe
  30.     ${source_files}
  31. )
  32.  
  33.  

Attention, l'exécutable main.exe sera situé dans le sous-répertoire bin/Release car on a utilisé la directive set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}).

1.10.6.c  Création et utilisation d'une librairie utilisateur

On dispose de l'arborescence suivante pour les sources :

src
├── file1.cpp
├── file1.h
├── main.cpp
└── maths
    ├── statistics.cpp
    └── statistics.h

On veut faire en sorte de créer une librairie avec les fichiers du sous-répertoire maths puis obtenir l'exécutable à partir des fichiers file1.cpp et main.cpp en réalisant l'édition de liens avec la librairie produite.

  1.  
  2. # define compiler in order not to take clang
  3. # and clang++ as default compilers
  4. # do rm -rf CMakeFiles/* CMakeCache.txt
  5. # if it is not working
  6. set(CMAKE_C_COMPILER "/usr/bin/gcc")
  7. set(CMAKE_CXX_COMPILER "/usr/bin/g++")
  8.  
  9. project(MyProject)
  10.  
  11. set(CMAKE_BUILD_TYPE Release)
  12. set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE})
  13. set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -std=c++11")
  14.  
  15. # tell where to find the .h files
  16. include_directories(src src/maths)
  17.  
  18. # find .cpp files for the maths library
  19. file(GLOB_RECURSE source_files_maths src/maths/*.cpp)
  20. # tell to create a library
  21. add_library(maths SHARED ${source_files_maths})
  22.  
  23. file(GLOB_RECURSE source_files src/*.cpp)
  24.  
  25. # will generate the binary ./main.exe
  26. add_executable(main.exe ${source_files})
  27.  
  28. # link with library maths
  29. target_link_libraries(main.exe maths)
  30.  
  31.  

On ajoute les 4 lignes suivantes :

Au final on obtient une lirairie dans le répertoire principal du projet (libmaths.so) qui est liée à l'exécutable main.exe.

1.10.6.d  Création et utilisation d'une librairie externe

On désire réaliser l'édition de lien avec la librairie boost (system, filesystem). Pour cela il suffit d'utiliser find_package et d'inclure les librairies de target_link_libraries.

add_executable(
	main.exe 
	\${source_files}
)

find_package(
	Boost 
	COMPONENTS system filesystem 
	REQUIRED)

target_link_libraries(
	main.exe 
	maths 
	\${Boost_FILESYSTEM_LIBRARY_RELEASE} 
	\${Boost_SYSTEM_LIBRARY_RELEASE}
)
  1.  
  2. # define compiler in order not to take clang
  3. # and clang++ as default compilers
  4. # do rm -rf CMakeFiles/* CMakeCache.txt
  5. # if it is not working
  6. set(CMAKE_C_COMPILER "/usr/bin/gcc")
  7. set(CMAKE_CXX_COMPILER "/usr/bin/g++")
  8.  
  9. # ---------------------
  10. # the project
  11. # ---------------------
  12. project(MyProject)
  13.  
  14. # define build to be a release
  15.     CMAKE_BUILD_TYPE
  16.     Release
  17. )
  18.  
  19. # put binary in 'bin' directory
  20.     EXECUTABLE_OUTPUT_PATH
  21.     bin/${CMAKE_BUILD_TYPE}
  22. )
  23.  
  24. # add flags to be able to compile with C++11
  25.     CMAKE_CXX_FLAGS_RELEASE
  26.     "${CMAKE_CXX_FLAGS_RELEASE} -std=c++11"
  27. )
  28.    
  29.     src
  30.     src/maths
  31. )
  32.  
  33. # define files of user 'maths' library
  34.     GLOB_RECURSE
  35.     source_files_maths
  36.     src/${VERSION}/maths/*
  37. )
  38.    
  39.     maths
  40.     SHARED
  41.     ${source_files_maths}
  42. )
  43.  
  44. # define files of the binary
  45.     GLOB_RECURSE
  46.     source_files
  47.     src/*.cpp
  48. )
  49.  
  50. # tell to generate the binary ./main.exe from source files
  51.     main.exe
  52.     ${source_files}
  53. )
  54.  
  55. # find boost libraries
  56.     Boost
  57.     COMPONENTS system filesystem REQUIRED
  58. )
  59.  
  60. # tell to link with maths and boost libraries
  61.     main.exe
  62.     maths
  63.     ${Boost_FILESYSTEM_LIBRARY_RELEASE}
  64.     ${Boost_SYSTEM_LIBRARY_RELEASE}
  65. )
  66.  
  67.  

1.10.6.e  Génération des fichiers de test

On utilise des tests unitaires situés dans le répertoire tst du projet et on aimerait les générer. On utilise l'instruction foreach pour itérer sur les fichiers et générer des exécutables en incluant la librairie cppunit

  1. # -------------------------------------------------------------------
  2. # tests
  3. # -------------------------------------------------------------------
  4.  
  5. # tell in which directory to find 'FindCppUnit.cmake'
  6.     CMAKE_MODULE_PATH
  7.     "${CMAKE_SOURCE_DIR}/cmake/modules"
  8. )
  9.  
  10. # tell to find the package to later integrate it
  11.     CppUnit
  12.     REQUIRED
  13. )
  14.  
  15. # tell where to find include directories
  16. # - user defined
  17. # - and cppunit includes
  18.     src/${VERSION}
  19.     ${CPPUNIT_INCLUDE_DIR}
  20. )
  21.  
  22. # add CppUnit
  23.     APPEND
  24.     UnitTestLibs
  25.     ${CPPUNIT_LIBRARY}
  26. )
  27.  
  28. #Include the "unit-tests" directory
  29.  
  30. #Find all source files in unit test
  31.     GLOB_RECURSE
  32.     UNIT_TEST
  33.     "tst/*.cpp"
  34. )
  35.  
  36. # for each source file tell to produce some binary
  37. # and link with cppunit
  38. foreach(a_file ${UNIT_TEST})
  39.         a_target
  40.         ${a_file}
  41.         NAME_WE
  42.     )
  43.     project(${a_target})
  44.     set(
  45.         CMAKE_BUILD_TYPE
  46.         release
  47.     )
  48.     set(
  49.         CMAKE_CXX_FLAGS_RELEASE
  50.         "${CMAKE_CXX_FLAGS_RELEASE} -Wall -ftree-vectorize -msse2 -std=c++11"
  51.     )
  52.         ${a_target}
  53.         ${a_file}
  54.     )
  55.         ${a_target}
  56.         ${UnitTestLibs}
  57.         base
  58.         maths
  59.     )
  60. endforeach(a_file ${UNIT_TEST})
  61.  
  62.  

Il faudra ajouter dans le répertoire cmake/modules -- car on a utilisé set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") -- le fichier FindCppUnit.cmake qui permet de détecter la librairie CppUnit et de fixer des variables en conséquence.

  1. #
  2. # http://root.cern.ch/viewvc/trunk/cint/reflex/cmake/modules/FindCppUnit.cmake
  3. #
  4. # - Find CppUnit
  5. # This module finds an installed CppUnit package.
  6. #
  7. # It sets the following variables:
  8. #  CPPUNIT_FOUND       - Set to false, or undefined, if CppUnit isn't found.
  9. #  CPPUNIT_INCLUDE_DIR - The CppUnit include directory.
  10. #  CPPUNIT_LIBRARY     - The CppUnit library to link against.
  11.  
  12. FIND_PATH(CPPUNIT_INCLUDE_DIR cppunit/Test.h)
  13. FIND_LIBRARY(CPPUNIT_LIBRARY NAMES cppunit)
  14.  
  15. IF (CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARY)
  16.    SET(CPPUNIT_FOUND TRUE)
  17. ENDIF (CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARY)
  18.  
  19. IF (CPPUNIT_FOUND)
  20.  
  21.    # show which CppUnit was found only if not quiet
  22.    IF (NOT CppUnit_FIND_QUIETLY)
  23.       MESSAGE(STATUS "Found CppUnit: ${CPPUNIT_LIBRARY}")
  24.    ENDIF (NOT CppUnit_FIND_QUIETLY)
  25.  
  26. ELSE (CPPUNIT_FOUND)
  27.  
  28.    # fatal error if CppUnit is required but not found
  29.    IF (CppUnit_FIND_REQUIRED)
  30.       MESSAGE(FATAL_ERROR "Could not find CppUnit")
  31.    ENDIF (CppUnit_FIND_REQUIRED)
  32.  
  33. ENDIF (CPPUNIT_FOUND)
  34.              
  35.  

Au final si on compare cmake à make, on peut considérer que cmake est assez difficile d'utilisation pour au moins deux raisons : d'une part l'existence de variables prédéfinies qu'il faut connaître, d'autre part des commandes pas toujours évidentes pour réaliser ce que l'on souhaite faire.

En outre la portabilité des plateformes (Linux, Windows, OSX, ...) doit être gérée par l'utilisateur.

A titre d'exemple, voici le fichier CMakeLists.txt de 1450 lignes de la SDL (Simple DirectMedia Layer a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D).

1.10.7. Autres utilitaires

1.10.7.a  Autoconf / autotools

A suite of programming tools designed to assist in making source code packages portable to many Unix-like systems.

1.10.7.b  SCons

SCons is a software construction tool—that is, a superior alternative to the classic "Make" build tool that we all know and love.

SCons is implemented as a Python script and set of modules, and SCons "configuration files" are actually executed as Python scripts. This gives SCons many powerful capabilities not found in other software build tools.

1.10.7.c  Ant

L'équivalent de make pour Java qui utilise une syntaxe XML.