tags:

views:

86

answers:

5

I have a class X that I would like to put into an STL map of type std::map. An STL map needs to have X stored in memory somewhere so I'm looking for an efficient (run time and memory) way to create X and store it in the map.

I noticed that the following code where x is an object of type X and stlMap is a map of type std::map:

stlMap["test"] = x;

Results in the following being called:

  1. X default constructor
  2. X Copy constructor
  3. X Copy constructor
  4. X destructor
  5. X destructor
  6. X assignment constructor
  7. X destructor

Why are so many X objects being created?

Is it an inefficient use of time and memory?

Is there a better way to put an object into a map? Maybe changing the map to be a map of strings to x*?

+3  A: 

STL containers have copy semantics, so what you observe is typical.

You could use pointers instead, but you can avoid the associated memory-management pain by using smart pointers (at the cost of a small amount of overhead).

Oli Charlesworth
+1 for redirect away from map<X*>
Steve Townsend
A: 

STL works on the basis of copying and assigning. So some amount of copying and assignment is expected. On the question of why so many, one way to find that is to put breakpoints at appropriate positions in X code, and run the program in debugger.

In the current way, std::map is doing the memory management for X. If you switch to using X*, then you have to do the memory management yourself. I find the former suitable in most cases.

ArunSaha
+3  A: 

Try stlMap.insert( map<string, X>::value_type("test", x) ):

#include <iostream>
#include <string>
#include <map>

using namespace std;

class X
{
public:
X() { cout << "X default constructor" << endl; }
~X() { cout << "X destructor" << endl; }
X( const X& other ) { cout << "X copy constructor" << endl; }
X& operator=( const X& other ) { cout << "X copy-assignment operator" << endl; }
int x;
};


int main()
{
X x;
map< string, X > stlMap;

cout << "INSERT BEGIN" << endl;
stlMap.insert( map< string, X >::value_type( "test", x ) );
cout << "INSERT END" << endl;
stlMap.clear();
cout << "ASSIGN BEGIN" << endl;
stlMap["test"] = x;
cout << "ASSIGN END" << endl;

return 0;
}

On my g++ that whittles things down to:

  1. X copy constructor
  2. X copy constructor
  3. X destructor

EDIT: Per ArunSaha's suggestion, updated the test. The insert() output is unchanged, while the assignment sequence looks like this:

  1. X default constructor
  2. X copy constructor
  3. X copy constructor
  4. X destructor
  5. X destructor
  6. X copy-assignment operator
genpfault
+1: I was thinking about the same experiment... Can you add a `cout` in the copy-assignment operator and re-run the experiment?
ArunSaha
@ArunSaha: Good catch, knew I forgot something :) Updated.
genpfault
Perfect, thanks. I'll try the value_type operation.
C Nielsen
A: 

Is the order of construction and destruction different when you enable optimizations? Many compilers can omit temporary objects involved in such statements when optimizing. I'd guess the output would involve fewer objects when optimized.

C++0x improves the situation considerably with move constructors. If your class X has a move constructor (which will look like X x(X&& m) { ... }, then you can change your statement to stlMap["test"] = std::move(x);. That statement is essentially equivalent to constructing a single X and then moving it to ownership of the map. Look up some articles on C++0x move semantics if you're not clued up, it's useful stuff.

AshleysBrain
A: 

Using this as a reference:

#include <iostream>
#include <map>

class X
{
    public:
     X()                    { std::cout << "Default Construct\n";}
    ~X()                    { std::cout << "Destroy\n";}
     X(X const&)            { std::cout << "Copy Construct\n";}
     X& operator=(X const&) { std::cout << "Assignment\n";}
};


int main()
{
    std::map<int,X>     store;
    X                   x;
    X                   y;

    std::cout << "Inserting x\n";
    store[1]    = x;
    std::cout << "Finished Insert\n";
    std::cout << "Inserting y\n";
    store[1]    = y;
    std::cout << "Finished Insert\n";
}

Running we get the following output:

Default Construct                    Building X
Default Construct                    Building Y
Inserting x                          ---- Start of an insert
Default Construct                    -------- Work to insert an item that is not in the map
Copy Construct                        
Copy Construct                       
Destroy                              
Destroy                             -------- Finished work to insert a new item
Assignment                          Assign x into the internal object
Finished Insert                     ---- Done
Inserting y                         ---- Start of an insert
Assignment                          Assign y onto the internal object.
Finished Insert                     ---- Done
Destroy                             Destroy y
Destroy                             Destroy x
Destroy                             Destroy map containing one X
Martin York