Du bon usage du using namespace

Utiliser correctement les using namespace en C++

Je vois très souvent la notion de namespace, mal utilisée. J’explique souvent sur le forum, la raison pour laquelle il faut absolument proscrire des écritures du genre: using namespace. Voici un petit tutoriel exhaustif sur la raison pour laquelle je déconseille fortement ce genre de pratique.

Dans ce tutoriel, je vais aborder la notion d’espace de nom. En effet, je vois très souvent des programmeurs C++, mal les utiliser. Entrons directement dans le vif du sujet: n’utilisez pas using namespace, jamais ! Je vais découper ce tutoriel en trois parties:

  • Rappel.
  • Pourquoi ne pas l’utiliser.
  • Par quoi remplacer cette clause.

I - Rappel

1) Les espaces de noms

Tout d’abord, un rappel sur ce qu’est un espace de nom. Un espace de nom est, comme son nom l’indique, un espace restreint ou l’on peut entreposer des fonctions ou variables, sans craindre de conflits. En C, lorsque l’on veut créer une nouvelle fonction, on doit s’assurer qu’elle n’est pas en conflit avec une autre, et donc lui donner un nom différent le cas échéant.

Exemple:

void system_display(void);
void my_display(void);

Pourquoi ne pas leur donner le même nom ? Après tout même si une fonction existe déjà, il est légitime de pouvoir choisir le nom qui nous plait. C’est pour résoudre cette problématique que les espaces de noms ont été inventés.

Exemple:

namespace system
{
  void display(void);
}
namespace my
{
  void display(void);
}

Ici, on voit bien que le problème est résolu de manière plus élégante, puisque le nom de la fonction n’a pas été modifiée à cause de contraintes techniques. À l’utilisation il suffit juste de préciser l’espace de nom.

Exemple:

system::display();
my::display();

Après ce bref rappel, passons maintenant à la problématique énoncée.

2) Propreté et clarté du code, le cas de std

Il faut se rappeler que les fonctions standard du C++, sont codées par des gens (doués) et que cet espace de nom leur est réservé. En tapant, using namespace std;, cela revient presque à faire:

namespace std
{
  // Code
  int main()
  {
    // Code
    return 0;
  }
}

C’est presque, et je dis bien presque, comme si vous codiez directement dans l’espace std ! Autant dire que vous n’avez rien à faire ici. D’un point de vue propreté, ce n’est pas ça. Enfin, d’un point de vue clarté du code, le namespace est une information souvent utile, puisqu’elle permet d’un coup d’oeil de connaître le contexte de ce qu’on utilise. Si je vois “std::“, je sais immédiatement que j’appel quelque chose de standard du C++. De même, si je vois “boost::“, je sais que j’utilise du Boost.

Enfin, pourquoi vouloir faire “sauter” cette fonctionnalité fort pratique ? Certains me répondrons on gagne 5 caractères. Oui, mais à quelle prix ? Êtes-vous vraiment à 5 caractères près ? Je rappelle quand même que si l’espace de nom “std” se nomme comme cela, et pas “standard”, c’est qu’il y avait une raison… Mais bon soit, gardons l’argument, et voyons ce qu’il peut en couter.

II - Les dangers du “using namespace”, ou pourquoi ne pas l’utiliser

Cette clause, utilisée dans le but de gagner du temps, peut en réalité en faire perdre énormément. Nous allons voir plusieurs problèmes:

1) Les conflits de namespace, ou comment s’arracher les cheveux

Prenons simplement le code suivant:

#include <iostream>
#include <cmath>

double abs(double)
{
  return -1;
}

int main()
{
  std::cout << abs(-4.5) << std::endl;
  std::cout << std::abs(-4.5) << std::endl;

  return 0;
}

Ici, pas de surprise. Le code compile bien, et fait ce que l’on veut. Maintenant, la version avec des using namespace:

#include <iostream>
#include <cmath>

using namespace std;

double abs(double)
{
  return -1;
}

int main()
{
  cout << abs(-4.5) << endl;
  cout << std::abs(-4.5) << endl;

  return 0;
}

On aura ici des problèmes de compilation. En effet, la fonction abs, entre en conflit avec la fonction std::abs. L’erreur n’est pas très dur à corriger, il suffit d’indiquer ce que l’on veut utiliser, explicitement (écrire ::abs). Maintenant, imaginez que vous récupériez une bibliothèque réalisée par un tiers. Si vous faite “sauter” les namespaces, vous risquez fort de vous arracher les cheveux à cause des problèmes de compilation. Dites-vous bien qu’ici, c’est un exemple très simple de problème induit par l’utilisation de cette clause. Sur de gros projets, ce type d’erreur peut être bien plus difficile à déceler.

2) Le masquage de nom, source d’erreur

Prenons le code suivant:

