C++

De WikiBR

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

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]