OVH Cloud OVH Cloud

Reference constante sur donnee membre d'un objet temporaire ?

13 réponses
Avatar
Aurélien REGAT-BARREL
Bonjour,
D'après ce que je sais, si une référence constante est initialisée avec un
objet temporaire, on a la garantie que l'objet temporaire persiste tant que
la référence existe. OK.
Question : et si c'est une donnée membre d'un objet temporaire :

#include <vector>
#include <iostream>

class Test
{
public:
Test()
{
vect.push_back( 1 );
vect.push_back( 2 );
vect.push_back( 3 );
}

const std::vector<int> & get_vector() const
{
return this->vect;
}
private:
std::vector<int> vect;
};

Test get_test()
{
return Test();
}

int main()
{
const std::vector<int> & v = get_test().get_vector();
std::cout << v.size() << '\n';
}

g++ m'affiche 3, VC++ 0. Qui a raison ?

--
Aurélien REGAT-BARREL

10 réponses

1 2
Avatar
Ahmed MOHAMED ALI
Bonjour,
int main()
{
const std::vector<int> & v = get_test().get_vector();
std::cout << v.size() << 'n';
}

g++ m'affiche 3, VC++ 0. Qui a raison ?


Je pense que c'est un comportement indéfini.Après l'affectation le
destructeur de Test peut être appelé à tout moment car tu n'as pas de
référence constante sur
l'objet Test.

Ahmed MOHAMED ALI

"Aurélien REGAT-BARREL" wrote in message
news:4254ed28$0$12968$
Bonjour,
D'après ce que je sais, si une référence constante est initialisée avec un
objet temporaire, on a la garantie que l'objet temporaire persiste tant
que

la référence existe. OK.
Question : et si c'est une donnée membre d'un objet temporaire :

#include <vector>
#include <iostream>

class Test
{
public:
Test()
{
vect.push_back( 1 );
vect.push_back( 2 );
vect.push_back( 3 );
}

const std::vector<int> & get_vector() const
{
return this->vect;
}
private:
std::vector<int> vect;
};

Test get_test()
{
return Test();
}

int main()
{
const std::vector<int> & v = get_test().get_vector();
std::cout << v.size() << 'n';
}

g++ m'affiche 3, VC++ 0. Qui a raison ?

--
Aurélien REGAT-BARREL




Avatar
Aurélien REGAT-BARREL
J'ai modifié mon test pour voir si ma référence ne désignait pas un objet
détruit, et c'est le cas. Je suppose juste que le std::vector de g++ ne met
pas son size à 0 lors de sa destruction, contrairement à celui de VC++.

class A
{
public:
~A()
{
std::cout << "~A()n";
}
};

class Test
{
public:
Test() : vect( 3 )
{
}

const std::vector<A> & get_vector() const
{
return this->vect;
}
private:
std::vector<A> vect;
};

Test get_test()
{
return Test();
}

int main()
{
const std::vector<A> & v = get_test().get_vector();
std::cout << "Taille = " << v.size() << 'n';
}

ce qui m'affiche:

~A()
~A()
~A()
~A()
Taille = 0

Par contre je pige pas d'où sort le 4° (le premier en fait) objet A détruit.

--
Aurélien REGAT-BARREL
Avatar
kanze
Aurélien REGAT-BARREL wrote:
J'ai modifié mon test pour voir si ma référence ne désignait
pas un objet détruit, et c'est le cas. Je suppose juste que le
std::vector de g++ ne met pas son size à 0 lors de sa
destruction, contrairement à celui de VC++.


Je ne vois toujours pas où ton test vérifie le nombre de copies
qui existe de l'object en question (un std::vector, n'oublions
pas).

Il y a en fait plusieurs choses à dire en ce qui concerne ton
test. Le premier, c'est que le code que j'ai vu ni lie pas un
temporaire à une référence. Nulle part. Donc, la règle dont tu
parles n'entre pas en ligne de compte (et en effect, tu as un
comportement indéfinit qui dépend en fait de l'implémentation).

class A
{
public:
~A()
{
std::cout << "~A()n";
}
};

