IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > Divers
        Comment exécuter une commande système ou un autre programme ?
        Qu'est-ce que C++0x ?
        Peut-on tester C++0x ?
        Qu'est-ce que le Library Technical Report (tr1 / tr2) ?
        Quelles sont les questions à poser pour savoir si un candidat connaît vraiment son sujet ?
        Comment accélérer la compilation de mes projets ?
        Mes calculs sur nombres flottants sont imprécis, que faire ?
        Comment connaître les macros prédéfinies pour les différentes plateformes ?
        Quels sont les identificateurs interdits par la norme ?
        Qu'est-ce qu'un type POD ?
        Que sont les rvalues et lvalues ?
        Qu'est-ce que la RVO ?
        Qu'est-ce que la NRVO ?
        Quand s'appliquent la RVO et la NRVO ?
        Quels impacts de la (N)RVO sur le passage d'arguments ?
        (N)RVO et classes non copiables ?
        Qu'est-ce que le mot-clef volatile ?
        Qu'est-ce que l'immuabilité ?



Comment exécuter une commande système ou un autre programme ?
Créé le 18/04/2005[haut]
auteur : Aurélien Regat-Barrel
La fonction standard std::system() définie dans <cstdlib> permet d'exécuter la commande qui lui est fournie en paramètre au moyen de l'interpréteur de commande du système d'exploitation. Son comportement est donc spécifique à chaque OS.
#include <cstdlib> 

