Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move constructor behaviour

I recently stumbled upon some strange behaviour (strange from my point of view) from move constructor. The result is different when compiling with GCC and Visual Studio. I would like to hear the explanation of this behaviour, don't think this is a bug but probably compiler specific.

Consider the following code:

#include <iostream>
#include <unordered_map>

struct Test
{
    std::unordered_map<int, int> v;
    std::unordered_map<int, int>::iterator vend;

    Test(std::unordered_map<int, int>::iterator &it)
        : vend { v.end() }
    {
        it = this->vend;
    };

    Test() = delete;
    Test(Test const &) = delete;
    Test(Test &&) = default; // <- line in question
};

int main()
{
    std::unordered_map<int, int>::iterator it;
    std::unordered_map<int, Test> m;
    m.emplace(0, Test{ it });
    std::cout << std::boolalpha << (m.at(0).v.end() == it) << "\n";

    return 0;
}

So I am storing iterator to the end of map in map element upon creation of the element. I also take a reference to it to compare later. From std::unordered_map::emplace:

Inserts a new element into the container constructed in-place with the given args if there is no element with the key in the container.

Careful use of emplace allows the new element to be constructed while avoiding unnecessary copy or move operations.

Using default move constructor, the iterator stored in map element and my reference are the same:

Test(Test &&) = default; 

Results are true in GCC and true in VS. Now if I change move constructor to:

Test(Test &&) {} 

GCC still returns true but VS returns false

Just in case tried with c++17, same results. So could anyone please explain what is happening here?

like image 638
Killzone Kid Avatar asked Dec 15 '18 15:12

Killzone Kid


1 Answers

In this line:

m.emplace(0, Test{ it });

... the newly inserted Test object is constructed from std::forward<Test>(Test{ it }), so the move constructor is indeed called (because of the forwarding, copy elision does not work here). If you want to construct Test directly, you can use m.emplace(0, it) instead.

Now we can see

  • With Test(Test &&) = default;, the temporary object designated by Test{ it }.v is moved into m.at(0).v. If it remains valid (this is not guaranteed), m.at(0).v.end() == it evaluates to true; otherwise the program results in undefined behavior.

  • With Test(Test &&) {}, m.at(0).v is value-initialized, and it is invalidated as the temporary object designated by Test{ it }.v is destroyed. The program results in undefined behavior.

In libstdc++'s implementation, the end iterators for different unordered_maps have the same value (similar to std::istream_iterator), so the behavior of GCC is reasonable.

like image 88
xskxzr Avatar answered Oct 14 '22 23:10

xskxzr