class Test
{
public:
Test() : vect( 3 )
{
}

const std::vector<A> & get_vector() const


Attention : ce que tu renvoies ici n'est pas un temporaire, mais
une référence. Dans le langage de la norme, une lvalue. Lier un
lvalue à une référence n'a jamais le moindre influence sur la
durée de vie d'un objet.

Pour être « intéressant » (par rapport à la question posée), il
faudrait que tu renvoies par valeur. Mais alors, l'objet renvoyé
n'est plus un membre. C'est donc normal que le fait de le lier à
une référence n'influe pas sur la durée de vie de Test.

Une autre altérnatif serait de rendre le membre vect public, et
essayer de le lier à la référence.

{
return this->vect;
}
private:
std::vector<A> vect;
};

Test get_test()
{
return Test();
}

int main()
{
const std::vector<A> & v = get_test().get_vector();


Du moment que get_vector() renvoie une référence, l'expression
d'initialisation est un lvalue. On lie sans créer de temporaire,
et donc, sans influer sur la durée de vie de quoique ce soit.

Tu remarqueras aussi que tu pourrais enlever tous les const, et
l'expression serait tout aussi légale.

Plus intéressant serait de rendre le membre vect public, et
d'écrire :

std::vector<A> const& v = get_test().vect ;

Là, j'avoue ne pas être très sur de l'effet possible sur la
durée de vie du temporaire de type Test. Selon la norme,
l'implémentation a deux choix :

-- lier la référence à l'objet que représente l'expression
temporaire (le « rvalue »), ou

-- faire un temporaire du type de l'expression
d'initialisation (par exemple, de la classe dérivée, même si
on lie à une référence de type classe de base), et lier
l'expression au temporaire.

Dans les deux cas, le compilateur a droit à lier à un
sous-objet.

Note en revanche ici que le type de l'expression
d'initialisation est bien std::vector<A>, et NON Test. Je ne
suis pas vraiment sûr de l'intention ici, mais je ne vois rien
dans le texte de la norme qui donne au compilateur le droit de
prolonger la durée de vie du temporaire Test (qui lui n'est lié
à aucune référence). Et je ne vois pas comment le compilateur
pourrait detruire le Test sans en detruire le sous-objet vect.
Du coup, le seul alternatif que je vois que lui est ouvert est
de profiter du deuxième point ci-dessus, de faire une copie de
vect, et de lier la copie à la référence (en lui donnant la
durée de vie de la référence).

std::cout << "Taille = " << v.size() << 'n';
}

ce qui m'affiche:

~A()
~A()
~A()
~A()
Taille = 0

Par contre je pige pas d'où sort le 4° (le premier en fait)
objet A détruit.


Tu as bien initialisé le vecteur avec quelque chose, non.
L'initialisation des éléments d'un vector se fait toujours au
moyen d'un constructeur de copie. Alors, il faut bien un objet
qu'on copie. Ça fait donc trois éléments dans le vecteur, et un
objet (temporaire) qu'on copie pour les initialiser, donc,
quatre objets.

--
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
Aurélien REGAT-BARREL
int main()
{
const std::vector<A> & v = get_test().get_vector();
std::cout << "Taille = " << v.size() << 'n';
}

Je ne vois toujours pas où ton test vérifie le nombre de copies
qui existe de l'object en question (un std::vector, n'oublions
pas).


Une fois le programme exécuté, j'ai 4 objets A détruits avant le cout, et
aucun après. J'en déduis que mon vecteur a été détruit avant l'appel à cout,
et qu'il n'en n'existe aucune copie (aucun affichage par la suite).

Plus intéressant serait de rendre le membre vect public, et
d'écrire :

std::vector<A> const& v = get_test().vect ;

Là, j'avoue ne pas être très sur de l'effet possible sur la
durée de vie du temporaire de type Test. Selon la norme,
l'implémentation a deux choix :

-- lier la référence à l'objet que représente l'expression
temporaire (le « rvalue »), ou

-- faire un temporaire du type de l'expression
d'initialisation (par exemple, de la classe dérivée, même si
on lie à une référence de type classe de base), et lier
l'expression au temporaire.

Dans les deux cas, le compilateur a droit à lier à un
sous-objet.