#include <iostream>

int min(float, float)
{
  std::cout << "Enter min" << std::endl;
  return -1;
}

int main()
{
  std::cout << min(45.6, 5) << std::endl;
  std::cout << min(45, 3) << std::endl;
  return 0;
}

Ce code, tout simple, affichera:

Enter min
-1
Enter min
-1

En effet, on s’attend bien évidemment à rentrer dans la fonction “min” que l’on a crée, et qui retourne toujours -1.

Réalisons le même code, avec une clause using namespace:

#include <iostream>

using namespace std;

int min(float, float)
{
  cout << "Enter min" << endl;
  return -1;
}

int main()
{
  cout << min(45.6, 5) << endl;
  cout << min(45, 3) << endl;
  return 0;
}

Vous vous attendez au même résultat ? Eh bien non !

Le résultat sera:

Enter min
-1
3

C’est complètement inattendu, mais au final tout à fait logique. En effet, la fonction std::min existe ! En faisant sauter les espaces de noms, on a implicitement déclenché l’utilisation de cette fonction. Puisqu’on n’utilise des entiers, et non des “float”, le compilateur a pensé qu’il serait judicieux d’utiliser la fonction std::min qui est plus adapté à prendre des arguments “int, int”.

Aucune erreur ne vous est montré, ni aucun avertissement puisque c’est du C++ tout à fait valide, techniquement. Ce que j’essaie ici de vous démontrer, c’est que vous ne pouvez pas connaître tout ce qu’il y a dans l’espace de nom std. D’une part parce qu’il y a beaucoup de chose, d’autre part, parce que le C++ est un langage qui évolue, et il est fortement possible que des choses soient ajoutées dans le futur. Votre code risque d’être potentiellement incompatible avec de futures versions du C++, si vous utilisez mal les namespaces. Bien évidemment, certains me rétorqueront que je pourrais écrire ::min au lieu de min, ce qui résoudrait le problème. C’est tout à fait exact, mais honnêtement, combien de temps passerez-vous à découvrir, sur un projet d’envergure, un problème de ce genre ?

Dans le même ordre d’idée, le masquage peut se faire de l’autre sens:

Exemple:

#include <iostream>
int main()
{
  int hex = 0;
  std::cout << std::hex << 42 << std::endl;

  return 0;
}

Ce code affichera bien entendu: 2a. Maintenant la version avec la clause using namespace:

#include <iostream>
using namespace std;
int main()
{
  int hex = 0;
  cout << hex << 42 << endl;

  return 0;
}

Ce code affichera: 0. En effet, le compilateur ne sait pas s’il doit prendre std::hex ou la variable hex. Il prend le plus “proche”, c’est-à-dire la variable hex. On a ici un masquage. Pour le résoudre, rien de plus simple, il suffit de mettre std::hex.

3) La pire chose à faire

Utiliser une clause using namespace peut avoir des impacts limités dans un fichier de code, mais si vous l’indiquer dans un fichier header, alors les impacts peuvent être catastrophiques. En effet, si vous développez en équipe, ou si vous créez une bibliothèque que vous allez distribuer, toute personne qui inclura votre travail, verra ses espaces de noms “sauter” alors qu’elle n’a rien demandé !

III - Par quoi remplacer la clause using namespace ?

Maintenant que l’on a vu, pourquoi ne jamais utiliser de clause “using namespace”, voyons comment remplacer celle-ci de manière intelligente.

Pour reprendre à contre-pied ma propre argumentation, il est vrai qu’il est parfois plus lisible de lire un code qui n’est pas préfixé en permanence. C’est notamment le cas de certaines bibliothèques comme boost::spirit, qui émule une grammaire BNF, de la manière la plus visuelle possible. Il serait franchement désagréable d’avoir à indiquer des espaces de noms devant chacune des règles, puisque ça briserait la tentative de ressemblance. Ce n’est qu’un exemple parmi d’autre, et nous allons voir comment y remédier avec élégance.

1) Aliasing

Dans le cas de préfixe de nom trop long, il est possible de faire un “alias”. C’est-à-dire, de faire une sorte de “typedef” sur un espace de nom. Un namespace n’étant pas un type, mais un mécanisme à part, la syntaxe est bien évidemment différente.

Si je prends l’exemple suivant:

#include <iostream>
int main()
{
  std::cout << "Hello World!" << std::endl;

  return 0;
}

Rien ne m’empêche de créer un alias sur std, pour le raccourcir:

#include <iostream>

namespace s = std;

int main()
{
  s::cout << "Hello World!" << s::endl;

  return 0;
}

Cette technique est beaucoup utilisée avec Boost, qui possède parfois de noms de namespace à rallonge.

Exemple:

namespace po = boost::program_options;
void usage(const po::options_description& desc)
{
  std::cout << "Version: " << desc;
}

