Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

initialisation par retour de fonction

9 réponses
Avatar
Yoxoman
Bonjour à tous.

Lors d'une initialisation d'un objet à l'aide d'un retour de fonction,
j'ai remarqué que (sous gcc), ni le constructeur de copie, ni
l'opérateur d'affectation n'étaient invoqués. Il y a (je crois) juste un
"branchement" entre la variable crée dans le corps de la fonction et le
nouvel objet.

En fait, je voudrais savoir si ce comportement est standard, ou si c'est
une optimisation du compilateur.
Dans tous les cas, ce type de declaration n'est-il pas à proscrire,
étant donné qu'on perd le contrôle de l'opérateur = ?

Ci-joint mon bout de code :

#include <iostream>
using namespace std;

class X
{
public:
int i;

X() {cout << "constructeur\n";}
X(int a) : i(a) {cout << "constructeur int\n";}
X(const X& x) : i(x.i) {cout << "constructeur copie\n";};

X f(int a);

X &operator=(const X &x) {cout << "affectation\n"; i = x.i; return
*this;}
};

X X::f(int a)
{
X x(a);
return x;
}

int main()
{
X x1(1); // appel au constructeur (entier)

X x2 = x1; // appel au constructeur par copie

X x3(2); // appel au constructeur (entier)
x3 = x1; // appel a l'operateur d'affectation

X x4 = x1.f(3); // appel a... rien
}

--
"Yo!"
Martin Heidegger

9 réponses

Avatar
Fabien LE LEZ
On Thu, 3 Nov 2005 19:12:17 +0100, Yoxoman :

En fait, je voudrais savoir si ce comportement est standard, ou si c'est
une optimisation du compilateur.


C'est, si je ne m'abuse, une optimisation explicitement autorisée par
la norme.

Dans tous les cas, ce type de declaration n'est-il pas à proscrire,
étant donné qu'on perd le contrôle de l'opérateur = ?


Ce qui AMHA est franchement à proscrire, c'est un constructeur de
copie qui fasse autre chose que ce qu'on attend de lui.

En d'autres termes, si cette optimisation pose problème dans un code
réel, c'est a priori que ton constructeur de copie ne se comporte pas
correctement.

Avatar
Arnaud Debaene
Yoxoman wrote:
Bonjour à tous.

Lors d'une initialisation d'un objet à l'aide d'un retour de fonction,
j'ai remarqué que (sous gcc), ni le constructeur de copie, ni
l'opérateur d'affectation n'étaient invoqués. Il y a (je crois) juste
un "branchement" entre la variable crée dans le corps de la fonction
et le nouvel objet.

En fait, je voudrais savoir si ce comportement est standard, ou si
c'est une optimisation du compilateur.
C'est autorisé spécifiquement par la norme (12.8/15). C'est la "NRVO" (Named

Return Value Optimization).

Dans tous les cas, ce type de declaration n'est-il pas à proscrire,
étant donné qu'on perd le contrôle de l'opérateur = ?
Effectivement, cette optimisation peut entrainer des changements de

