Yesterday I asked this question that got answered properly Insert unique_ptr in a map and keep ownership of unique_ptr on failed insertion.
I recite it here because the problem is now a little bit extended:
I have a method addToMap
that accepts a string
as a key and an unique_ptr
as rvalue -reference that should then be emplaced in a map
. If the insertion fails the caller of the method should keep the ownership. For the method try_emplace(...)
in the map
class this is possible.
This works now, if the unique_ptr
holds an integer or non inherited class. If I use an abstract class as template parameter the suggested solution does not work anymore.
Here the sample code: https://godbolt.org/z/MeeYGEE9n
#include <map>
#include <string>
#include <memory>
#include <iostream>
template<typename T>
struct Foo
{
void addToMap(const std::string& key, std::unique_ptr<T>&& value)
{
_map.try_emplace(key, std::move(value));
std::cout << "Inside " << value << std::endl;
// after a failed insertion value is not empty and usable
}
std::map<std::string, std::unique_ptr<T>> _map;
};
class IBar
{
public:
virtual ~IBar() = default;
virtual void Zoo() = 0;
};
class BarImp : public IBar
{
public:
~BarImp() override = default;
void Zoo() override { }
};
int main()
{
{
auto a = std::make_unique<int>(0);
auto b = std::make_unique<int>(1);
Foo<int> foo;
foo.addToMap("0", std::move(a));
foo.addToMap("0", std::move(b));
std::cout << "Outside " << a << std::endl;
std::cout << "Outside " << b << std::endl;
}
std::cout << std::endl;
{
auto a = std::make_unique<BarImp>();
auto b = std::make_unique<BarImp>();
Foo<IBar> foo;
foo.addToMap("0", std::move(a));
foo.addToMap("0", std::move(b));
std::cout << "Outside " << a << std::endl;
std::cout << "Outside " << b << std::endl;
}
}
Example output:
Inside 00000000
Inside 010F3F88
Outside 00000000
Outside 010F3F88
Inside 00000000
Inside 010F26D0
Outside 00000000
Outside 00000000
As it can be seen, the last 2 “Outside” are empty but the last one should not be empty. Why is that and how can I achieve to correct it? As you can see the map emplace
is working correctly (line 5 & 6 of output).
5
As pointed out by Alan in the comment section, there is an implicit conversion from std::unique_ptr<BarImp>
to std::unique_ptr<IBar>
, creating a temporary. The behaviour of std::move
with regard to the value after the move is unspecified. cppreference.com states:
Unless otherwise specified, all standard library objects that have been moved from are placed in a “valid but unspecified state”, […]
Hence you cannot rely on a particular behaviour once you have moved your std::unique_ptr<BarImp>
into a std::unique_ptr<IBar>
.
A simple solution to this problem is to make the addToMap
function a template, accepting unique_ptr
of a different type, so the implicit type conversion only happens inside the try_emplace()
:
template<typename U>
void addToMap(const std::string& key, std::unique_ptr<U>&& value)
{
_map.try_emplace(key, std::move(value));
std::cout << "Inside " << value << std::endl;
// after a failed insertion value is not empty and usable
}
With gcc 13, the output changes then to:
Inside 0
Inside 0x5ffa465e92d0
Outside 0
Outside 0x5ffa465e92d0
Inside 0
Inside 0x5ffa465e92b0
Outside 0
Outside 0x5ffa465e92b0
1