2) Une portée limitée

L’une des solutions envisageables, est l’utilisation de la clause using namespace, dans une portée limitée. C’est-à-dire, à l’intérieur même d’une fonction, ou d’un namespace.

Exemple:

#include <iostream>

namespace
{
  using namespace std; // Effectif uniquement dans le namespace anonyme

  void hello()
  {
    cout << "kikoo" << endl;
  }

  void pouet()
  {
    cout << "pouet" << endl;
  }
}

void test()
{
  using namespace std; // Effectif uniquement dans la fonction test

  cout << "pouet" << endl;
}

int main()
{
  // Pas affecté par le using namespace std;
  std::cout << "Hello World!" << std::endl;

  hello();
  pouet();
  test();

  return 0;
}

Malheureusement, cette technique ne permet pas de résoudre tous les problèmes. Une méthode plus rigoureuse est nécessaire, et c’est l’objet de mon prochain point.

3) Une portée contrôlée

Reprenons l’un des problèmes déjà énoncé en mettant la clause dans une portée limitée:

#include <iostream>

int min(float, float)
{
  using namespace std;
  cout << "Enter min" << endl;
  return -1;
}
int main()
{
  using namespace std;
  cout << min(45.6, 5) << endl;
  cout << min(45, 3) << endl;
  return 0;
}

Le problème n’est toujours pas résolu. Ici, le problème n’est pas dû à une portée, mais à un choix non sélectif fait par le programmeur. La solution est tout simplement de choisir explicitement ce que l’on veut importer:

#include <iostream>

using std::cout;
using std::endl;

int min(float, float)
{
  cout << "Enter min" << endl;
  return -1;
}
int main()
{
  cout << min(45.6, 5) << endl;
  cout << min(45, 3) << endl;
  return 0;
}

Le problème est maintenant résolu, puisque nous n’avons pas explicitement autorisé le remplacement de min par std::min. Ici, nous voyons bien que la déclaration contrôlée de chacun des symboles, permet de ne pas créer de problème dû à une “importation sauvage” des symboles de std.

4) Une portée limitée et regroupée

Le seul défaut de cette technique, qui n’en est pas vraiment un, est qu’il est parfois plus long d’écrire un à un, chacun des symboles que l’on ne veut plus préfixer. C’est souvent par flemme que les programmeurs utilisent “using namespace”. Pourtant cette raison n’est pas valide: il est possible d’avoir le beurre et l’argent du beurre ! En effet, il est tout à fait possible de regrouper les directives sous l’égide d’une seule, afin de n’écrire qu’une seule ligne.

Exemple:

#include <iostream>

namespace mystd // Peut être mis dans un header
{
  using std::cout;
  using std::endl;
}

int min(float, float)
{
  using namespace mystd;
  cout << "Enter min" << endl;
  return -1;
}

int main()
{
  using namespace mystd;
  cout << min(45.6, 5) << endl;
  cout << min(45, 3) << endl;
  return 0;
}

Le petit problème de cette technique, est qu’elle ne permet pas d’éviter toutes les erreurs.

5) Une portée limitée et contrôlée

Reprenons un exemple déjà vu plus haut, en utilisant la technique de la portée limitée.

#include <iostream>

int main()
{
  using namespace std; // ou using namespace mystd; comme vu plus haut,
                       //mais avec using std::hex en plus dedans
  int hex = 0;
  cout << hex << 42 << endl;

  return 0;
}

Ici, on a bien le using namespace qui n’est actif que dans la fonction main, mais il n’empêche malheureusement pas le masquage d’argument. Il aurait été sympathique de la part du compilateur, de nous dire que la variable hex risque de créer un conflit.

Fort heureusement, ce système existe. Plutôt que d’utiliser la clause using namespace, préférez la clause “using simple” en portée limitée. Je ne peux que vous conseiller d’utiliser cette technique, sans utiliser le regroupement vu plus haut. L’utilisation de cette technique, vous permettra d’avoir un contrôle fin sur ce que vous faites. Appliquons ceci à notre fameux exemple:

#include <iostream>

int main()
{
  using std::hex;
  using std::cout;
  using std::endl;

  int hex = 0;
  cout << hex << 42 << endl;

  return 0;
}

Et là, miracle, le compilateur nous prévient:

 error: ‘int hex’ redeclared as different kind of symbol

Cette technique présente donc l’avantage de nous prévenir en cas de masquage d’argument.

IV) Conclusion

Je vous ai détaillé la raison pour laquelle l’utilisation du using namespace est à proscrire absolument. J’espère que grâce à ce tutoriel, vous utiliserez les namespaces avec un peu plus de contrôle. Le gros problème étant que certaines mauvaises utilisations sont très présentes sur le net, voir même en salle de cours !

 
comments powered by Disqus