OVH Cloud OVH Cloud

std::vector copy constructor

34 réponses
Avatar
Cyrille \cns\ Szymanski
Est-il possible d'utiliser std::vector de telle sorte que les appels à
resize construisent chaque objet plutôt que d'utiliser un copy
constructor ?

L'idée est d'avoir un fonctionnement similaire à celui du code suivant
mais en évitant la copie de l'objet et le gâchis de mémoire :

vector<MyClass> vec;
vec.reserve(20);
for( int i=0; i<10; ++i )
{
vec.push_back( *(new MyClass()) );
}

A défaut, quelle est la façon propre de faire un
// construire l'objet
MyClass *tmp = new MyClass();
// le placer dans le tableau
vec.push_back( *tmp );
// limérer la mémoire sans appeler le destructeur
free( tmp );

Je veux bien fournir mon propre copy constructor et lui faire créer un
nouvel objet de toutes pièces, mais cela est d'une part détourner son
vrai objectif et d'autre part cela risque d'interférer avec les vraies
copies d'objet.

--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/

10 réponses

1 2 3 4
Avatar
Loïc Joly
Cyrille "cns" Szymanski wrote:
il semble que tu prennes un malin plaisir à tout embrouiller dans le
post d'après ;)

Peut-être que si tu reformulais ton problème tel qu'il est réellement,
ie en montrant la classe des objets que tu veux gérer dans ton vector,
ce serait un peu plus clair.



Voici donc plusieurs exemples concrets de classes dont je désire créer un
tableau :

struct MyClass {
int nb;
MyClass() {
nb = rand();
cout << "Objet " << nb << "n";
}
~MyClass() {
cout << "Delete " << nb << "n";
}
};


vector<MyClass> vec;
for (int i=0 ; i! ; ++i)
{
MyClass c;
vec.push_back(c);
}


Il faut que les nombres contenus dans les éléments du tableau de MyClass
soient différents (comme le ferait un "MyClass[] vec = new MyClass[]"),
ce que ne fait pas un "vector<MyClass> vec(10)".


Un autre exemple :
struct MyClass {
HANDLE hwnd;
MyClass() {
hwnd = OpenHandle();
}
~MyClass() {
CloseHandle( hwnd );
}
};

Faire
vector<MyClass> vec;
for( i=0; i<10; ++i) {
vec.push_back( MyClass() );
}
ne fonctionne pas car le handle est fermé tout de suite.


En fait, comme on l'a dit, pour pouvoir mettre une classe dans un
vector, elle DOIT pouvoir être copiée (avec la sémentique du mot
copier). Ce n'est pas le cas de cette classe. Dans cet example
windowsien, un truc qui ressemble à ça devrait marcher :

MyClass::MyClass(MyClass const &mc)
{
DuplicateHandle(GetCurrentProcess(), mc.hwnd, GetCurrentProcess(), hwnd);
}

--
Loïc


Avatar
Loïc Joly
Vivien Gallinaro wrote:

Cyrille "cns" Szymanski wrote:


v.push_back( MyClass() ); // l'objet temporaire est alloué sur



Aïe, l'objet temporaire est détruit. Il faut donc que je ruse pour que
le destructeur ne libère pas des ressources que la copie pourrait
utiliser.



Mais dans ce cas, tu wrappes (emballe) ta "ressource" dans une classe
rien que pour ça, qui devient membre de MyClass, et dont un rôle
principal est de déterminer si, quand on passe par son destructeur, la
ressource doit être libérée.

Ca sent le pointeur quelque part (dans la nouvelle classe, je dirais),
et on me dit que ce que je raconte navigue entre les idées de pointeur
intelligent, comptage de références et singleton.


Sauf que dans certains cas, les ressources allouées peuvent déjà être
gérées par comptage de référence par l'OS, ce qui évite au programmateur
d'utiliser des pointeur intelligents pour obtenir ce comportement.

--
Loïc



Avatar
Fabien LE LEZ
On 22 Aug 2003 15:39:19 GMT, "Cyrille "cns" Szymanski"
wrote:

Alors ma question est comment déplacer un objet ?


A priori, la seule solution est de le copier au nouvel emplacement,
via le constructeur de copie, puis de supprimer l'original.

Avatar
Arnaud Debaene
Cyrille "cns" Szymanski wrote:

Ce que je veux c'est dynamiquement allouer de la mémoire (un tableau
de n objets, malloc( sizeof(MyClass)*n ) ) et instancier un objet à
chaque emplacement (new (&vec[i]) MyClass()) ce qui est a mon avis un
moyen assez performant pour instancier n objets et pouvoir y accéder
rapidement ensuite.
Franchement, ce genre de problème deperformances ne devrait pas t'embêter

pour l'instant, surtout si tu es sous Windows donc sans contraintes temps
réel fortes. "Early otpimisation is the root of all evil". Commences par
faire une solution :
1) Correcte.
2) Facilement compréhensible.
3) Maintenable.
... et seulement ensuite préoccupes toi de performances, si des mesures avec
u profiler révèlent que c'est nécessaire.


v.push_back( MyClass() ); // l'objet temporaire est alloué
sur


Aïe, l'objet temporaire est détruit. Il faut donc que je ruse pour
que le destructeur ne libère pas des ressources que la copie pourrait
utiliser.
Ce que tu ne sembles pas avoir compris, c'est que pour pouvoir être stockés

dans un vector, tes objets doivent être copiables ce qui n'est pas le cas
actuellement à cause de ton handle ou autre référence système. Ce que tu
dois faire, c'est donc écrire un constructeur par copie qui donne une
sémantique de copie correcte à ton objet.

<HS> Dans ton exemple, ça donne :
struct MyClass {
HANDLE hwnd;
MyClass() {
hwnd = OpenHandle();
}

MyClass(const MyClass& rs) {
hwnd=DuplicateHandle(rs.hwnd);
}

~MyClass() {
CloseHandle( hwnd );
}
};

</HS>

Arnaud


Avatar
Patrick Mézard
Est-il possible d'utiliser std::vector de telle sorte que les appels à
resize construisent chaque objet plutôt que d'utiliser un copy
constructor ?

L'idée est d'avoir un fonctionnement similaire à celui du code suivant
mais en évitant la copie de l'objet et le gâchis de mémoire :

vector<MyClass> vec;
vec.reserve(20);
for( int i=0; i<10; ++i )
{
vec.push_back( *(new MyClass()) );
}

A défaut, quelle est la façon propre de faire un
// construire l'objet
MyClass *tmp = new MyClass();
// le placer dans le tableau
vec.push_back( *tmp );
// limérer la mémoire sans appeler le destructeur
free( tmp );

Je veux bien fournir mon propre copy constructor et lui faire créer un
nouvel objet de toutes pièces, mais cela est d'une part détourner son
vrai objectif et d'autre part cela risque d'interférer avec les vraies
copies d'objet.



C'est tordu ton affaire mais en même temps c'est vendredi.

Pour résumer, d'après les post précédents, tu veux :
1 - Que les instances des tes objets soient contigües en mémoire.
2 - Supporter des objets non copiables.
3 - Pouvoir agrandir/réduire le tableau dynamiquement.

(2) et (3) sont visiblement incompatibles si on suppose qu'un
redimensionnement peut occasionner une réallocation de mémoire : il faut
alors déplacer les objets de leur ancien emplacement vers le nouveau. Cela
suppose que les objets sont déplaçables. La STL impose que les objets soient
copiables ce qui implique déplaçables. Ton problème avec ça c'est que le
constructeur de copie peut coûter cher s'il doit copier beaucoup de données,
alors qu'il pourrait juste en transférer la responsabilité. Pour moi c'est
un faux problème, car soit l'objet ne coûte pas cher à copier, soit il coûte
cher et il suffit d'en manipuler un (smart-)pointeur pour que ça ne coûte
pas cher.