int main() 
{ 
    // exécute la commande système "dir" 
    // le texte affiché dépend de l'OS 
    std::system( "dir" ); 
    // exécute le programme "toto.exe" 
    std::system( "toto.exe" ); 
}
L'appel à cette fonction ne rend pas la main tant que la commande n'a pas terminé son exécution. Si vous souhaitez un comportement différent (que la fonction rende la main immédiatement, qu'une console ne soit pas créée, rediriger les flux standard, ...), il faut vous tourner vers une solution spécifique à votre système d'exploitation.
Voir les fonctions faq CreateProcess / ShellExecute[Ex] sous Windows, et les fonctions de type exec*() et spawn() définies dans les headers <unistd.h> / <process.h> pour les systèmes UNIX/POSIX (Notez qu'elles sont aussi disponibles avec un underscore préfixé à leur nom (en _exec et en _spawn sous Visual C++)


Qu'est-ce que C++0x ?
Mise à jour le 03/19/2006[haut]
auteurs : Aurélien Regat-Barrel, Laurent Gomila
C++0x est le nom de code de la prochaine version de l'actuel standard ISO C++ 98, qui porte le nom de C++98. Cette future norme devrait être ratifiée d'ici 2009, et le 'x' dans son nom de code sera alors remplacé par l'année en cours (par exemple, C++09 si cela a lieu en 2009).

Faire évoluer un langage tel que le C++ est une tâche complexe qui nécessite du temps. Conscient que C++0x ne serait pas ratifié avant la fin de la décénie, le commité de normalisation ISO a entrepris et achevé en 2005 la rédaction d'un document intermédiaire limité au développement d'extensions à la bibliothèque standard du C++. Vous pouvez lire à ce sujet Qu'est-ce que le Library Technical Report (tr1 / tr2) ?.

Voici quelques liens vers des documents sur le sujet :

On peut également trouver des brouillons du prochain standard à cette adresse : en C++ Standards Committee Papers.


Peut-on tester C++0x ?
Créé le 17/11/2008[haut]
auteur : Alp Mestan
Il est déjà possible de tester quelques nouvelles fonctionnalités de C++0x grâce au travail énorme fourni par les équipes de développement de 2 compilateurs majeurs : Microsoft Visual C++ et g++.

En effet, depuis quelques années, nous avons pu voir le compilateur g++ implémenter des fonctionnalités de C++0x. En ce qui concerne les concepts de C++0x, ils sont dans une grande mesure utilisables par le biais de en ConceptGCC. Parallèlement, g++ intègre peu à peu des fonctionnalités du monde C++0x au fil de ses versions 4.x et l'on peut suivre ce développement sur en le site officiel de gcc/g++.

Enfin, Visual C++ commence également à intégrer des fonctionnalités de C++0x. En effet, en Visual C++ 2010 Community Technology Preview propose déjà l'utilisation des fonctionnalités suivantes :

  • lambda Expressions ;
  • rvalue References ;
  • static_assert ;
  • mot clé auto, mais dans sa nouvelle version (il fait partie des fonctionnalités relatives à l'inférence de type en C++).
lien : faq Qu'est-ce que C++0x ?

Qu'est-ce que le Library Technical Report (tr1 / tr2) ?
Créé le 03/02/2007[haut]
auteur : Aurélien Regat-Barrel
Le Technical Report on C++ Library Extensions est le premier Technical Report (TR1) consacré à la bibliothèque standard du C++. Il a été réalisé en 2005, et constitue une sorte de brouillon officiel décrivant un ensemble d'extensions à la bibliothèque standard actuelle suceptibles d'être adoptées dans C++0x. Un deuxième document du même type est actuellement à l'étude (TR2).

Un Technical Report n'est pas normatif, c'est-à-dire qu'il ne fait pas officiellement partie de la norme, et ne garantit pas que ce sera le cas dans le futur. Les distributeurs de compilateurs ne sont pas obligés non plus de développer et fournir ces extensions. Un autre élément important est que les extensions proposées s'appuient sur les capacités actuelles du langage C++, et non sur les futures possibilités de C++0x.

Cependant, un TR (Technical Report) préfigure fortement ce que sera la prochaine norme C++, et l'objectif d'un tel document est double :

  • fournir dès maintenant aux programmeurs C++ de nouvelles bibliothèques préstandards ;
  • obtenir des retours d'expériences sur ces ajouts afin d'y apporter des améliorations avant leur normalisation définitive.
Au niveau pratique, toutes les nouveautés introduites par le TR1 le sont au sein d'un nouveau namespace tr1 membre du namespace standard std. De nombreux éléments sont directement issus de la bibliothèque boost. On retrouve par exemple les fameux shared_ptr (voir Comment utiliser les pointeurs intelligents de Boost ?), qui ont été intégrés aux côtés de std::auto_ptr dans le fichier d'en-tête <memory> (voir Pourquoi faut-il se méfier de std::auto_ptr ?):
#include <memory>

int main()
{
    std::tr1::shared_ptr<int> p( new int );
}
Les principales extensions issues de boost et incluses dans le TR1 sont :

  • enrichissement de <memory> avec des pointeurs intelligents (shared_ptr, weak_ptr, ...) ;
  • enrichissement de <functional> (bind, ref, reference_wrapper, mem_fn, result_of, ...) ;
  • enrichissement de <utility> (tuple_size, tuple_element, ...) ;
  • ajout de <tuple> (collections d'éléments : paires, triples, quadruples, ...) ;
  • ajout de <type_traits> (aide à la méta-programmation) ;
  • ajout de <random> (génération de nombres aléatoires) ;
  • ajout de <array> (tableau de taille fixe) ;
  • ajout de <unordered_set> et de <unordered_map> (tables de hachage) ;
  • ajout de <regex> (expressions régulières).
Pour plus de détails, vous pouvez aussi lire Library Technical Report.

Mais le TR1 ne se résume pas à la récupération de bibliothèques de Boost. Il y a aussi de nombreuses nouveautés issues du C99 et un enrichissement important des fonctions mathématiques.

Plusieurs compilateurs et développeurs de bibliothèque standard se sont lancés dans leur propre implémentation du TR1. C'est le cas de GCC et de CodeWarrior par exemple, ainsi que de Dinkumware qui est le premier a l'avoir implémenté à 100% (Dinkumware est aussi le fournisseur de la STL de VC++, ce qui laisse penser que ce dernier supportera lui aussi le TR1).

Les développeurs de Boost se sont bien sûr eux aussi atelés à la tâche, et Boost.TR1 est actuellement en cours de développement.


Quelles sont les questions à poser pour savoir si un candidat connaît vraiment son sujet ?
Créé le 10/02/2004[haut]
auteur : Marshall Cline
Cette question se destine tout d'abord aux responsables non techniques et au personnel des ressources humaines qui essaient de faire un travail de bonne qualité quand ils interviewent des candidats développeurs C++. Si vous êtes un programmeur C++ sur le point d'être interviewé, et que vous consultez cette FAQ en espérant savoir à l'avance quelles questions vont vous être posées de façon à ne pas devoir apprendre réellement le C++, honte à vous. Prenez le temps de devenir un développeur compétent et vous n'aurez pas besoin d'essayer de tricher.

Pour revenir aux non techniciens et au personnel des ressources humaines : il est évident que vous êtes parfaitement qualifiés pour juger si un candidat a un profil correspondant à la culture de votre entreprise. Cependant, il y a assez de charlatans, de menteurs et de frimeurs pour que vous ayez besoin de faire équipe avec quelqu'un qui est techniquement compétent pour s'assurer que le candidat ait le niveau technique adéquat. Assez de sociétés ont souffert d'avoir engagé des gens sympathiques mais incompétents, des gens globalement incompétents malgré le fait qu'ils connaissent les réponses à quelques questions très pointues. La seule façon de démasquer les menteurs et les frimeurs est d'avoir à vos cotés une personne capable de poser des questions techniques pointues. Il n'y a aucun espoir que vous y arriviez par vous-même. Même si je vous donnais une série de questions pièges, elles ne vous permettraient pas de démasquer ces personnes.

Votre partenaire technique n'a pas besoin d'être qualifié (et bien souvent, il ne l'est pas) pour juger la personnalité du candidat, donc, par pitié, n'oubliez pas que vous êtes le seul juge au final. Mais ne pensez pas que vous pouvez poser une demi-douzaine de questions sur le C++ et avoir la moindre chance de savoir si le candidat sait de quoi il parle, du moins d'un point de vue technique.

Par contre, si vous avez un niveau technique suffisant pour lire cette FAQ, vous pouvez y piocher quantité de bonnes questions pour une interview. La FAQ contient bon nombre de choses permettant de séparer le bon grain de l'ivraie. La FAQ se concentre sur ce que les programmeurs doivent faire, plutôt que d'essayer de voir ce que le compilateur leur laisse faire. Il y a des choses en C++ qu'il est possible de faire mais qu'il vaut mieux éviter. La FAQ sert à faire le tri là-dedans.


Comment accélérer la compilation de mes projets ?
Créé le 17/10/2005[haut]
auteur : Laurent Gomila
Voici quelques astuces pour réduire les temps de compilation, qui peuvent parfois se compter en minutes voire en heures sur des projets de taille conséquente.

1/ Utiliser les déclarations anticipées

Si dans un en-tête vous ne déclarez qu'un pointeur ou une référence sur une classe, alors vous n'avez pas besoin d'inclure son en-tête : une déclaration anticipée suffit.

MaClasse.h

class Classe1; // pas d'inclusion de "Classe1.h"
class Classe2; // pas d'inclusion de "Classe2.h"

class MaClasse
{
    void Something(const Classe1& c);

    Classe2* c2;
};
MaClasse.cpp

#include "Classe1.h"
#include "Classe2.h"

void MaClasse::Something(const Classe1& c)
{
    // Ici on peut accéder aux membres de Classe1 et Classe2,
    // on a inclus leurs en-têtes
}
Ainsi lorsque Classe1 ou Classe2 sera modifiée, seul MaClasse.cpp sera recompilé. Sans cette astuce, vous auriez dû recompiler (inutilement) tous les fichiers incluant MaClasse.h. L'élimination des dépendances inutiles est essentielle pour améliorer les temps de compilation.

2/ Utiliser l'idiome enveloppe-lettre, appelé aussi Pimpl (Private Implementation)

Le principe de cet idiome est de séparer l'interface publique d'une classe de son implémentation.

MaClasse.h (interface)

class MaClasseImpl;

class MaClasse
{
public :

    void Set(int);
    int Get() const;

private :

    MaClasseImpl* pImpl;
};
MaClasse.cpp

#include "MaClasseImpl.h"

void MaClasse::Set(int x)
{
    pImpl->Set(x);
}

int MaClasse::Get() const
{
    return pImpl->Get();
}
MaClasseImpl.h (implémentation)

class MaClasseImpl
{
public :

    void Set(int);
    int Get() const;

private :

    int val;
};
MaClasseImpl.cpp

#include "MaClasseImpl.h"

void MaClasseImpl::Set(int x)
{
    val = x;
}

int MaClasseImpl::Get() const
{
    return val;
}
Ainsi, seule une modification de l'interface de MaClasse entraînera une recompilation des fichiers qui en dépendent. Un changement dans son implémentation n'entraînera la recompilation que de deux fichiers au maximum : MaClasse.cpp et MaClasseImpl.cpp.
Comme vous le voyez dans cet exemple, MaClasse est ici limitée à son interface publique, toutes ses données privées et protégées sont cachées dans MaClasseImpl. C'est un autre avantage de ce procédé : en plus de réduire les dépendances et d'accélérer la compilation, l'idiome pimpl permet de ne rendre visible pour les clients d'une classe que son interface publique.

Pour plus de détails sur l'idiome pimpl, nous vous invitons à consulter ces liens tirés de GOTW :
en http://www.gotw.ca/gotw/024.htm
en http://www.gotw.ca/gotw/028.htm

3/ Utiliser les en-têtes précompilés

En plus des techniques citées ci-dessus, certains environnements de développement comme VC++ et BCB proposent d'utiliser un en-tête précompilé. Pour en tirer partie il faut :

  • Activer son utilisation dans les options du projet.
  • Inclure dans l'en-tête précompilé (par défaut, stdafx.h sous VC++) tous les en-têtes que vous ne modifierez pas ou peu souvent (en-têtes standards, de bibliothèques externes, ...).
  • Inclure l'en-tête précompilé dans toutes les unités de traduction (les .cpp) du projet.
Attention, si vous y incluez un en-tête subissant des modifications fréquentes cela aura l'effet inverse : à chaque modification de celui-ci vous aurez droit à une recompilation complète.


Mes calculs sur nombres flottants sont imprécis, que faire ?
Créé le 03/02/2007[haut]
auteurs : Laurent Gomila, Jean-Marc.Bourguet
C'est tout à fait normal. Cela est dû au codage interne des flottants en mémoire (selon la norme IEEE 754). Ces imprécisions deviennent d'autant plus gênantes qu'elles s'accumulent en même temps que les opérations que vous effectuez sur vos nombres ; ainsi il est tout à fait possible d'obtenir des nombres très éloignés des résultats théoriques.

Il n'existe pas de solution miracle, cependant vous pouvez tout de même trouver un compromis selon vos besoins :

-> Vous avez besoin d'une précision exacte (gestion de comptes ou autres applications monétaires) : utilisez plutôt ce que l'on appelle les nombres en virgule fixe, qui, généralement codés sous forme d'entiers, ne souffrent pas de ces imprécisions. Le C++ ne dispose pas en standard de types en virgule fixe, mais vous pourrez facilement trouver des classes ou des bibliothèques toutes faites, voire construire la vôtre.

-> Vous voulez tenir compte des imprécisions dans vos calculs : utilisez un epsilon (nombre très petit) lors de vos tests de comparaison. Typiquement, l'espilon choisi dépend de votre application et des grandeurs manipulées. Un point de départ est l'espilon défini dans la bibliothèque standard (std::numeric_limits<float>::epsilon() pour les float par exemple), qui définit le plus petit flottant tel que 1 + espilon > 1. Ainsi vos comparaisons deviennent :

#include <cmath> // pour std::abs
#include <limits> // pour std::numeric_limits

using namespace std;

float f1 = 0.1f;
float f2 = 1.1f;
float f3 = f2 - f1;

// Version incorrecte ne tenant pas compte des imprécisions
if (f3 == 1.0f)
{
    // Pratiquement jamais vrai !
}

// Version correcte tenant compte des imprécisions
// err est une variable choisie en fonction de l'erreur autorisée
if (abs(f3 - 1.0f) <= err * max(abs(f3), abs(1.0f)) * numeric_limits<float>::epsilon())
{
    // Ok
}

Comment connaître les macros prédéfinies pour les différentes plateformes ?
Créé le 03/02/2007[haut]
auteur : Laurent Gomila
Lorsque l'on veut écrire du code portable, il est parfois nécessaire d'utiliser la compilation conditionnelle, c'est-à-dire compiler tel ou tel code selon la plateforme sur laquelle on se trouve.

Par exemple, voici un code qui sera capable de compiler correctement à la fois sous Windows et sous Unix :

#if defined (linux)
    // Code spécifique à Linux
#elif defined (_WIN32)
    // Code spécifique à Windows
#else
    // Plateforme non gérée, on peut par exemple placer une erreur de compilation
    #error Plateforme non gérée
#endif
Cependant, connaître toutes les macros prédéfinies pour chaque OS, compilateur, ... est très difficile.

La solution la plus rapide est d'aller visiter le site Pre-defined C/C++ Compiler Macros, qui maintient une liste à jour des macros prédéfinies pour les choses suivantes :

  • Standard.
  • Compilateurs.
  • Systèmes d'exploitation.
  • Bibliothèques.
  • Architectures.
Une bonne habitude est également d'aller regarder les en-têtes de bibliothèques portables (par exemple boost). Ceux-ci sont truffés de conditions sur les plateformes, et vous donneront de bonnes informations.


Quels sont les identificateurs interdits par la norme ?
Mise à jour le 17/03/2008[haut]
auteurs : Laurent Gomila, JolyLoic, Mat007
En plus de ses mots-clé, le C++ réserve certains identificateurs, notamment pour l'implémentation de la bibliothèque standard ou pour les ajouts propriétaires que l'on peut trouver sur la plupart des compilateurs. Cela assure qu'il n'y aura aucun conflit entre votre code et celui livré par le compilateur, qui ne sera pas forcément documenté.

En outre, la norme réserve (et donc interdit) les identificateurs suivant :

  • Les noms commençant par un _ suivi d'une majuscule.
  • Les noms commençant par _ dans le namespace global.
  • Les noms contenant __ (deux _ d'affilée).
Une version simplifiée de cette règle est donc de ne pas commencer ses noms par un _ du tout, ni d'utiliser __.


Qu'est-ce qu'un type POD ?
Créé le 15/10/2009[haut]
auteur : r0d
Un type POD (de l'anglais Plain Old Data) est un type C++ qui a un équivalent en C, et qui utilise les mêmes règles que le C pour l'initialisation, la copie et l'adressage.

La définition précise d'un type POD est récursive et un peu absconse. Voici une définition légèrement simplifiée : les données membres non statiques d'un type POD doivent être publiques et peuvent être de ce type :

  • bool ;
  • tous les types numériques, y compris des divers char ;
  • enumération ;
  • pointeur de données (i.e, tous les types convertibles en void*) ;
  • pointeur de fonction (mais pas un pointeur sur une fonction membre) ;
  • type POD, y compris un tableau de POD.
Notez que les références ne sont pas permises pour un type POD. De plus, un type POD ne peut avoir ni constructeur, ni fonction virtuelle, ni classe de base (pas d'héritage), ni surcharge d'opérateur d'assignation.

Dans certaines situations, le C++ permet uniquement l'utilisation de POD. Par exemple, une union ne peut pas contenir une classe qui a des fonctions virtuelles ou des constructeurs non triviaux. Les PODs peuvent également être utilisés pour interfacer du code C++ avec du code C.


Que sont les rvalues et lvalues ?
Créé le 15/10/2009[haut]
auteur : Alp Mestan
Une lvalue est, à l'origine, une expression qui peut se trouver du côté gauche lors d'une affectation, c'est-à-dire une expression à laquelle on peut affecter quelque chose. Désormais, une lvalue désigne toute expression qui fait référence à un objet, c'est-à-dire toute expression qui fait référence à une région contigüe de mémoire. Cela peut donc être par exemple un objet constant, ce qui ne rentre pas forcément dans la définition utilisant l'affectation.

Par conséquent, les identifiants (de variables) sont des lvalues. Il en va de même pour les références.

Voici un code qui va peut-être aider à la compréhension :

int n = 5; // n est une lvalue
Client& getClient() { /* ... */ }
getClient() = UnAutreClient; // getClient() désigne ici une lvalue car la référence renvoyée correspond à la définition d'une lvalue
 
std::string s = "C++"; // s est une lvalue
// ...
Une rvalue est, à l'origine, une expression qui peut apparaître du côté droit d'une affectation mais pas du côté gauche. De façon peut-être plus claire, on définit une rvalue comme une expression qui n'est pas une lvalue.

Les temporaires par exemple sont des rvalues. Les rvalues peuvent être considérées comme des "valeurs" d'expressions (constantes, résultat d'évaluations d'expressions, ...).

Il est utile de connaître ces notions et savoir les distinguer pour avoir une meilleure maîtrise de son code, savoir ce que l'on peut modifier, ce que l'on ne peut pas et ainsi donner le droit ou non de modifier des objets lorsque l'on crée des fonctions notamment.


Qu'est-ce que la RVO ?
Créé le 15/10/2009[haut]
auteur : Florian Goujeon
La RVO (pour Return Value Optimization, optimisation de la valeur de retour) est une optimisation automatique du code source effectuée par la plupart des compilateurs C++.
Cette optimisation s'applique lors d'un appel de fonction renvoyant un objet dont la copie est couteuse. Elle consiste à modifier la façon dont la fonction renvoie son objet de retour, de telle façon à ce qu'aucune copie dudit objet ne soit créée. Il en résulte alors un code potentiellement bien plus rapide.
Prenons comme exemple le code suivant. Celui-ci définit tout d'abord une classe car (voiture), composée de nombreux objets. Cette classe comprend un constructeur, un constructeur par copie et un destructeur.
class car
{
  public:
    //constructeur
    car
    (
      const engine& e, //moteur
      const wheels& w, //roues
      const doors& d, //portières
      const steering_wheel& s //volant
      //etc...
    );

    //constructeur par copie
    car(const car& c);

    //destructeur
    ~car();

  private:
    engine engine_;
    wheels wheels_;
    doors doors_;
    steering_wheel steering_wheel_;
    //d'autres composants...
};
Les constructeurs et destructeur contiennent une instruction d'écriture sur la sortie standard. Ainsi, nous aurons un rapport détaillé des différentes opérations effectuées.
car::car
(
  const engine& e,
  const wheels& w,
  const doors& d,
  const steering_wheel& s
  //etc...
):
  engine_(e),
  wheels_(w),
  doors_(d),
  steering_wheel_(s)
  //etc...
{
  std::cout << "Appel au constructeur de car.\n";
}

car::car(const car& s):
  engine_(s.engine_),
  wheels_(s.wheels_),
  doors_(s.doors_),
  steering_wheel_(s.steering_wheel_)
  //etc...
{
  std::cout << "Appel au constructeur par copie de car.\n";
}

car::~car()
{
  std::cout << "Appel au destructeur de car.\n";
}
Notre code contient également une fonction build_sport_car(), renvoyant un objet de type car :
const car
build_sport_car()
{
  const engine e = build_sport_engine();
  const wheels w = build_sport_wheels();
  const doors d = build_sport_doors();
  const steering_wheel s = build_sport_steering_wheel();
  //fabrication des autres composants...

  return car(e, w, d, s);
}
Enfin, la fonction main() se contente d'appeler la fonction build_sport_car() :
int
main()
{
  car my_car = build_sport_car();

  std::cout << "Fin du programme.\n";
  return 0;
}
Si l'on désactive la RVO, le programme produit la sortie suivante :

Appel au constructeur de car. (1)
Appel au constructeur par copie de car. (2)
Appel au destructeur de car. (3)
Appel au constructeur par copie de car. (4)
Appel au destructeur de car. (5)
Fin du programme.
Appel au destructeur de car. (6)
  1. Tout d'abord, la fonction build_sport_car(), appelée depuis main(), crée une instance de car.
  2. Le retour de l'objet créé se traduit par sa copie vers un objet temporaire anonyme. Cet objet temporaire sera en fait la valeur de l'expression « build_sport_car() » située dans la fonction main().
  3. Une fois l'objet copié avec succès, la fonction build_sport_car() se termine. Les objets de la pile créés par cette fonction, comprenant l'instance de car, sont détruits un à un.
  4. De retour à la fonction main(), c'est au tour de l'objet temporaire anonyme d'être copié dans l'objet my_car...
  5. ... avant d'être détruit à son tour.
  6. Enfin, le programme se terminant, l'objet my_car est détruit. Il n'existe alors plus d'instance de car.
Activons maintenant la RVO (il s'agit du réglage par défaut de tous les compilateurs supportant cette optimisation).
Le programme produit une sortie tout à fait différente.

Appel au constructeur de car.
Fin du programme.
Appel au destructeur de car.
Comme dans le cas précédent, la fonction build_sport_car() crée l'instance de car. Mais cette fois-ci, aucun objet temporaire n'est créé et aucune copie vers l'objet my_car n'est effectuée.
Le compilateur a détecté que l'instance anonyme de voiture créée dans la fonction build_sport_car() devait être copiée vers l'objet my_car.
Une fois l'optimisation appliquée, ces deux objets ne font plus qu'un. Nous évitons alors deux copies et deux destructions, et gagnons ainsi en vitesse d'exécution.


Qu'est-ce que la NRVO ?
Créé le 15/10/2009[haut]
auteur : Florian Goujeon
Un type d'optimisation similaire peut également être appliqué dans le cas où l'instance de retour de la fonction est nommée. On parle alors de NRVO (Named Return Value Optimization, pour optimisation de la valeur de retour nommée).
const car
build_sport_car()
{
  const engine e = build_sport_engine();
  const wheels w = build_sport_wheels();
  const doors d = build_sport_doors();
  const steering_wheel s = build_sport_steering_wheel();
  //fabrication des autres composants...

  car sport_car(e, w, d, s); //l'instance est nommée
  sport_car.price(75000);
  sport_car.build_date(today());

  return sport_car;
}

Quand s'appliquent la RVO et la NRVO ?
Créé le 15/10/2009[haut]
auteur : Florian Goujeon
Là où il est intéressant d'être conscient de l'existence de ces deux types d'optimisation, c'est que leur application dépend de la façon dont sont écrites les fonctions concernées.
En effet, il pourra être impossible pour le compilateur de déterminer l'instance de retour si la fonction comporte plusieurs points de sortie (plusieurs instructions return) avec des instances différentes.
Par exemple, la NRVO ne pourra être appliquée au code suivant :
const car
build_car(const bool type_sport = true)
{
  if(type_sport)
  {
    const engine e = build_sport_engine();
    const wheels w = build_sport_wheels();
    const doors d = build_sport_doors();
    const steering_wheel s = build_sport_steering_wheel();
    //fabrication des autres composants...

    car sport_car(e, w, d, s); //une première instance…

    sport_car.price(75000);

    return sport_car; //un premier point de sortie…
  }
  else
  {
    const engine e = build_town_engine();
    const wheels w = build_town_wheels();
    const doors d = build_town_doors();
    const steering_wheel s = build_town_steering_wheel();
    //fabrication des autres composants...

    car town_car(e, w, d, s); //une autre instance…

    town_car.price(28000);

    return town_car; //un autre point de sortie…
  }
}
En revanche, un léger réaménagement du code rendra son application possible :
const car
build_car(const bool sport_type = true)
{
  std::auto_ptr<engine> e;
  std::auto_ptr<wheels> w;
  std::auto_ptr<doors> d;
  std::auto_ptr<steering_wheel> s;

  if(sport_type)
  {
    e = std::auto_ptr<engine>(new engine(build_sport_engine()));
    w = std::auto_ptr<wheels>(new wheels(build_sport_wheels()));
    d = std::auto_ptr<doors>(new doors(build_sport_doors()));
    s = std::auto_ptr<steering_wheel>(new steering_wheel(build_sport_steering_wheel()));
    //fabrication des autres composants...
  }
  else
  {
    e = std::auto_ptr<engine>(new engine(build_town_engine()));
    w = std::auto_ptr<wheels>(new wheels(build_town_wheels()));
    d = std::auto_ptr<doors>(new doors(build_town_doors()));
    s = std::auto_ptr<steering_wheel>(new steering_wheel(build_town_steering_wheel()));
    //fabrication des autres composants...
  }

  car new_car(*e, *w, *d, *s); //une seule instance
  new_car.price(sport_type ? 75000 : 28000);

  return new_car; //un seul point de sortie
}

Quels impacts de la (N)RVO sur le passage d'arguments ?
Créé le 15/10/2009[haut]
auteur : Florian Goujeon
Tout bon cours sur le C++ vous dira qu'il est préférable de passer les objets aux fonctions par référence constante plutôt que par valeur. Il existe cependant un cas où cette assertion est fausse : celui où la (N)RVO s'en mêle.
Soit une fonction effectuant une copie, puis une suite de modifications d'une instance de voiture. Nommons simplement cette fonction copy_and_modify_car().
La fonction copy_and_modify_car() est appelée depuis la fonction main() de la façon suivante :
int
main()
{
  copy_and_modify_car(build_car()); //on passe directement le retour de la fonction build_car()
  std::cout << "Fin du programme.\n";
  return 0;
}
Commençons par écrire cette fonction de façon académique, en prenant une référence constante :
void
copy_and_modify_car(const car& const_c)
{
  car c(const_c);
  //modifier c...
}
La (N)RVO agissant, le programme produit la sortie suivante :
Appel au constructeur de car.
Appel au constructeur par copie de car.
Appel au destructeur de car.
Fin du programme.
Appel au destructeur de car.
Écrivons maintenant une nouvelle version de cette fonction, en passant l'instance par valeur :

void
copy_and_modify_car(car c)
{
  //modifier c...
}
Contre toute attente, aucune copie implicite n'est créée :
Appel au constructeur de car.
Fin du programme.
Appel au destructeur de car.
Le principe de la (N)RVO (la suppression des objets temporaires intermédiaires) est ici étendu au passage d'objet.
L'expression « build_car() » de la fonction main() est associée à une instance temporaire de voiture. Cet objet temporaire n'ayant de toute façon d'autre destin que d'être détruit une fois la copie implicite effectuée, le compilateur juge bon (à raison) de le passer directement à la fonction copy_and_modify_car() plutôt que d'en effectuer une copie.
Conséquence de ce raisonnement : l'utilisation d'une instance nommée (donc non-temporaire) empêche l'application de cette optimisation :
int
main()
{
  car c = build_car();
  copy_and_modify_car(c); //pas d'optimisation liée à la (N)RVO…

  // étant donné que c peut toujours être utilisé dans la suite du programme

  return 0;
}

(N)RVO et classes non copiables ?
Créé le 15/10/2009[haut]
auteur : Florian Goujeon
Il est important de noter que cette optimisation est implicite. Par conséquent, elle ne permet en aucun cas de retourner des objets d'un type défini comme faq non-copiable.
En effet, l'écriture d'une fonction retournant un tel objet mènerait à une erreur de compilation, même si en pratique aucune copie n'aurait été effectuée.
Heureusement, le prochain standard du langage C++ (nom de code C++0x) introduit la notion de sémantique de mouvement, permettant de passer outre cette limitation.


Qu'est-ce que le mot-clef volatile ?
Créé le 15/10/2009[haut]
auteur : dourouc05
Les variables volatiles sont des variables qui peuvent être modifiées par un processus hors de contrôle du compilateur. On doit accéder à une copie de la variable si elle a pu être modifiée.
Seules des fonctions membres volatiles peuvent être appelées pour des objets volatils, et des fonctions membres volatiles ne peuvent appeler que des autres fonctions membres volatiles. La viralité peut être détournée.
Le compilateur s'assure qu'aucune fonction membre n'ait d'optimisation quant aux références mémoire. En l'absence du mot-clef volatile, il peut optimiser le code de la fonction membre.
Cependant, la présence de ce mot-clef n'empêchera pas le processeur d'optimiser pour la cohérence des caches.
Utilisez ce mot-clef pour des variables que vous ne voulez pas voir optimisées par le compilateur.
Il est faux de penser que ce mot-clef apporte la moindre sécurité face au mutlithread. Il faut pour cela attendre le mot-clef atomic du C++0x.
Voici un petit exemple d'utilisation.
class Gadget
{
public:
void attendre()
{
while (! _drapeau)
{
dormir(1000); // attend 1000 ms
}
}
void seLever()
{
_drapeau = true;
}
...
private:
volatile bool _drapeau;
};
Cette classe est prévue pour attendre qu'un autre thread vienne modifier une de ses variables membres, et vérifier la situation toutes les secondes.
Si la variable n'avait pas été déclarée volatile, le compilateur aurait cru qu'il n'y a qu'un appel à une fonction externe qui ne peut modifier le drapeau. Il en aurait conclu qu'il peut mettre le drapeau en cache. Ceci fonctionne très bien quand il n'y a qu'un thread. Avec le mot-clef, le compilateur comprend que la variable peut être modifiée de l'extérieur, et ne la mettra pas en cache.

lien : Volatile - Multithreaded Programmer's Best Friend, par Andrei Alexandrescu

Qu'est-ce que l'immuabilité ?
Créé le 15/10/2009[haut]
auteur : white_tentacle
On parle d'immuabilité (ou non-mutabilité) d'un élément (au sens large) lorsque celui-ci ne change pas, tout au long de sa durée de vie. En C++, il faut distinguer plusieurs cas d'immuabilité :

  • immuabilité de membres ;
  • immuabilité des paramètres ;
  • fonctions membres ne modifiant pas l'objet ;
  • types immuables.
L'immuabilité en C++ se déclare au moyen du mot clé const.
Pour l'immuabilité d'un membre :
class C {
  int const TAILLE_MAX;
//[...]
};
Une fois affectée (dans le constructeur), TAILLE_MAX ne pourra plus être modifiée. const s'applique normalement à ce qui le précède (à sa gauche), et à défaut à ce qui le suit (à sa droite). On prendra donc l'habitude d'écrire int const plutôt que const int, même si les deux sont équivalents, et on comprendra alors plus facilement la différence entre int const * et int * const.
Pour l'immuabilité d'un paramètre :
int longueur(std::string const & chaine)
{
//[...]
}
Ici, on garantit à l'appelant qu'on ne modifie pas le paramètre qu'il nous donne (bien que celui-ci soit passé par référence). L'appelant peut se baser sur cet engagement lorsqu'il valide son algorithme.
Fonctions membres ne modifiant pas l'objet :
class chaine {
public:
int longueur() const;
[...]
};
Ici, de la même manière, on garantit à l'appelant que le paramètre this (l'objet courant) n'est pas modifié par cet appel. C'est à dire qu'aucun des membres de l'objet n'est modifié, l'objet reste dans le même état. Un corolaire intéressant de ceci est que (hors accès concurrents), deux appels successifs à une fonction membre const doivent donner le même résultat (c'est une garantie sémantique - apparentée à un contrat -, puisqu'en pratique, de nombreuses façons existent de la détourner).
Si un type ne possède que des fonctions membres ne modifiant pas l'objet, alors, il n'existe aucun moyen de modifier cet objet et il est dit immuable (ou non-mutable). Cette notion est moins importante en C++ que dans d'autres langages, du fait que n'importe quel objet mutable devient immuable par l'usage du mot clé const.
Il existe toutefois plusieurs restrictions :

  • Dans une fonction membre const, les membres pointeurs sont de type X* const, et non X const * const. Ceci fait qu'il est tout à fait légal d'écrire le code suivant :
    class A {
    int * m_val;
    public:
        int IncrementByOne() const { return  ++(*m_val); }
    };
    
    Pour justifier ce comportement, prenez l'exemple d'un smart_pointer. Il est logique qu'un smart_ptr<int> const se comporte de la même manière qu'un int * const, tandis qu'un smart_ptr<const int> se comportera comme un int const *.

  • Les membres déclarés avec le mot clé mutable ignorent les règles d'immuabilité imposées par const. Ceci permet des implémentations par compteur de références sur des objets const, par exemple.
  • Il est possible à tout moment, au moyen de const_cast, de forcer la mutabilité d'une variable immuable.
Il convient donc de se rappeler que const a avant tout valeur d'indication sémantique, et l'utiliser à bon escient. const bien utilisé facilite la maintenance du code (on sait ce qui change), ainsi que l'utilisation du code par un tiers (limitation des effets de bord). En revanche, un const volontairement détourné obfusque le code et rend complexe sa compréhension.
En règle générale, on utilisera donc const chaque fois que c'est possible, et on utilisera mutable et const_cast uniquement lorsque c'est absolument nécessaire et que ça n'induit pas de comportement contre-intuitif (fonction const qui aurait des effets de bord, par exemple...).



Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

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 © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.