In an STL Map of structs, why does the "[ ]" operator cause the struct's dtor to be invoked 2 extra times?
Asked Answered
T

7

8

I've created a simple test case exhibiting a strange behavior I've noticed in a larger code base I'm working on. This test case is below. I'm relying on the STL Map's "[ ]" operator to create a pointer to a struct in a map of such structs. In the test case below, the line...

TestStruct *thisTestStruct = &testStructMap["test"];

...gets me the pointer (and creates a new entry in the map). The weird thing I've noticed is that this line not only causes a new entry in the map to be created (because of the "[ ]" operator), but for some reason it causes the struct's destructor to be called two extra times. I'm obviously missing something - any help is much appreciated! Thanks!

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

using namespace std;
struct TestStruct;

int main (int argc, char * const argv[]) {

    map<string, TestStruct> testStructMap;

    std::cout << "Marker One\n";

    //why does this line cause "~TestStruct()" to be invoked twice?
    TestStruct *thisTestStruct = &testStructMap["test"];

    std::cout << "Marker Two\n";

    return 0;
}

struct TestStruct{
    TestStruct(){
        std::cout << "TestStruct Constructor!\n";
    }

    ~TestStruct(){
        std::cout << "TestStruct Destructor!\n";
    }
};

the code above outputs the following...

/*
Marker One
TestStruct Constructor!             //makes sense
TestStruct Destructor!               //<---why?
TestStruct Destructor!               //<---god why?
Marker Two
TestStruct Destructor!               //makes sense
*/

...but I don't understand what causes the first two invocations of TestStruct's destructor? (I think the last destructor invocation makes sense because testStructMap is going out of scope.)

Tko answered 25/10, 2010 at 19:2 Comment(2)
The answer is below. But turn the optimization level as high as it will go and see how many copies get elided.Leann
Further details about this question and the why behind it may be found in this question: #26021618Hangnail
I
18

The functionality of std::map<>::operator[] is equivalent to

(*((std::map<>::insert(std::make_pair(x, T()))).first)).second

expression, as specified in the language specification. This, as you can see, involves default-constructing a temporary object of type T, copying it into a std::pair object, which is later copied (again) into the new element of the map (assuming it wasn't there already). Obviously, this will produce a few intermediate T objects. Destruction of these intermediate objects is what you observe in your experiment. You miss their construction, since you don't generate any feedback from copy-constructor of your class.

The exact number of intermediate objects might depend on compiler optimization capabilities, so the results may vary.

Illconditioned answered 25/10, 2010 at 19:20 Comment(0)
G
8

You have some unseen copies being made:

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

using namespace std;
struct TestStruct;

int main (int argc, char * const argv[]) {

    map<string, TestStruct> testStructMap;

    std::cout << "Marker One\n";

    //why does this line cause "~TestStruct()" to be invoked twice?
    TestStruct *thisTestStruct = &testStructMap["test"];

    std::cout << "Marker Two\n";

    return 0;
}

struct TestStruct{
    TestStruct(){
        std::cout << "TestStruct Constructor!\n";
    }

    TestStruct( TestStruct const& other) {
        std::cout << "TestStruct copy Constructor!\n";
    }

    TestStruct& operator=( TestStruct const& rhs) {
        std::cout << "TestStruct copy assignment!\n";
    }

    ~TestStruct(){
        std::cout << "TestStruct Destructor!\n";
    }
};

Results in:

Marker One
TestStruct Constructor!
TestStruct copy Constructor!
TestStruct copy Constructor!
TestStruct Destructor!
TestStruct Destructor!
Marker Two
TestStruct Destructor!
Giorgione answered 25/10, 2010 at 19:11 Comment(0)
T
5

add the following to TestStruct's interface:

TestStruct(const TestStruct& other) {
    std::cout << "TestStruct Copy Constructor!\n";
}   
Tabathatabb answered 25/10, 2010 at 19:8 Comment(0)
L
4

operator[] inserts to the map if there is not already an element there.

What you are missing is output for the compiler-supplied copy constructor in your TestStruct, which is used during container housekeeping. Add that output, and it should all make more sense.

EDIT: Andrey's answer prompted me to take a look at the source in Microsoft VC++ 10's <map>, which is something you could also do to follow this through in all its gory detail. You can see the insert() call to which he refers.

mapped_type& operator[](const key_type& _Keyval)
    {   // find element matching _Keyval or insert with default mapped
    iterator _Where = this->lower_bound(_Keyval);
    if (_Where == this->end()
        || this->comp(_Keyval, this->_Key(_Where._Mynode())))
        _Where = this->insert(_Where,
            value_type(_Keyval, mapped_type()));
    return ((*_Where).second);
    }
Laky answered 25/10, 2010 at 19:8 Comment(0)
G
4

Your two mysterious destructor calls are probably paired with copy constructor calls going on somewhere within the std::map. For example, it's conceivable that operator[] default-constructs a temporary TestStruct object, and then copy-constructs it into the proper location in the map. The reason that there are two destructor calls (and thus probably two copy constructor calls) is implementation-specific, and will depend on your compiler and standard library implementation.

Gant answered 25/10, 2010 at 19:8 Comment(0)
C
0

so the lesson is - dont put structs in a map if you care about their lifecycles. Use pointers, or even better shared_ptrs to them

Carranza answered 25/10, 2010 at 21:19 Comment(0)
D
0

You can check it out through this more simple code.

#include <iostream>
#include <map>

using namespace std;

class AA
{
public:
  AA()            { cout << "default const" << endl; }
  AA(int a):x(a)  { cout << "user const" << endl; }
  AA(const AA& a) { cout << "default copy const" << endl; }
  ~AA()           { cout << "dest" << endl; }
private:
  int x;
};

int main ()
{
  AA o1(1);

  std::map<char,AA> mymap;

  mymap['x']=o1;    // (1)

  return 0;
}

The below result shows that (1) line code above makes (1 default const) and (2 default copy const) calls.

user const
default const        // here
default copy const   // here
default copy const   // here
dest
dest
dest
dest
Disclamation answered 7/12, 2016 at 0:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.