在STL结构映射中,为什么使用"[ ]"运算符会导致结构体的析构函数被调用两次?

8

我创建了一个简单的测试用例,展示了我在正在工作的较大代码库中注意到的奇怪行为。该测试用例如下所示。我依靠STL Map的“[]”运算符,在这样的结构体映射中创建指向结构体的指针。在下面的测试用例中,该行...

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

当我使用"[ ]"运算符时,它会给我返回指针,并在映射表中创建一个新条目。我注意到的奇怪之处是,这一行不仅会因为"[ ]"运算符而创建新的映射表条目,还会导致结构体析构函数被额外调用两次。很明显我漏掉了什么 - 非常感谢任何帮助!谢谢!

#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";
    }
};

上面的代码输出以下内容...
/*
Marker One
TestStruct Constructor!             //makes sense
TestStruct Destructor!               //<---why?
TestStruct Destructor!               //<---god why?
Marker Two
TestStruct Destructor!               //makes sense
*/

...但我不理解是什么导致了前两次调用 TestStruct 的析构函数?(我认为最后一次析构函数的调用是有意义的,因为 testStructMap 超出了作用域。)


答案在下面。但是将优化级别调到最高,看有多少副本被省略。 - Martin York
此问题的更多细节以及背后的原因可以在以下问题中找到:https://dev59.com/UIPba4cB1Zd3GeqPvrFp - srm
7个回答

18

std::map<>::operator[]的功能等同于

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

根据语言规范指定的表达式。可以看到,这涉及到使用类型T的临时对象进行默认构造,将其复制到std::pair对象中,然后再次复制到映射的新元素中(假设它之前不存在)。显然,这将产生一些中间T对象。销毁这些中间对象是您在实验中观察到的。您错过了它们的构造,因为您没有从类的复制构造函数生成任何反馈。

中间对象的确切数量可能取决于编译器优化能力,因此结果可能会有所不同。


8

您有一些未被发现的副本正在制作:

#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";
    }
};

导致结果:

Marker One
TestStruct Constructor!
TestStruct copy Constructor!
TestStruct copy Constructor!
TestStruct Destructor!
TestStruct Destructor!
Marker Two
TestStruct Destructor!

5

请在TestStruct的接口中添加以下内容:

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

4

operator[] 如果该元素不存在于 map 中,则会插入该元素。

你缺少的是在你的 TestStruct 中编译器提供的复制构造函数的输出,它在容器维护期间使用。添加该输出,一切都会更加清晰。

编辑:Andrey 的答案促使我查看了 Microsoft VC++ 10 的 <map> 源代码,这也是你可以做的事情,以便深入了解其所有细节。你可以看到他所提到的 insert() 调用。

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);
    }

4

你的两个神秘的析构函数调用可能与std::map内部某处正在进行的复制构造函数调用相关。例如,operator[]可能会默认构造一个临时的TestStruct对象,然后将其复制构造到映射中的正确位置。有两个析构函数调用(因此可能有两个复制构造函数调用)的原因是实现特定的,并且取决于您的编译器和标准库实现。


0
所以教训是 - 如果你关心结构体的生命周期,不要将它们放入映射中。使用指针,甚至更好的是使用 shared_ptr。

0

你可以通过这个更简单的代码来检查它。

#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;
}

以下结果显示,(1)行代码上方进行了(1个默认const)和(2个默认copy const)调用。
user const
default const        // here
default copy const   // here
default copy const   // here
dest
dest
dest
dest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接