简体   繁体   中英

What happens with an object when passed by const reference is added to a container like std::map?

I'm having a problem with understanding the concept of ownership of objects in C++.

To give you a little bit of a background, I am programming a small app in C++ 11 using SFML 2.1 on Windows using Visual Studio 2013.

Basically I wrote a class AssetsManager to wrap around my sf::Texture objects. I'd like to store my textures in a std::map to be able to refer to and find them easily with a key. That map is a private member of the AssetsManager class.

I have two public member functions for "registering" (adding) a texture object to the map and getting a texture object from the map.

This is how I declare the map in my class header:

private:
    // Resources are mapped to a key for easy access.
    std::map < std::string, sf::Texture > m_textures;

And these are the two function definitions in the cpp file:

sf::Texture AssetsManager::GetTexture(const std::string& key)
{
    return m_textures[key];
}

bool AssetsManager::RegisterTexture(const std::string& key, const sf::Texture& texture)
{
    // If a texture in the std::map already exists with the given key, return false.
    if (m_textures.count(key))
        return false;

    // Add the texture with key to the std::map.
    m_textures.insert(std::make_pair(key, texture));

    return true;
}

In my main function, I do this:

AssetsManager assets;

sf::Texture myTexture;
myTexture.loadFromFile("Assets/MyCharacter.png");
assets.RegisterTexture("Hero Character", myTexture);

What I do not understand is what happens with the myTexture variable declared in my main function. I added the object to the map, so I can get it by calling

assets.GetTexture("Hero Character");

Can I do something with the myTexture variable in the main function though?

Are both objects the same one? Or did the ownership transfer from myTexture to the object in the map, so that myTexture became somewhat empty?

It would be nice if someone could clarify this, so I can continue my work.

I even bumped my head around if using pointers would be the right choice here, but I'd like the AssetsManager to have full control over the Textures , ie "own" them, not just pointers to the objects, if you understand what I mean.

Step by step analysis:

m_textures.insert(std::make_pair(key, texture));

First, std::make_pair(T&&, U&&) takes forwarding-references , and instantiates a pair of decayed types: std::pair<std::decay_t<T>, std::decay_t<U>> from arguments supplied. In your case those types are as follows:

T = const std::string&, std::decay_t<T> = std::string
U = const sf::Texture&, std::decay_t<U> = sf::Texture

Having that said, the pair is defined as std::pair<std::string, sf::Texture> , and since the original values had been const lvalue references , both arguments key and texture have been copy-constructed and therefore copies have been stored within an instance of the returned std::pair .

Now, the insert member-function. std::map has its overload taking another forwarding-reference , described as follows:

Value to be copied to (or moved as ) the inserted element.

That being said, the argument of insert (deduced type P = std::pair<std::string, sf::Texture> , with P&& ) is std::forward<P> ed to the newly created node's pair that your instance of std::map stores. This results in moving your pair, which is described as:

The object is initialized with the contents of the pr pair object. The corresponding member of pr is passed to the constructor of each of its members.

However, if either of those members don't implement move-constructor , the operation falls back into regular copy-construction for that specific member.

To sum up, with m_textures.insert(std::make_pair(key, texture)); statement in worst case scenario you may end up with two (possibly expensive) copies of both arguments made. The only benefit is you don't have to worry about ownership , as the std::map itself gains a brand new copy of those elements. As a side note, you actually never have to worry about ownership when someone takes your object by reference . It's very unlikely someone will even think of storing a reference or an address to such object, as its origin is unknown and one can't control its lifetime.

You can try to optimize this operation replacing it with following emplace member function's call:

m_textures.emplace(std::piecewise_construct
                 , std::forward_as_tuple(key)
                 , std::forward_as_tuple(texture));

which will use the arguments to emplace a pair directly within the associated node.

Nonetheless, I feel you should use std::shared_ptr for sf::Texture instances, since as the name suggests it can store large amount of data.

Additionally, consider reimplementing your GetTexture member function (with overloaded const-qualified one) to return reference :

sf::Texture& AssetsManager::GetTexture(const std::string& key)
//~~~~~~~~~^
{
    return m_textures[key];
}

const sf::Texture& AssetsManager::GetTexture(const std::string& key) const
//~~~~~~~~~~~~~~~^                                                   ~~~~^
{
    return m_textures.at(key);
}

because otherwise each call to GetTexture will make a copy as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM