C++
Cette page fournit les bases pour bien démarrer en C++ (Cpp).
Installation
Comme tout langage, il faudra des outils. Le Cpp est un langage compilé, les outils principaux seront un compilateur et un IDE. Ce tutoriel se concentrera sur Windows, mais les partie xmake et WSL intéressera les utilisateurs de GNU/Linux.
Visual Studio
Microsoft écrit tous ses outils disponibles gratuitement sous Visual Studio Community (et non VSCode, ne pas confondre). C'est un outil assez gros mais très complet. Une fois installé, créez un nouveau projet C++ et essayer de le débugger !
Cmake
Cmake est le système de build le plus commun dans l'écosystème C/C++. Il est assez compliqué pour ce qu'il fait et l'auteur de cette article ne se sent pas d'écrire un tuto pour Cmake.
Xmake
Pour ceux qui préfèrent gérer leurs projet plus manuellement, vous pouvez utiliser xmake. C'est un outil très pratique sous tout système d'exploitation. Sachez tout de même que c'est un outil peu connu : il reste idéal pour les projets perso ou les cours n'imposant pas d'environnement particulier. Xmake s'occupera pour vous de la compilation et de la gestion des dépendances. Pour démarrer rapidement :
Installation
Pour windows, tapper dans un terminal administrateur :
winget install xmake
Les autres OS l'auront aussi dans leur manager de paquets.
Nouveau projet
Ouvrez une console où votre dossier de projet sera créé
xmake create my-project
cd my-project
xmake build
xmake run
Voilà ! C'était facile non ? Vous pouvez aussi faire git init
et mettre votre code sur gitlab.
Configuration
La configuration se fait par xmake.lua
. Lisez la documentation pour tout savoir, mais globalement voici comment utiliser des paquets de conan (vous pouvez aussi utiliser les paquets de vcpkg
et xmake vcpkg
):
add_rules("mode.debug", "mode.release")
set_languages("c++17")
set_warnings("allextra")
set_policy("build.warning", true)
set_policy("run.autobuild", true)
------------------------------------------------------------------
-- Lib deps
------------------------------------------------------------------
add_requires("conan::glm/0.9.9.8", {alias = "glm", })
add_requires("conan::eigen/3.4.0", {alias = "eigen", })
------------------------------------------------------------------
-- Main target definition
------------------------------------------------------------------
target("my-project")
set_kind("binary")
add_packages("glm", "eigen")
add_files("src/**.cpp")
add_includedirs("src")
Intégration dans VSCode
Télécharger les extensions C/C++
et xmake
et executer xmake project -k compile_commands .vscode
pour que l'éditeur sache trouvez le code. Des boutons utiles apparaîtront sur le bas de l'éditeur (build, run, debug). Notez qu'il sera sûrement nécessaire d'éxecuter xmake project -k compile_commands .vscode
pour profiter de toutes les fonctionnalités de l'éditeur.
Utiliser Windows Subsystem for Linux
Voir [1]
Langage
Pour toutes vos question, cppreference.com vous répondra avec précision. Hélas, il ne s'agit pas d'un tuto. Nous explorerons ici quelques concepts.
Fonction main() et éléments
La fonction main()
ou main(int argc, char** argv)
est le point d'entrée du programme.
Comme dans de nombreux langages, on utilise les construction :
if(cond) { code } ; else if(cond2) { code } ; else { code }
while(cond) { boucle }
for(std::size_t i = 0; i < MAX_DE_I; i++) { boucle }
for(auto element : container) { boucle }
.
Les fonctions s'écrivent :
auto doubler(int x) -> int { return 2 * x ; }
int doubler_encore(int x) { return 2 * x ; }
// fonction lambda, compacte pour certains usages "fonctionnels"
auto lambda_qui_double = [](int x){ return 2 * x ; };
Variables
Le C++ est au typage fort (contrairement à son ancêtre le C). Une variable est déclarée avec son type ou celui-ci est déduit.
// une fonction
auto treize() -> int { return 13; }
int main() {
int a = 13;
int b = treize();
auto c = treize();
}
Une variable peut être constante const int x = 13;
Une variable peut être une référence à une autre (qui doit vivre tant que la référence existe !!!). C'est très utile pour prendre comme paramètre un type lourd
// une fonction
auto treize() -> int { return 13; }
int main() {
int a = 13;
int& ref1 = a;
auto& ref2 = a;
auto const& const_ref = a;
}
// évite la copie de "entiers" en prenant une référence !
auto sum(std::vector<int> const& entiers) -> int { ... }
Entrées-sorties console
Les opérateurs << ; >>
permettent de manipuler des "flux". L'IO console se fait ainsi :
int main() {
// Sortie standard
std::cout << "Hello, world!" << std::endl;
// Entrée standard
int x;
std::cout << "Entrez un entier : ";
std::cin >> x; // Attend l'entrée de l'utilisateur et stocke la valeur dans x
std::cout << "Vous avez entré : " << x << std::endl;
return EXIT_SUCCESS;
}
Objets
Les objets en C++ sont créés par constructeur et détruits de manière déterministe à la fin du bloc d'accolades dans lequel il est crée si il est lié à une variable, sinon à la fin de l'expression.
struct rationnel{
int a{0}; // public par défaut dans les "struct", privée par défaut dans les "class"
int b{1};
};
int main() {
const rationnel r1{1, 3}; // construit r = 1/3
if (r1.a == 1) {
const auto r2 = rationnel{1, 4}; // construit r2 = 1/4
} // détruit r2
const rationnel par_defaut1; // construit par_defaut1 = 0/1
const rationnel par_defaut2{}; // construit par_defaut2 = 0/1
rationnel{2, 3}; // construit un temporaire, il est immédiatement détruit
const rationnel r3 = rationnel{r1.b, 2}; // construit r3 = r1.b/2
return 0; // à la sortie de la fonction, r3, par_defaut2, par_defaut1, r1 sont détruits dans cet ordre
}
La destruction déterministe est très utile et permet d'utiliser les objets comme gestionnaire de ressource (équivalent du context manager en python par exemple). Ce mécanisme s'appelle la RAII. Par exemple voici comment créer un buffer OpenGL qui se désalloue automatiquement :
struct buffer
{
private:
GLuint m_id;
public:
buffer() { glGenBuffers(1, &m_id); }
~buffer() {
if (m_id) { glDeleteBuffers(1, &m_id); }
}
// une méthode ordinaire
auto id() const -> GLuint { return m_id; }
// points bonus : permet de "move" un buffer vers un autre qui le remplace
buffer(buffer &&moved) : m_id{ std::exchange(moved.m_id, 0) } {}
};
int main() {
const auto mybuffer = buffer{}; // glGenBuffers()
// use my buffer...
return 0; // ici glDeleteBuffers() est appelé !
}
Les objets supportent aussi beaucoup de fonctionnalités. N'abuser pas des bonnes choses, combiner toutes ces fonctionnalités est la meilleure méthode pour ne plus s'y retrouver...
- membres privés, protégés, publics
- fonctions virtuelles et virtuelles pure ; classes abstraites
- amis (friend) ; c'est un peu obscure
- héritage : simple, multiple, virtuel ; ne pas en abuser svp
- redéfinissions des constructeurs, constructeur par défaut, de copie ou de move
- redéfinissions des opérateurs arithmétiques, d'appel, d'indexation...
Pour s'exercer à la création de structure, un mini-TD d'inf443 est efficace : [2]
Eléments utiles du standard
Conteneurs
Cf cppreference
- std::string ; avec std::string_view, à toujours préférer à char*
- std::array<type, taille> ; à toujours préférer à type[taille], hérité du C
- std::vector<type> ; tableau dynamique, très utile
- std::unordered_set<type> ; hashset
- std::unordered_map<type> ; hashmap
Vues sur une partie du conteneur (slice)
Attention : ces vues ne sont pas propriétaire des données sous-jacente : il est de votre responsabilité de vous assurer que les données du conteneur vers lesquelles elles pointent existent encore.
- std::string_view (nécessite C++17)
- std::span<type> (nécessite C++20)
Monades et assimilés
- std::optionnal<type> (C++ 17) : peut contenir une valeur ou rien.
- std::variant<type1, type2, ...> (C++ 17) : peut contenir un seul des types
- std::expected<type, erreur> (C++23) : contient un type ou une erreur
Le visiteur de variant :
#include <iostream>
#include <variant>
// pattern très sympa
template <typename... Base> struct overload : Base... { using Base::operator()...; };
template <typename... T> overload(T...) -> overload<T...>;
int main() {
const auto var = std::variant<float, bool>{true};
const auto visitor = overload{
[](float x) { std::cout << "float: " << x << std::endl; },
[](bool b) { std::cout << "boolean: " << b << std::endl; }
};
std::visit(visitor, var);
}
Algorithmes
De nombreux algorithmes classiques sont implémentés de manière fonctionnelle et générique : [3]