Le second problème concerne les objets qui ne coûtent potentiellement pas
cher à copier mais dont la sémantique ne le permet pas. Les allouer
dynamiquement et les manipuler via des pointeurs te semble trop cher. En
fait, il faudrait un constructeur qui prenne l'objet à déplacer en
paramètre, le copie/déplace et laisse l'objet déplacé dans un état
indéterminé mais destructible. On va appeler ça un "move constructor" :-).
Le problème, comme on a pu le soulever dans un autre fil dont quelqu'un t'as
déjà donné la référence, c'est que ça n'existe pas, ou pas encore. Que
nenni, on peut toujours l'implémenter soi-même :

namespace cns
{

struct move_it
{
};

const move_it move;

} //namespace csn

/// On impose qu'un move constructor ait la signature suivante pour un type
T donné :
/// Le move_it juste pour pas confondre avec autre chose
/// (et un explicit serait pas mal), et le throw() parce que si jamais
/// ça lance une exception au milieu d'une série de déplacement, ben on est
mort (ce qui
/// n'est pas le cas avec de la copie...)
T(T& t, const cns::move_it& m) throw();

/// Et on a un truc du genre
struct MyClass
{
char ptr[];

MyClass()
{
ptr = new char[10];
sprintf( ptr, "%d", time() );
}

MyClass(MyClass& c, const cns::move_it&) :
ptr(c.ptr)
{
c.ptr = 0;
}

~MyClass()
{
delete ptr;
}
};

Et ensuite tu remplaces le uninitialized_copy de l'autre jour par un truc
qui appelle le move constructor à la place du constructeur de copie. C'est
superTM. Ca impose juste d'avoir un move constructor pour toutes les classes
que tu veux utiliser avec ton conteneur (c'est à dire que toutes les classes
dont elles dérivent ou qu'elles englobent directement ont un move
constructor), ce qui en pratique est une contrainte hyper-forte. Dans le
style maintenable on a fait mieux, et il reste à prouver qu'on a gagner
quelque chose à faire ça.

Et comme je suis décidément hyper motivé et curieux, j'ai un petit test plus
bas, sous Windows (limitation due aux timers).
En gros, j'ai deux types d'objets avec des pseudo-"move constructor":

- CnsDeadlyObject qui contient 8 handles (int) qui sont valides si != 0
- CnsDeadlyObjects qui contient 4 CnsDeadlyObject

Le test crée un tas de chaque, et tente de les déplacer/copier sur un bloc
de mémoire non-initialisé.
Pour résumer, chez moi j'ai un gain d'environ 40% en faveur du move
constructor. Pas mal (et pas hyper suprenant non plus sinon il n'y aurait
pas de "proposal" en sa faveur), mais honnêtement pas suffisant pour que ça
vaille la peine de se prendre la tête à moins d'écrire un bibliothèque de
calcul numérique. De plus le test est hautement criticable: il ne mesure pas
le coût des réallocations, et travaille sur des ensembles d'éléments assez
énormes pour que le gain ait un sens quelconque. Mais si vous avez mieux à
proposer...

Patrick Mézard


//*****************************
#include <windows.h>
#include <memory>
#include <vector>
#include <new>

namespace cns
{

struct move_it {};

const move_it move;

} //namespace cns

class CnsDeadlyObject
{
public:
CnsDeadlyObject(CnsDeadlyObject& c, const cns::move_it&) : //throw()
h1_(c.h1_), h2_(c.h2_), h3_(c.h3_), h4_(c.h4_),
h5_(c.h5_), h6_(c.h6_), h7_(c.h7_), h8_(c.h8_)
{
c.h1_ = c.h2_ = c.h3_ = c.h4_ = c.h5_ = c.h6_ = c.h7_ = c.h8_ = 0;
}

CnsDeadlyObject() : h1_(0), h2_(0), h3_(0), h4_(0), h5_(0), h6_(0), h7_(0),
h8_(0)
{

}

CnsDeadlyObject(int) :
h1_(rand()), h2_(rand()), h3_(rand()), h4_(rand()),
h5_(rand()), h6_(rand()), h7_(rand()), h8_(rand())
{
}

private:
int h1_;
int h2_;
int h3_;
int h4_;
int h5_;
int h6_;
int h7_;
int h8_;
};

class CnsDeadlyObjects
{
public:
CnsDeadlyObjects(int a) : o1_(a), o2_(a), o3_(a), o4_(a)
{
}

CnsDeadlyObjects(CnsDeadlyObjects& o, const cns::move_it& m) : //throw()
o1_(o.o1_, m), o2_(o.o2_, m), o3_(o.o3_, m), o4_(o.o4_, m)
{
}

private:
CnsDeadlyObject o1_;
CnsDeadlyObject o2_;
CnsDeadlyObject o3_;
CnsDeadlyObject o4_;
};

namespace
{

inline void QueryPerfCounter(LARGE_INTEGER* t)
{
if(0==::QueryPerformanceCounter(t))
throw std::runtime_error("Error");
}

template <class T>
void TestMove(int nObj)
{
double dcopy, dmove;

//Create a bunch of objects
typedef std::vector<T> ObjArray;
ObjArray v;
for(int i=0; i<nObj; ++i)
v.push_back( T(1) );

LARGE_INTEGER freq, t1, t2;
if(0==::QueryPerformanceFrequency(&freq) || freq.QuadPart==0)
throw std::runtime_error("Error");

T* mem = (T*)malloc(sizeof(T)*nObj);
if(mem==0)
throw std::bad_alloc();

// Test with copy
QueryPerfCounter(&t1);

std::uninitialized_copy(v.begin(), v.end(), mem);

QueryPerfCounter(&t2);
dcopy = (t2.QuadPart - t1.QuadPart)/(double)freq.QuadPart;
std::cout<<"Copy time: "<<dcopy<<" s"<<endl;

// Test with move
QueryPerfCounter(&t1);

T* m = mem;
for(ObjArray::iterator o = v.begin(); o!=v.end(); ++o)
{
new(m) T(*o, cns::move);
}

QueryPerfCounter(&t2);
dmove = (t2.QuadPart - t1.QuadPart)/(double)freq.QuadPart;
std::cout<<"Move time: "<<dmove<<" s"<<endl;
std::cout<<"Move/Copy: "<<100.0*(dcopy-dmove)/dcopy<<" %"<<endl;
}

} //namespace


int _tmain(int argc, _TCHAR* argv[])
{
TestMove<CnsDeadlyObject>(2000000);
TestMove<CnsDeadlyObjects>(200000);
}

//********************************

Avatar
Cyrille \cns\ Szymanski
Franchement, ce genre de problème deperformances ne devrait pas
t'embêter pour l'instant, surtout si tu es sous Windows donc sans
contraintes temps réel fortes. "Early otpimisation is the root of all
evil". Commences par faire une solution :
1) Correcte.
2) Facilement compréhensible.
3) Maintenable.
... et seulement ensuite préoccupes toi de performances, si des
mesures avec u profiler révèlent que c'est nécessaire.