Note en revanche ici que le type de l'expression
d'initialisation est bien std::vector<A>, et NON Test. Je ne
suis pas vraiment sûr de l'intention ici, mais je ne vois rien
dans le texte de la norme qui donne au compilateur le droit de
prolonger la durée de vie du temporaire Test (qui lui n'est lié
à aucune référence). Et je ne vois pas comment le compilateur
pourrait detruire le Test sans en detruire le sous-objet vect.
Du coup, le seul alternatif que je vois que lui est ouvert est
de profiter du deuxième point ci-dessus, de faire une copie de
vect, et de lier la copie à la référence (en lui donnant la
durée de vie de la référence).


C'est ce que j'ai pu vérifier en testant.

Par contre je pige pas d'où sort le 4° (le premier en fait)
objet A détruit.


Tu as bien initialisé le vecteur avec quelque chose, non.
L'initialisation des éléments d'un vector se fait toujours au
moyen d'un constructeur de copie. Alors, il faut bien un objet
qu'on copie. Ça fait donc trois éléments dans le vecteur, et un
objet (temporaire) qu'on copie pour les initialiser, donc,
quatre objets.


Ah oui.
Je pensais que vector faisait directement 3 new de placement.
En fait, je pige pas pourquoi il utilise la recopie. Ca lui fait créer un
temporaire en plus, non ?

Merci.

--
Aurélien REGAT-BARREL


Avatar
James Kanze
Aurélien REGAT-BARREL wrote:
int main()
{
const std::vector<A> & v = get_test().get_vector();
std::cout << "Taille = " << v.size() << 'n';
}


Je ne vois toujours pas où ton test vérifie le nombre de
copies qui existe de l'object en question (un std::vector,
n'oublions pas).



Une fois le programme exécuté, j'ai 4 objets A détruits avant
le cout, et aucun après. J'en déduis que mon vecteur a été
détruit avant l'appel à cout, et qu'il n'en n'existe aucune
copie (aucun affichage par la suite).


Tu comptes donc le nombre d'objets A, et non le nombre de
vector. Et comme tu as constaté, il n'y a pas forcément un
rapport direct.


Par contre je pige pas d'où sort le 4° (le premier en fait)
objet A détruit.




Tu as bien initialisé le vecteur avec quelque chose, non.
L'initialisation des éléments d'un vector se fait toujours au
moyen d'un constructeur de copie. Alors, il faut bien un objet
qu'on copie. Ça fait donc trois éléments dans le vecteur, et
un objet (temporaire) qu'on copie pour les initialiser, donc,
quatre objets.



Je pensais que vector faisait directement 3 new de placement.
En fait, je pige pas pourquoi il utilise la recopie. Ca lui
fait créer un temporaire en plus, non ?


Il lui faut le temporaire de toute façon. Tu dois lui dire d'une
façon à une autre avec quoi il doit initialiser les éléments.

Dans l'implémentation initiale, et dans pas mal
d'implémentations aujourd'hui, il y'avait pas de constructeur à
un paramètre. Il n'y avait que :

vector<T>::vector( size_t n, T const& v = T() ) ;

--
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
Vincent Lascaux
D'après ce que je sais, si une référence constante est initialisée avec un
objet temporaire, on a la garantie que l'objet temporaire persiste tant
que
la référence existe. OK.


Je ne savais pas ca...
Est ce que le code suivant est valide dans ce cas :

const std::string& foo() { return "bar"; }
int main() { std::cout << foo(); }

Visual Studio n'a pas l'air d'aimer trop ca (il donne un warning sur le fait
qu'on retourne une référence sur un objet temporaire et le code n'affiche
pas bar)...

--
Vincent

Avatar
kanze
Vincent Lascaux wrote:

D'après ce que je sais, si une référence constante est
initialisée avec un objet temporaire, on a la garantie que
l'objet temporaire persiste tant que la référence existe.
OK.


Je ne savais pas ca...
Est ce que le code suivant est valide dans ce cas :

const std::string& foo() { return "bar"; }
int main() { std::cout << foo(); }


Non. §12.2/5 :

