Blog 5 min de lecture

Curieux de savoir où la sémantique de déplacement est utilisée automatiquement dans vos projets C++ ?

Share this article
Curieux de savoir où la sémantique de déplacement est utilisée automatiquement dans vos projets C++ ?

La sémantique de déplacement, introduite en C++11, permet de transférer plus efficacement les ressources (comme la mémoire dynamique) d'un objet à un autre. Elle remédie aux inefficacités liées à la copie profonde des objets, en particulier des plus volumineux, en permettant aux objets de « voler » les ressources d'autres objets lorsque c'est possible, plutôt que de les dupliquer.

Avant C++11, lorsque vous affectiez un objet à un autre, un constructeur de copie ou un opérateur d'affectation était invoqué, ce qui produisait une copie profonde des données de l'objet. Ce processus pouvait être coûteux, en particulier pour les gros objets ou ceux contenant des allocations de mémoire dynamique. Cette fonctionnalité est particulièrement utile dans les scénarios où l'optimisation des performances est critique, comme le calcul haute performance, le développement de jeux et les environnements aux ressources limitées.

La sémantique de déplacement introduit les références rvalue, ainsi que les constructeurs de déplacement et les opérateurs d'affectation par déplacement.

Voici comment cela fonctionne :

  1. Références rvalue (&&): les références rvalue permettent de se lier à des objets temporaires (rvalues), qui sont souvent des objets sur le point d'être détruits ou qui ne sont plus nécessaires. Elles sont notées par la double esperluette &&.
  2. Constructeurs de déplacement: un constructeur de déplacement est un constructeur spécial qui prend une référence rvalue vers un autre objet du même type et « déplace » les ressources de cet objet vers le nouvel objet. Il le fait en « volant » les pointeurs ou handles internes de l'objet source et en les annulant, laissant l'objet source dans un état valide mais non spécifié. Cela se fait typiquement à l'aide de la fonction std::move(), qui convertit une lvalue en rvalue.
  3. Opérateurs d'affectation par déplacement: similaires aux constructeurs de déplacement, les opérateurs d'affectation par déplacement permettent le transfert efficace de ressources entre objets. Ils prennent une référence rvalue vers un autre objet du même type et effectuent une opération d'affectation par déplacement, laissant l'objet source dans un état valide.

Cependant, faut-il maîtriser cette fonctionnalité pour bénéficier de ses avantages significatifs en performances ? Heureusement, dans de nombreux cas, le compilateur fait un excellent travail à votre place et utilise la sémantique de déplacement dès que possible.

Prenons cet exemple simple :

class test
{

};
test f()
{
    test t;
    return t;
}

Dans cet exemple, la fonctionnalité de déplacement n'est pas utilisée explicitement par le développeur, mais elle l'est implicitement par le compilateur. En effet, lorsqu'une fonction renvoie un objet par valeur, une copie de l'objet est traditionnellement effectuée, ce qui implique d'allouer de la mémoire pour la copie puis de copier le contenu de l'objet d'origine dans la mémoire nouvellement allouée. Cependant, avec la sémantique de déplacement, au lieu d'effectuer une copie, les ressources détenues par l'objet local de la fonction sont transférées à l'objet renvoyé, si possible.

Comment détecter où le compilateur utilise automatiquement cette fonctionnalité ?

1 - Avec la commande clang -cc1 -ast-dump

La commande de dump de l'arbre syntaxique abstrait (AST) de Clang est un outil puissant fourni par le front-end du compilateur Clang pour C, C++ et Objective-C. Elle permet aux développeurs d'inspecter la représentation interne de leur code source sous forme d'arbre syntaxique abstrait, une représentation hiérarchique de la structure du code source.

Exécutons cette commande sur notre exemple simple

|-CXXRecordDecl 0x1ccdf79b618 <test.cpp:1:1, line:4:1> line:1:7 referenced class test definition
| |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists trivial constexpr defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param implicit_has_const_param
| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| |-CXXRecordDecl 0x1ccdf79b738 <col:1, col:7> col:7 implicit class test
| |-CXXConstructorDecl 0x1ccdf888b58 <col:7> col:7 implicit used constexpr test 'void () noexcept' inline default trivial
| | `-CompoundStmt 0x1ccdf889098 <col:7>
| |-CXXConstructorDecl 0x1ccdf888cc8 <col:7> col:7 implicit constexpr test 'void (const test &)' inline default trivial noexcept-unevaluated 0x1ccdf888cc8
| | `-ParmVarDecl 0x1ccdf888de8 <col:7> col:7 'const test &'
| `-CXXConstructorDecl 0x1ccdf888ec8 <col:7> col:7 implicit used constexpr test 'void (test &&) noexcept' inline default trivial
|   |-ParmVarDecl 0x1ccdf888fe8 <col:7> col:7 'test &&'
|   `-CompoundStmt 0x1ccdf8892a0 <col:7>
`-FunctionDecl 0x1ccdf8889d8 <line:5:1, line:9:1> line:5:6 f 'test ()'
  `-CompoundStmt 0x1ccdf8892f8 <line:6:1, line:9:1>
    |-DeclStmt 0x1ccdf8891f0 <line:7:5, col:11>
    | `-VarDecl 0x1ccdf888ad8 <col:5, col:10> col:10 used t 'test':'test' nrvo callinit
    |   `-CXXConstructExpr 0x1ccdf8891c8 <col:10> 'test':'test' 'void () noexcept'
    `-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
      `-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
        `-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
          `-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'

Comme on peut le voir, le compilateur génère le constructeur de déplacement et l'opérateur d'affectation par déplacement pour la classe test

| | |-MoveConstructor exists simple trivial
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit

Pour l'instruction return de la fonction f(), le constructeur de déplacement est appelé.

 `-ReturnStmt 0x1ccdf8892e0 <line:8:5, col:12>
      `-CXXConstructExpr 0x1ccdf8892b0 <col:12> 'test':'test' 'void (test &&) noexcept'
        `-ImplicitCastExpr 0x1ccdf889228 <col:12> 'test':'test' xvalue <NoOp>
          `-DeclRefExpr 0x1ccdf889208 <col:12> 'test':'test' lvalue Var 0x1ccdf888ad8 't' 'test':'test'

Grâce à la norme C++11, les compilateurs modernes utilisent la fonctionnalité de déplacement dans tous les cas où son emploi est trivial, apportant une valeur ajoutée en performances par rapport aux compilateurs C++ antérieurs à la norme C++11.

2 - Avec CppDepend

CppDepend utilise Clang en interne pour analyser le code source, parcourir l'AST et obtenir le maximum de données utiles nécessaires au calcul des métriques, et détecter la structure et les dépendances de votre base de code.

Pour la classe test, on peut voir le constructeur de déplacement généré dans l'explorateur de classes de CppDepend :

Pour voir où il est utilisé, il suffit de faire un clic droit sur le constructeur de déplacement et de sélectionner les méthodes qui l'utilisent

Voici le résultat :

Comme on peut le voir, la fonction f() utilise implicitement le constructeur de déplacement.

CppDepend récupère l'AST de Clang et le convertit en un modèle adapté que nous pouvons interroger à l'aide de notre langage de requête de code :

Si vous êtes curieux de savoir où la sémantique de déplacement est utilisée dans vos projets C++, vous pouvez employer l'une de ces deux approches pour mieux connaître votre base de code.

Share this article