Ok, Arnaud est généralement de bon conseil.

Tout ce que je veux c'est (en gros comme dans le post dont on m'a donné
les références) placer plein d'objets quelconques dans un conteneur de
taille variable et pouvoir y accéder rapidement.

Ce que tu ne sembles pas avoir compris, c'est que pour pouvoir être
stockés dans un vector, tes objets doivent être copiables ce qui n'est
pas le cas actuellement à cause de ton handle ou autre référence
système. Ce que tu dois faire, c'est donc écrire un constructeur par
copie qui donne une sémantique de copie correcte à ton objet.


Oui je peux faire ça.

C'est quand-même gênant de devoir rendre un objet copiable pour les beaux
yeux de la STL alors qu'il ne sera pas copié dans ma partie du code. En
clair ça augmente la taille du code et le rend moins compréhensible. Je
reconnais quand même que le code sera moins long que si j'implémente mon
propre cnsVector<>.

Ce qui est encore plus rageant c'est qu'on ne puisse pas déplacer un
objet autrement qu'en le copiant et en détruisant l'ancienne copie (il
faut donc le rendre copiable lui aussi).

Il doit sûrement y avoir des raisons à cela, mais je trouve ça horrible
et au risque de me faire incendier, un realloc() bien utilisé est quand
même vachement pratique.

