FAQ C++Consultez toutes les FAQ

Nombre d'auteurs : 35, nombre de questions : 368, dernière mise à jour : 23 mai 2017  Ajouter une question

 

Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.

Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.

Sur ce, nous vous souhaitons une bonne lecture.


SommaireLes fonctions (19)
précédent sommaire suivant
 

Par défaut les paramètres de fonctions sont passés par valeur, c'est-à-dire que c'est une copie du paramètre passé qui est manipulée par la fonction et non l'original. Cela peut paraître anodin lorsqu'il s'agit de passer un type de base, mais cela devient vite pénalisant lorsqu'il s'agit d'une instance de classe dont la copie peut s'avérer coûteuse (par exemple un vector de string). Cela peut également être un problème si l'on souhaite passer en paramètre une classe qui n'est tout simplement pas copiable (par exemple un flux standard). Pour régler le problème, on utilise ainsi ce qu'on appelle le passage par référence ou par référence constante. En passant une référence, on s'assure que c'est l'objet initial qui est manipulé dans la fonction et donc qu'aucune recopie indésirable n'est effectuée.
En passant une référence constante, on s'assure également que notre paramètre ne pourra pas être modifié par la fonction. Une bonne habitude est donc de prendre tout paramètre non modifiable par référence constante, excepté les types primitifs. D'autant plus que cela n'a strictement aucune autre conséquence, ni au niveau de la fonction ni au niveau de l'appelant.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <string> 
#include <vector> 
  
void FonctionPasOptimisee(std::vector<std::string> Tab) 
{ 
    // ... 
} 
  
void FonctionOptimisee(const std::vector<std::string>& Tab) 
{ 
    // ... 
} 
  
std::vector<std::string> v(5000, std::string(1000, '-')); // Tableau de 5000 chaînes de 1000 caractères 
  
FonctionPasOptimisee(v); // recopie inutilement nos 5 millions de caractères 
FonctionOptimisee(v); // ne recopie rien du tout
Attention à ne pas oublier le const si le paramètre n'est pas modifié dans la fonction : cela permet en effet de passer ce
que l'on appelle des temporaires non nommés.

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <string> 
#include <vector> 
  
void FonctionIncorrecte(std::string& s) 
{ 
    // ... 
} 
  
void FonctionCorrecte(const std::string& s) 
{ 
    // ... 
} 
  
std::string s1 = "salut"; 
std::string s2 = "hello"; 
  
FonctionIncorrecte(s1); // Ok 
FonctionCorrecte(s1); // Ok 
  
FonctionIncorrecte(s1 + s2); // Erreur : s1 + s2 est un temporaire 
FonctionCorrecte(s1 + s2); // Ok 
  
FonctionIncorrecte("bonjour"); // Erreur : "bonjour" est un temporaire 
FonctionCorrecte("bonjour"); // Ok
Cette remarque vaut également pour les pointeurs :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
void FonctionIncorrecte(char* s) 
{ 
    // ... 
} 
  
void FonctionCorrecte(const char* s) 
{ 
    // ... 
} 
  
FonctionIncorrecte("bonjour"); // Erreur : "bonjour" est un temporaire 
FonctionCorrecte("bonjour"); // Ok

Mis à jour le 17 octobre 2005 Laurent Gomila

La surcharge est un mécanisme qui permet d'utiliser le même nom pour une fonction mais en lui passant des paramètres de types différents et/ou en nombre différent. Le nom de la fonction et les types des paramètres constituent ce qu'on appelle la signature de la fonction.

Code c++ : Sélectionner tout
1
2
3
int moyenne(int i1, int i2); 
float moyenne(float f1, float f2); //surcharge valide 
float moyenne(int i1, int i2); //surcharge non valide

Mis à jour le 20 avril 2003 Cline

