jeudi 16 janvier 2014

C++, "copy elision" et copy-initialization

What the hell are these ?

Prenons une classe C++ ordinaire définissant un constructeur à un argument, un constructeur par copie et un destructeur, soit :

#include <iostream>
using namespace std;

class MaClasse {
public:
  MaClasse(int i) {
    cout << "ctor " << this << " avec i=" << i << endl;
  }
  MaClasse(const MaClasse &m) {
    cout << "ctor " << this << " avec " << &m << endl;
  }
  ~MaClasse() {
    cout << "dtor " << this << endl;
  }
};

int main() {
  MaClasse c1(1);
  MaClasse c2 = 2;

  cout << "fin" << endl;
  return 0;
}

Compilons ordinairement avec gcc (par exemple), puis exécutons :

% g++ -o test test.cpp
% ./test
ctor 0x7fff52a7f9d8 avec i=1
ctor 0x7fff52a7f9d0 avec i=2
fin
dtor 0x7fff52a7f9d0
dtor 0x7fff52a7f9d8
%

Ceci donne à penser qu'on obtient une construction ordinaire dans les deux cas, en utilisant le fait qu'on a à faire à la syntaxe d'initialisation traditionnelle, e.g. int i=4, et fonctionnelle, e.g. int i(4), sachant que l'existence d'un constructeur à un argument autorise (soit-disant) cette syntaxe d'initialisation par uniformité syntaxique.

En réalité, la sémantique est plus complexe (désormais?) puisque si cette fois on compile de la façon suivante, on obtient :

% g++ -fno-elide-constructors -o test test.cpp
% ./test
ctor 0x7fff4fce09d8 avec i=1
ctor 0x7fff4fce09c8 avec i=2
ctor 0x7fff4fce09d0 avec 0x7fff4fce09c8
dtor 0x7fff4fce09c8
fin
dtor 0x7fff4fce09d0
dtor 0x7fff4fce09d8
%

Où l'on observe qu'un objet temporaire (celui d'adresse 0x7fff4fce09c8) a été créé à l'aide d'un constructeur à un argument (avec la valeur 2), puis que cet objet temporaire a été utilisé dans la construction par copie pour l'objet désiré in-fine. L'objet temporaire étant alors immédiatement détruit.

Le mécanisme est dit de copy elision, puisque, par défaut dans gcc, on élimine cette copie inutile (création d'un objet temporaire, copie, puis suppression) afin d'éviter des constructions/destructions trop nombreuses (la plaie de la programmation objet mal contrôlée).
Bien entendu, ce mécanisme est employé élégamment lors des passages d'arguments de type objet et retour de fonctions afin d'éviter les copies lors de passage par valeur (la plaie du C++).

Néanmoins, il me semble que le mécanisme ne devrait pas être visible pour les initialisations, si effectivement on utilisait une simple uniformité syntaxique de sorte que O o(v) serait syntaxiquement équivalent à O o=v. Ce que l'expérience prouve être faux (pour s'en convaincre encore il suffit de qualifier le constructeur à un argument d'explicit, ce qui a pour effet de provoquer une erreur de compilation dans les deux cas).

Dans l'expérience, il semble donc que l'opérateur d'initalisation = soit traité à la manière de l'opérateur d'affectation =, c'est-à-dire que le compilateur tente d'effectuer une conversion de la valeur vers une valeur du type de l'objet à affecter, puis réaliser l'affectation; ceci dans le cadre d'une initialisation, d'où le refus lorsqu'explicit est employé.
Or initialisation et affectation sont a-priori des opérations bien différentes et pourraient être traitées comme telles. Ici, privilège est donné à la construction par copie, à laquelle une élision est appliquée, d'ailleurs :

La norme C++11 précise l'existence de trois syntaxes d'initialisation :
  1. T x(a)
  2. T x = a
  3. T x{a}
Note: pour le troisième cas, prière de compiler avec -std=c++11.

D'après §8.5 de la norme ISO/IEC JTC1 SC22 WG21 N 3690, les cas 1 et 3 sont appelés direct-initialization, signifiant ainsi qu'aucun objet temporaire n'est requis.
Le cas 2 est appelé copy-initialization, signifiant que l'objet x est normalement créé par copie d'un objet de type compatible; mais que la copy elision permet d'éliminer (voir §12.8).

L'enfer se cache dans les détails…
Il n'existe pas d'équivalence syntaxique ou sucre syntaxique (du moins pour les types objets) entre initialisation avec liste à un argument et initialisation avec opérateur =, pas d'équivalence opérationnelle non plus d'ailleurs (sauf dans le cas de l'optimisation par élision), il n'y a qu'une équivalence sémantique (dénotationnelle) entre initialisation avec liste d'argument à un argument et initialisation avec opérateur =.
Djibee

Wanna play with direct and copy initialization ? See Herb Sutter problem #36.

Aucun commentaire:

Enregistrer un commentaire