The second context [où la durée de vie d'un temporaire ne
termine pas à la fin de l'expression complète] is when a
reference is bound to a temporary. The temporary to which
the reference is bound or the temporary that is the complete
object to a subobject of which the temporary is bound
persistes for the lifetime of the referenc except as
specified below. [...] A temporary bound to the returned
value in a funciton return statement persists until the
function exits.

La seule véritable intérêt que je vois, c'est dans les
expressions du genre :

Base const& b = condition ? Base() : Derived() ;

Mais dans la pratique, ce qu'on veut en géneral, c'est quelque
chose du genre :

Base const& b = condition ? Derived1() : Derived2() ;

Seulement, ce n'est pas légal, et si j'ajoute les casts
nécessaire pour le rendre légale, je tombe dans un cas où je lis
les temporaires à des références temporaires (qui elles servent
dans l'initialisation de b), et donc, que je ne prolonge pas la
vie des temporaires.

--
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
Ultimataupe
Bonjour,
D'après ce que je sais, si une référence constante est initialisée avec un
objet temporaire, on a la garantie que l'objet temporaire persiste tant que
la référence existe. OK.
J'aimerais bien savoir d'ou tu tiens ça.

Question : et si c'est une donnée membre d'un objet temporaire :

#include <vector>
#include <iostream>

class Test
{
public:
Test()
{
vect.push_back( 1 );
vect.push_back( 2 );
vect.push_back( 3 );
}

const std::vector<int> & get_vector() const
{
return this->vect;
}
private:
std::vector<int> vect;
};

Test get_test()
{
return Test();
}

int main()
{
const std::vector<int> & v = get_test().get_vector();
std::cout << v.size() << 'n';
}

g++ m'affiche 3, VC++ 0. Qui a raison ?

En rajoutant le destructeur de Test

~Test()
{
std::cout <<"~Test()" << 'n';
}
tu t'apercois que ton objet Test ( et donc le vector) est detruit avant
d'appeller v.size();
Donc v.size() est appellé sur un objet détruit.

Comme l'objet est stocké dans la pile, la zone mémoire n'est pas écrasé
(sauf si tu sors de la fonction et que tu rentre dans un autre) c'est
pur ça que l'appel à size() ne plante pas.

Apparemment le destructeur de vector de VC++ efface bien le tableau et
remet sa taille à 0 ce qui n'est pas fait par g++.
On pourrait dire que VC++ est plus propre.

Personne n'a raison ni tord . Rien n'indique dans les spec C++ ce qu'on
doit mettre dans de la mémoire non utilisée.

Ce genre de pratique est à bannir.
D'une manière générale il est fortement déconseillé de retourner une
référence ou un pointeur sur une donnée membre (ce qui est fait dans
get_vector) car on a plus aucune visibilité sur la durée de vie de
l'objet retourné.

Avatar
Vincent Lascaux
Ce genre de pratique est à bannir.
D'une manière générale il est fortement déconseillé de retourner une
référence ou un pointeur sur une donnée membre (ce qui est fait dans
get_vector) car on a plus aucune visibilité sur la durée de vie de l'objet
retourné.


C'est quand même fait à plein d'endroits par la STL...
Par exemple l'operateur constant [] de vector retourne une référence
constante sur l'objet. Tu trouves que c'est une mauvaise technique de
programmation ?

--
Vincent

Avatar
Ultimataupe
Ce genre de pratique est à bannir.
D'une manière générale il est fortement déconseillé de retourner une
référence ou un pointeur sur une donnée membre (ce qui est fait dans
get_vector) car on a plus aucune visibilité sur la durée de vie de l'objet
retourné.



C'est quand même fait à plein d'endroits par la STL...
Par exemple l'operateur constant [] de vector retourne une référence
constante sur l'objet. Tu trouves que c'est une mauvaise technique de
programmation ?

Dans le cas des collections effectivement il faut mieux que la classe

laisse l'acces à ses données membres.
dans ce cas il faut obligatoirement retourner une reference pour pouvoir
faire des choses du genre:
vect[index].methode()

par contre si on commence à faire des choses genre:

elem & monelem = vect[index];
il y a plutôt intéret à faire gaffe à ce qu'on fait.

Si en plus il s'agit d'une appli multithread avec un tableau partagé je
te raconte pas le boxon.

Dans le cas d'une classe normale qui est censée masquer son
implementation, dommage de retourner des references sur ses données
internes.


ex:
std::vector<int> vect;
vect.push_back( 1 );

int & i = vect[0];
std::cout << i << std::endl;

vect.clear();
std::cout << i << std::endl;//ligne tres suspecte

vect.push_back( 2 );
std::cout << i << std::endl;


1 2