En C, il est possible de déclarer une fonction acceptant un nombre de paramètres variables via « ... » (c'est ce qu'on appelle l'ellipse). L'exemple le plus connu est celui de la fonction d'affichage printf().

En C++ il est bien entendu toujours possible d'utiliser cette méthode mais il y a mieux : le chaînage d'appels. Les avantages sont multiples :

  • Typage beaucoup plus fort.
  • Pas besoin de manipuler des macros bizarroïdes pour récupérer les paramètres.
  • Pas besoin d'indication supplémentaire pour marquer le nombre et le type des paramètres.
  • Beaucoup plus simple à écrire, et plus flexible.

Cette méthode est intensivement utilisée par exemple pour manipuler les flux standards :

Code c++ : Sélectionner tout
1
2
3
#include <iostream> 
  
std::cout << x << y << z;
On peut également imaginer d'autres formes de chaînages pour d'autres applications :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector> 
  
class Polygon 
{ 
pulic : 
  
    Polygon& Add(int x, int y) 
    { 
        Points_.push_back(Point(x, y)); 
  
        return *this; 
    } 
  
private : 
  
    std::vector<Point> Points_; 
}; 
  
Polygon Poly; 
Poly.Add(1, 2) 
    .Add(5, 8) 
    .Add(15, 19) 
    .Add(0, 54);
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sstream> 
#include <string> 
  
class StringBuilder 
{ 
pulic : 
  
    template <typename T> 
    StringBuilder& operator ()(const T& t) 
    { 
        std::ostringstream oss; 
        oss << t; 
        String_ += oss.str(); 
  
        return *this; 
    } 
  
private : 
  
    std::string String_; 
}; 
  
StringBuilder s; 
s("salut j'ai ")(24)(" ans ");
Comme vous le voyez, ce qui rend possible le chaînage est le renvoi de l'instance courante par la fonction. Ainsi, Poly.Add(x, y) renvoie Poly, sur lequel on peut de nouveau appeler Add() etc.

Mis à jour le 17 octobre 2005 Laurent Gomila

En C++, en général, on évite d'utiliser les fonctions callback au profit d'alternatives un peu plus « objet » (tel les foncteurs par exemple). Cependant, on est parfois obligé d'y recourir, typiquement pour s'interfacer avec une autre bibliothèque écrite en C. Si tel est votre cas, il vous faut alors être prudent et veiller à ce que votre fonction callback C++ passée à la bibliothèque C ne lève pas d'exception, surtout si vous l'utilisez déjà compilée (dll). La raison est que les exceptions C++ ne sont pas supportée en C, et les conséquences d'une exception levée dans votre callback C++ et remontant jusqu'au code C appelant peuvent être fâcheuses. Et d'une manière plus générale, les exceptions posent problème dès qu'il s'agit de franchir les limites d'un module compilé (telle une dll), même entre differents modules développés en C++ (ABI incompatibles).

On peut toutefois préciser que sous Windows, un système d'exceptions (Structured Exception Handling, ou SEH) est intégré au sein même du système. Certains compilateurs l'exploitent, y compris en langage C (au moyen de mots clés spécifiques), ce qui permet à du code C d'être traversé sans problème par des exceptions lancées depuis un code C++, si celles-ci ont été émises sous forme de SEH. Certains compilateurs s'appuient sur SEH pour implémenter leurs exceptions C++ (c'est le cas de Visual C++ par exemple), les rendant ainsi compatibles avec n'importe quel autre code compilé. Consultez la documentation de votre compilateur pour plus de détails.

Mis à jour le 15 octobre 2009 Aurelien.Regat-Barrel

Parce qu'on vous a menti !
Cette pratique provient d'anciens langages de programmation, qui ne disposaient pas de mécanismes élégants pour la gestion de la mémoire.
Ainsi, on n'avait coutume d'écrire des choses qui ressemblent à ça :

Code c : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int f() 
{ 
    char * my_string = (char*) malloc(20 * sizeof(char) ); 
    int result; 
    /* début du traitement complexe */ 
        if(error) 
        { 
            result = -1; 
            goto end_f; 
        } 
    /* fin du traitement complexe */ 
:end_f 
    free(my_string); 
    return result; 
}
Ceci permettait de gérer toute la libération mémoire dans un seul bloc de code. Mais pour passer dans ce bloc de code à coup sûr, il était nécessaire d'utiliser, soit des imbrications de ifà n'en plus finir, soit des gotos. Les return multiples étaient sources de nombreuses fuites mémoires, difficiles à diagnostiquer.
Heureusement, C++ possède un mécanisme très puissant, le destructeur.
Ainsi, le code précédent s'écrira :

Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
int f() 
{ 
    std::string my_string; 
    /* début du traitement complexe */ 
       if(error) 
            return -1; 
    /* fin du traitement complexe, qui contient maintenant le return */ 
}
Plus besoin de gérer l'allocation mémoire, elle sera libérée par le destructeur de std::string. Et si l'on est obligé d'allouer avec new, C++ nous offre là encore un moyen simple de gérer cela, std::auto_ptr :

Code c++ : Sélectionner tout
1
2
3
4
5
int f() 
{ 
    std::auto_ptr<Obj> local_object = new CMyClass(); 
    // reste de la fonction 
}
local_object sera libéré (appel de delete) dès que l'on sort de la portée de la variable.
Et pour les objets alloués avec new[] ? C++ ne fournit pas en standard de moyen... sauf d'utiliser plutôt std::vector !
Et comme souvent, si ce n'est pas possible, Boost vient à la rescousse avec boost::scoped_array.
Et pour les objets dont l'allocation est faite par une fonction d'une bibliothèque, à libérer en appelant une autre fonction de cette bibliothèque ? C++ nous offre la possibilité d'utiliser des scope_guard, qui appelleront automatiquement la fonction de libération lors de leur destruction.
Il n'y a donc de nos jours plus aucune raison de se priver de la possibilité d'utiliser plusieurs instructions return, lorsque cela apporte plus de clarté et de lisibilité au code.

Mis à jour le 15 octobre 2009 white_tentacle

Réponse courte :

Il convient de retarder la déclaration (définissante) des variables locales jusqu'au dernier moment. Si en plus la variable peut être déclarée immuable, ce n'est que mieux.

N.B. : Cela est également applicable au C99, sauf si vous visez la portabilité avec des compilateurs C non entièrement compatibles comme les VC++.

Justifications
Certains langages comme Pascal ou le C90 limitent les endroits où on peut définir des variables, ce qui force parfois à le faire non seulement bien avant leur première utilisation (en début de nouveaux blocs imbriqués, voire en début de fonction/procédure), mais avant même qu'il soit possible de les initialiser correctement.

De fait, les règles qualité en vigueur dans ces langages demandent généralement d'initialiser ces variables aussitôt que possible pour ne pas les laisser dans un état aléatoire. En effet, cela évite les erreurs difficilement reproductibles (d'une machine à l'autre) que l'on observerait avec :
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
int f(void) { 
    int i; 
    int result; 
    if (h(time()) { 
        result = g(i); 
    } 
    // ... du code2 ... 
    i = 42; 
    // ... du code3 ... 
    return result; 
}
En initialisant i à 0 par exemple, on peut observer des comportements erronés, mais reproductibles. En initialisant result à quelque chose, si h() déroute l'affectation de result, nous aurons toujours un résultat non aléatoire. Pas forcément celui qu'il aurait convenu, mais il aurait été non aléatoire.
Nous sommes en pleine programmation défensive : on évite au programme de planter, et tant qu'à faire, on essaie de rendre les comportements erronés reproductibles. Seulement, nous masquons les vrais problèmes. Dans ce code, il ne faudrait pas pouvoir appeler g(i) tant que i n'est pas dans un état pertinent (ici, 42). Et il faudrait également ne pas oublier d'affecter le résultat.

C'est là que le C++ et le C99 entrent en scène. Ces deux langages laissent le choix quant à l'endroit où l'on peut déclarer et définir une variable locale : on peut le faire n'importe où dans le corps d'un traitement (i.e. une fonction ou plus généralement un bloc d'accolades).

Cette propriété va nous permettre de déléguer au compilateur la vérification des invariants de nos variables. Eh oui, aussi étrange que cela puisse paraître, les variables ont bien des invariants : être dans des états exploitables et pertinents. Dans un monde idéal, une variable respecte son invariant de pertinence ou… n'existe pas. Et là est la réponse : tant que l'on n'est pas capable de donner une valeur pertinente à une variable pour qu'elle soit exploitable, faisons en sorte que la variable n'existe pas.
Ainsi, notre code précédent deviendrait :
Code c++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
int f(void) { 
    int result = valeur_par_defaut; 
    if (h(time()) { 
        result = g(i); 
    } 
    // ... du code2 ... 
    const int i = 42; 
    // ... du code3 ... 
    return result; 
}
Et nous voyons tout de suite qu'il y a une erreur -- au pire, le compilateur nous le rappellera. Il devient nécessaire de réorganiser le code pour que les choses s'enchaînent comme il se doit.

Application aux constructeurs
Dans la continuité de ce sujet, on retrouve les constructeurs. Leur rôle est de faire en sorte que les objets construits soient dans un état pertinent et exploitable à l'issue de la construction, ou de faire en sorte que ces objets n'existent pas. C'est pour cela que les fonctions init() sont décriées dans les langages OO. En effet, on commence par avoir un objet partiellement construit jusqu'à l'appel de la fonction init(). Si l'appel à cette dernière est oublié, l'objet ne sera jamais complètement construit. Et si init() échoue, nous avons toujours un objet présent, mais qu'il ne faut en aucun cas utiliser. Le code utilisateur devra le tester pour dérouter à la main, et le code appelé devra tester dans toutes les fonctions si l'objet est bien initialisé pour bloquer la suite des opérations.
Avoir un constructeur qui échoue (par exception) simplifie les choses : par construction/écriture du code, si l'objet n'est pas exploitable, il ne peut pas exister et il n'y aura rien à tester.

N.B. : des setters sont encore pires qu'init() à ce sujet. Quand on écrit trivialement des setters, on ne positionne jamais de flag is_initialized. Pire, il faudrait savoir que les 12 setters (différents) de la classe ont bien été appelés avant de pouvoir positionner ce flag. init() nous assure au moins de pouvoir vérifier dynamiquement l'invariant à défaut de pouvoir le faire statiquement (à la compilation).

Allons plus loin et rendons nos variables immuables
Sur un sujet connexe, si en plus nous savons que l'état de la variable ne changera jamais, il est préférable de la signaler comme non modifiable. Ce n'est pas pour des questions d'optimisation (car cela ne changera rien), mais pour des questions de facilité de maintenance. Dans six mois, lors de la relecture de votre code, le const vous permettra de savoir immédiatement qu'une fois la valeur connue, il n'est plus nécessaire de réfléchir à « Est-ce parce qu'elle a changé que j'ai un bug ? Mais où a-t-elle changé ? »

Voir à ce sujet :
C++ Coding Standards, item 15, use const proactively, H.Sutter & A.Alexandrescu
Summary:
const is your friend: Immutable values are easier to understand, track, and reason about, so prefer constants over variables wherever it is sensible and make const your default choice when you define a value: It's safe, it's checked at compile time (see Item 14), and it's integrated with C++'s type system. Don't cast away const except to call a const-incorrect function (see Item 94).
------
const est votre ami : les valeurs immuables sont plus faciles à comprendre, pister et appréhender, donc préférez les constantes aux variables lorsque cela a du sens et utilisez const par défaut lorsque vous définissez une valeur : c'est plus sûr car c'est vérifié à la compilation (voir partie 14) et il est intégré au système de type C++. N'effectuez pas de cast supprimant un const sauf pour appeler une fonction const-incorrecte (voir partie 94).

Mis à jour le 6 juillet 2014 Luc Hermitte

Proposer une nouvelle réponse sur la FAQ

Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour ça


Réponse à la question

Liens sous la question
précédent sommaire suivant
 

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

 
Contacter le responsable de la rubrique C++