comportement si le constructeur par copie et/ou l'operator= ont des effets
de bords (ce qui devrait être l'exception plutôt que la règle).

Arnaud

Avatar
kanze
Yoxoman wrote:

Lors d'une initialisation d'un objet à l'aide d'un retour de
fonction, j'ai remarqué que (sous gcc), ni le constructeur de
copie, ni l'opérateur d'affectation n'étaient invoqués. Il y a
(je crois) juste un "branchement" entre la variable crée dans
le corps de la fonction et le nouvel objet.

En fait, je voudrais savoir si ce comportement est standard,
ou si c'est une optimisation du compilateur.


C'est une optimisation explicitement permise par la norme, même
dans le cas où il y a changement dans le comportement visuel du
programme.

Dans tous les cas, ce type de declaration n'est-il pas à
proscrire, étant donné qu'on perd le contrôle de
l'opérateur= ?


D'abord, l'opérateur d'affectation n'entre en aucun cas en jeu ;
c'est le constructeur de copie qui est supprimé. La philosophie
derrière l'autorisation dans la norme, c'est un constructeur de
copie doit copier, et pas faire d'autres chose ; en général, il y
a beaucoup d'endroits où la norme donne des libertés en ce qui
concerne le nombre de copies.

Ci-joint mon bout de code :

#include <iostream>
using namespace std;

class X
{
public:
int i;

X() {cout << "constructeurn";}
X(int a) : i(a) {cout << "constructeur intn";}
X(const X& x) : i(x.i) {cout << "constructeur copien";};

X f(int a);

X &operator=(const X &x) {cout << "affectationn"; i = x.i; return
*this;}
};

X X::f(int a)
{
X x(a);
return x;
}

int main()
{
X x1(1); // appel au constructeur (entier)

X x2 = x1; // appel au constructeur par copie

X x3(2); // appel au constructeur (entier)
x3 = x1; // appel a l'operateur d'affectation

X x4 = x1.f(3); // appel a... rien


Mais si. Il doit bien y avoir un appel au constructeur (entier).
Dans la fonction f. Ce qu'il y a, en fait, c'est une fusion
entre la variable locale dans f, et x4 (et la valeur de rétour
de la fonction, évidemment, un temporaire qui logiquement ne
peut être ni la variable locale dans la fonction, ni x4).

}


La norme permet à l'implémentation de fusionner certains objets
(variables, temporaires), dans certaines conditions. Les règles
formelles sont assez compliquées (et je sais qu'il y a eu pas
mal de problèmes à trouver une formulation qui permettait tout
ce qu'on voulait permettre, mais rien de ce qu'on ne voulait pas
permettre). En gros, l'idée est que si, en tant qu'utilisateur,
tes constructeurs de copie ne font que copier, sans effets de
bord, et que l'identité de l'objet n'a pas d'importance, tu ne
dois pas pouvoir s'apperçois de l'optimisation.

Dans ton cas, tu pourrais aussi essayer de sortir l'adresse de x
dans f, et l'adresse de x4. S'il n'y a vraiment aucun appel au
constructeur de copie, elles doivent être identique. (En
revanche, ce n'est pas impossible que le fait de prendre
l'adresse de x, dans f(), déclenche la suppression de
l'optimisation. L'optimisation est encore permise, mais aucune
optimisation n'est exigée, et selon l'humeur de l'auteur du
compilateur, on peut le faire ou non.)

En gros, cette règle introduit un certain indeterminisme dans la
sémantique précise du code. En revanche, elle peut avoir un
effet positif très notable sur les temps d'exécution, et
l'indeterminisme ne doit pas être visible dans un programme bien
écrit.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
Yoxoman

En gros, l'idée est que si, en tant qu'utilisateur,
tes constructeurs de copie ne font que copier, sans effets de
bord, et que l'identité de l'objet n'a pas d'importance, tu ne
dois pas pouvoir s'apperçois de l'optimisation.


C'est dommage, j'ai l'impression que c'est une réelle restriction du
C++. Finalement, les concepteurs de la norme ont décidé que le
constructeur de copie doit faire ce qu'il est censé faire. Sous pretexte
qu'un programme bien écrit doit le faire.

On se retrouve dans la position du Java : conçu après le C++, il s'est
attaché à supprimer les fonctionnalités qui peuvent causer désordre dans
un programme. Pour arriver AMHA à un langage évolué plutôt fade, mais
productif. Sans grand intérêt pour un développeur enthousiaste, mais
d'un grand intérêt pour une société en cherche de profit.

Comment être sûr que le constructeur de copie ne pourrait être employé
quelque part, dans un programme joyeusement atypique, à autre chose ?
Arriver à un bout de code, une idée, sur lesquels on pourrait s'extasier
"Waou, c'est joli ça". Et hop, sous une contrainte de portabilité, les
rêves s'envolent. Je caricature, mais bon...

--
"Yo!"
Martin Heidegger

Avatar
Marc Boyer
Yoxoman a écrit :
En gros, l'idée est que si, en tant qu'utilisateur,
tes constructeurs de copie ne font que copier, sans effets de
bord, et que l'identité de l'objet n'a pas d'importance, tu ne
dois pas pouvoir s'apperçois de l'optimisation.


C'est dommage, j'ai l'impression que c'est une réelle restriction du
C++. Finalement, les concepteurs de la norme ont décidé que le
constructeur de copie doit faire ce qu'il est censé faire. Sous pretexte
qu'un programme bien écrit doit le faire.

On se retrouve dans la position du Java : conçu après le C++, il s'est
attaché à supprimer les fonctionnalités qui peuvent causer désordre dans
un programme. Pour arriver AMHA à un langage évolué plutôt fade, mais
productif. Sans grand intérêt pour un développeur enthousiaste, mais
d'un grand intérêt pour une société en cherche de profit.


C'est à dire que C++ serait un intrument de travail et pas un
jouet ?

Comment être sûr que le constructeur de copie ne pourrait être employé
quelque part, dans un programme joyeusement atypique, à autre chose ?


On ne peut pas. D'ailleurs, il est facile de faire autre chose.

Arriver à un bout de code, une idée, sur lesquels on pourrait s'extasier
"Waou, c'est joli ça". Et hop, sous une contrainte de portabilité, les
rêves s'envolent. Je caricature, mais bon...


Si un jour, quelqu'un trouve un *gros* désavantage à cette
optimisation, il ne sera pas très couteux aux fabriquants de
compilateur de l'inhiber.
Pour le moment, il me semble que c'est plutôt le contraire.

Marc Boyer
--
À vélo, prendre une rue à contre-sens est moins dangereux
que prendre un boulevard dans le sens légal. À qui la faute ?


Avatar
Anthony Fleury

Bonjour,

Comment être sûr que le constructeur de copie ne pourrait être employé
quelque part, dans un programme joyeusement atypique, à autre chose ?
Arriver à un bout de code, une idée, sur lesquels on pourrait s'extasier
"Waou, c'est joli ça". Et hop, sous une contrainte de portabilité, les
rêves s'envolent. Je caricature, mais bon...


Dans ce cas, si l'on veut vraiment employer le constructeur de copie
pour autre chose que son but initial, il faut en effet se débarasser de
la portabilité. Ces cas sont, à mon avis, vraiment minoritaires (que ce
soit le cas où c'est vraiment utile, ou alors le cas dans lequel ca fait
vraiment fun). Dans ce cas, on connait à mon avis le compilateur cible,
et rien n'empêche dans son makefile d'empêcher le compilateur de
s'amuser à optimiser. Si jamais il est bien écrit (cas de tous les
compilateurs que j'ai cottoyé), alors on pourra l'empêcher de faire ce
qu'il veut avec nos constructeurs.

Exemple pour notre ami gcc :

man g++
GCC(1) GNU Tools

GCC(1)

NAME
gcc, g++ - GNU project C and C++ Compiler (gcc-3.2.1)

SYNOPSIS
gcc [ option | filename ]...
g++ [ option | filename ]...

[...]

-felide-constructors
Elide constructors when this seems plausible (C++ only). With
this flag, GNU C++ initializes y directly from the call to foo
without going through a temporary in the following code:

A foo (); A y = foo ();

Without this option, GNU C++ first initializes y by calling the
appropriate constructor for type A; then assigns the result of
foo to a temporary; and, finally, replaces the initial value of
`y' with the temporary.

The default behavior (`-fno-elide-constructors') is specified by
the draft ANSI C++ standard. If your program's constructors
have side effects, using `-felide-constructors' can make your
program act differently, since some constructor calls may be
omitted.

more testNRVO.cpp
#include <iostream>


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

class A {
public:
A() { cout << "Constructor" << endl;}
A(const A&) { cout << "Copy Constructor" << endl;}
};

A f() {
A b;
return b;
}

int main() {
A a = f();
return 0;
}
g++ testNRVO.cpp
./a.out
Constructor

g++ -fno-elide-constructors testNRVO.cpp
./a.out
Constructor

Copy Constructor
Copy Constructor



On voit d'ailleurs que le default behavior a changé entre ma version de
la doc et ma nouvelle version de gcc :-)

Dans le cas de VC++, un /Od envoyé à cl suffit à faire taire cette
optimisation. (j'ai pas de VC++ chez moi pour la démo, donc j'ai préféré
g++)

--
Anthony Fleury

Avatar
James Kanze
Yoxoman wrote:

En gros, l'idée est que si, en tant qu'utilisateur, tes
constructeurs de copie ne font que copier, sans effets de
bord, et que l'identité de l'objet n'a pas d'importance, tu ne
dois pas pouvoir s'apperçois de l'optimisation.



C'est dommage, j'ai l'impression que c'est une réelle
restriction du C++. Finalement, les concepteurs de la norme
ont décidé que le constructeur de copie doit faire ce qu'il
est censé faire. Sous pretexte qu'un programme bien écrit doit
le faire.


Et alors... Où est le problème ? C'est une restriction, certes,
mais en fin de compte, c'est une restriction qu'on s'imposera de
toute façon à soi-même.

Note qu'il n'est pas interdit d'avoir des constructeurs de copie
un peu spéciaux -- déjà, dans la norme, il y a std::auto_ptr.
Ce qui est interdit, c'est de supporter la « copie » tout en
faisant dépendre la correction du programme du nombre de fois
qu'il a lieu.

On se retrouve dans la position du Java : conçu après le C++,
il s'est attaché à supprimer les fonctionnalités qui peuvent
causer désordre dans un programme.


Le problème de Java, c'est qu'il a supprimer des fonctionnalités
qui sont nécessaires pour éviter le désordre. Ce n'est pas la
même chose.

Pour arriver AMHA à un langage évolué plutôt fade, mais
productif. Sans grand intérêt pour un développeur
enthousiaste, mais d'un grand intérêt pour une société en
cherche de profit.


Comment être sûr que le constructeur de copie ne pourrait être
employé quelque part, dans un programme joyeusement atypique,
à autre chose ?


Par exemple ? Je dois payer une perte d'optimisation importante
pour supporter une éventuelle utilisation que personne n'a même
pas envisagée, et qui de toute façon donnera un code illisible
(parce que menteur).

Si on l'appelle constructeur de copie, il y a bien une raison.
C'est un constructeur très spécial, appelé implicitement dans
bien des cas. Et la norme permet de le supprimer dans beaucoup
de cas, non seulement ici. Par exemple :

struct S { T( T const& ) ; T( int ) ; } ;

S s = 1 ;

Ici, la sémantique formelle est de construire un T temporaire à
partir de l'int, puis de le copier dans s, pour ensuite le
detruire. Mais la norme permet la fusion du temporaire et s,
avec suppression de la copie (et la destruction du temporaire),
et tous les compilateurs que je connais font cette optimisation.
Interdire-la, et tu vas avoir pas mal de programmeurs fâchés à
cause de la perte de performances.

Si tu veux insister qu'on objet existe, il suffit de lui donner
un nom. Si tu veux s'assurer qu'un constructeur est appelé, il
suffit de ne pas le faire un constructeur de copie, en lui
donnant un paramètre bidon, par exemple.

Arriver à un bout de code, une idée, sur lesquels on pourrait
s'extasier "Waou, c'est joli ça". Et hop, sous une contrainte
de portabilité, les rêves s'envolent. Je caricature, mais
bon...


J'aimerais d'abord voir l'exemple. Franchement, je n'y crois
pas.

--
James Kanze mailto:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 pl. Pierre Sémard, 78210 St.-Cyr-l'École, France +33 (0)1 30 23 00 34


Avatar
Gabriel Dos Reis
Yoxoman writes:

|
| > En gros, l'idée est que si, en tant qu'utilisateur,
| > tes constructeurs de copie ne font que copier, sans effets de
| > bord, et que l'identité de l'objet n'a pas d'importance, tu ne
| > dois pas pouvoir s'apperçois de l'optimisation.
|
| C'est dommage, j'ai l'impression que c'est une réelle restriction du
| C++.

Si tu veux que ta fonction de copie fasse aurre chose que ce qui
ressemblerait à une construction par copie, appelle la autre chose
qu'un constructeur par copie. Où est la réelle restriction ?

| Finalement, les concepteurs de la norme ont décidé que le
| constructeur de copie doit faire ce qu'il est censé faire. Sous pretexte
| qu'un programme bien écrit doit le faire.

Ce comportement était déjà présent dans la spécification de l'ARM.

[...]

| Comment être sûr que le constructeur de copie ne pourrait être employé
| quelque part, dans un programme joyeusement atypique, à autre chose ?

Exemple ?

| Arriver à un bout de code, une idée, sur lesquels on pourrait s'extasier
| "Waou, c'est joli ça". Et hop, sous une contrainte de portabilité, les
| rêves s'envolent. Je caricature, mais bon...

?

-- Gaby
Avatar
Nico
Yoxoman m'a raconté au claviophone électronique moderne que :
Bonjour à tous.

Lors d'une initialisation d'un objet à l'aide d'un retour de fonction,
j'ai remarqué que (sous gcc), ni le constructeur de copie, ni
l'opérateur d'affectation n'étaient invoqués. Il y a (je crois) juste un
"branchement" entre la variable crée dans le corps de la fonction et le
nouvel objet.



class fractions ? si c'est ça, même souci ;o)

En fait, je voudrais savoir si ce comportement est standard, ou si c'est
une optimisation du compilateur.
Dans tous les cas, ce type de declaration n'est-il pas à proscrire,
étant donné qu'on perd le contrôle de l'opérateur = ?

Ci-joint mon bout de code :

#include <iostream>
using namespace std;

class X
{
public:
int i;

X() {cout << "constructeurn";}
X(int a) : i(a) {cout << "constructeur intn";}
X(const X& x) : i(x.i) {cout << "constructeur copien";};

X f(int a);

X &operator=(const X &x) {cout << "affectationn"; i = x.i; return
*this;}
};

X X::f(int a)
{
X x(a);
return x;
}

int main()
{
X x1(1); // appel au constructeur (entier)

X x2 = x1; // appel au constructeur par copie

X x3(2); // appel au constructeur (entier)
x3 = x1; // appel a l'operateur d'affectation

X x4 = x1.f(3); // appel a... rien
}