--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/

Avatar
Cyrille \cns\ Szymanski
Bon on va dire qu'on va oublier les bêtises que j'ai pu raconter, surtout
sur la fin.


Non, ça fait classe le namespace "cns".

--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/

Avatar
Cyrille \cns\ Szymanski
Pour résumer, d'après les post précédents, tu veux :
1 - Que les instances des tes objets soient contigües en mémoire.
2 - Supporter des objets non copiables.
3 - Pouvoir agrandir/réduire le tableau dynamiquement.


Exactement.


La STL impose que les objets soient copiables ce qui implique
déplaçables. Ton problème avec ça c'est que le constructeur de copie
peut coûter cher s'il doit copier beaucoup de données, alors qu'il
pourrait juste en transférer la responsabilité.
Pour moi c'est un faux problème, car
soit l'objet ne coûte pas cher à copier, soit il coûte cher et il
suffit d'en manipuler un (smart-)pointeur pour que ça ne coûte pas
cher.


Et le smart pointer contredit (1).


En fait, il faudrait un constructeur qui prenne l'objet à
déplacer en paramètre, le copie/déplace et laisse l'objet déplacé dans
un état indéterminé mais destructible. On va appeler ça un "move
constructor" :-). Que nenni, on peut toujours l'implémenter soi-même :


Ton implémentation est intéressante mais comme tu le dis toi-même :

C'est superTM. Ca impose juste d'avoir un move constructor pour
toutes les classes que tu veux utiliser avec ton conteneur (c'est à
dire que toutes les classes dont elles dérivent ou qu'elles englobent
directement ont un move constructor), ce qui en pratique est une
contrainte hyper-forte. Dans le style maintenable on a fait mieux, et
il reste à prouver qu'on a gagner quelque chose à faire ça.



Pour résumer, chez moi j'ai un gain d'environ 40% en faveur du move
constructor. Pas mal (et pas hyper suprenant non plus sinon il n'y
aurait pas de "proposal" en sa faveur), mais honnêtement pas suffisant
pour que ça vaille la peine de se prendre la tête à moins d'écrire un
bibliothèque de calcul numérique. De plus le test est hautement
criticable: il ne mesure pas le coût des réallocations, et travaille
sur des ensembles d'éléments assez énormes pour que le gain ait un
sens quelconque. Mais si vous avez mieux à proposer...


Je comprends très bien les problèmes liés au déplacement d'objets et je
vois tout à fait pourquoi la STL impose des objets copiables. C'est pas
une si grosse contrainte que ça quand on y réfléchit bien, mais je viens
du C et cette façon de penser me choque. Ne me faites pas croire que
constructeur copie + destructeur soit la façon la plus efficace de
déplacer un objet.

Copiable implique déplaçable, mais certainement pas l'inverse. Et je
trouve ça aberrant de devoir rendre un objet copiable pour pouvoir le
déplacer.

Pour ce qui me concerne directement ce qui serait top c'est qu'il y ait
une sémantique de réallocation de tableaux (T[] et non vector<T>
évidemment) et tous mes problèmes seraient résolus.


Mais je suis très têtu moi.


--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/

Avatar
Jean-Marc Bourguet
"Cyrille "cns" Szymanski" writes:

Il doit sûrement y avoir des raisons à cela, mais je trouve ça horr ible
et au risque de me faire incendier, un realloc() bien utilisé est quand
même vachement pratique.


Un realloc ce n'est possible que pour des POD.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index. html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
Jean-Marc Bourguet
"Cyrille "cns" Szymanski" writes:

Pour résumer, d'après les post précédents, tu veux :
1 - Que les instances des tes objets soient contigües en mémoire.
2 - Supporter des objets non copiables.
3 - Pouvoir agrandir/réduire le tableau dynamiquement.


Exactement.


Mais tu parles de realloc et l'utilisation de realloc implique que les
objets non seulement soient copiables mais soient des POD, ce qui est
une contraintes beaucoup plus forte qu'être copiable.

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


1 2 3 4