37

I've been working with pointers for a few years now, but I only very recently decided to transition over to C++11's smart pointers (namely unique, shared, and weak). I've done a fair bit of research on them and these are the conclusions that I've drawn:

  1. Unique pointers are great. They manage their own memory and are as lightweight as raw pointers. Prefer unique_ptr over raw pointers as much as possible.
  2. Shared pointers are complicated. They have significant overhead due to reference counting. Pass them by const reference or regret the error of your ways. They're not evil, but should be used sparingly.
  3. Shared pointers should own objects; use weak pointers when ownership is not required. Locking a weak_ptr has equivalent overhead to the shared_ptr copy constructor.
  4. Continue to ignore the existence of auto_ptr, which is now deprecated anyhow.

So with these tenets in mind, I set off to revise my code base to utilize our new shiny smart pointers, fully intending to clear to board of as many raw pointers as possible. I've become confused, however, as to how best take advantage of the C++11 smart pointers.

Let's assume, for instance, that we were designing a simple game. We decide that it is optimal to load a fictional Texture data type into a TextureManager class. These textures are complex and so it is not feasible to pass them around by value. Moreover, let us assume that game objects need specific textures depending on their object type (i.e. car, boat, etc).

Prior, I would have loaded the textures into a vector (or other container like unordered_map) and stored pointers to these textures within each respective game object, such that they could refer to them when they needed to be rendered. Let's assume the textures are guaranteed to outlive their pointers.

My question, then, is how to best utilize smart pointers in this situation. I see few options:

  1. Store the textures directly in a container, then construct a unique_ptr in each game object.

    class TextureManager {
      public:
        const Texture& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, Texture> textures_;
    };
    class GameObject {
      public:
        void set_texture(const Texture& texture)
            { texture_ = std::unique_ptr<Texture>(new Texture(texture)); }
      private:
        std::unique_ptr<Texture> texture_;
    };
    

    My understanding of this, however, is that a new texture would be copy-constructed from the passed reference, which would then be owned by the unique_ptr. This strikes me as highly undesirable, since I would have as many copies of the texture as game objects that use it -- defeating the point of pointers (no pun intended).

  2. Store not the textures directly, but their shared pointers in a container. Use make_shared to initialize the shared pointers. Construct weak pointers in the game objects.

    class TextureManager {
      public:
        const std::shared_ptr<Texture>& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, std::shared_ptr<Texture>> textures_;
    };
    class GameObject {
      public:
        void set_texture(const std::shared_ptr<Texture>& texture)
            { texture_ = texture; }
      private:
        std::weak_ptr<Texture> texture_;
    };
    

    Unlike the unique_ptr case, I won't have to copy-construct the textures themselves, but rendering the game objects is expensive since I would have to lock the weak_ptr each time (as complex as copy-constructing a new shared_ptr).

So to summarize, my understanding is such: if I were to use unique pointers, I would have to copy-construct the textures; alternatively, if I were to use shared and weak pointers, I would have to essentially copy-construct the shared pointers each time a game object is to be drawn.

I understand that smart pointers are inherently going to be more complex than raw pointers and so I'm bound to have to take a loss somewhere, but both of these costs seem higher than perhaps they should be.

Could anybody point me in the correct direction?

Sorry for the long read, and thanks for your time!

SamuelMS
  • 1,116
  • 1
  • 11
  • 22
  • 18
    Before I go past the first paragraph: smart pointers don't exist to replace pointers. They exist to replace manual memory management. They are not about the pointing; they are about the smarting. There isn't really a big reason to discard a design like your option #1 *but with a pointer in the `GameObject`*. Just make sure the TextureManager lives as long as all textures need to live. If the GameObject is going to own a texture anyway, using a unique_ptr there is just goshdamn pointless because you can just put a `Texture` directly with no pointers, dumb or smart... – R. Martinho Fernandes Nov 12 '13 at 13:05
  • 8
    _Let's assume the textures are guaranteed to outlive their pointers._ This together with R.MartinhoFernandes comment should already answer the question. Unless you need to weaken that guarantee, there is really no reason to use smart pointers, as you have no ownership concerns. – ComicSansMS Nov 12 '13 at 13:07
  • 1
    (That said, `TextureManager` sounds like a glorified container; I wonder why it needs a whole new class for that, or at the very least, why it doesn't have a name more explicit about its container-y nature) – R. Martinho Fernandes Nov 12 '13 at 13:08
  • 2
    I would reconsider your point 3 regarding weak pointers. When you don't want to express ownership but still need to "point" to something, use a raw pointer. Weak pointers can only be used in conjunction with shared pointers and they're designed to break ownership cycles. – bstamour Nov 12 '13 at 13:09
  • This is still true - "Prefer unique_ptr over raw pointers as much as possible." – SChepurin Nov 12 '13 at 13:22
  • 1
    Martinho and ComicSansMS, thanks for the comments. You're right, and I agree -- smart pointers aren't a replacement for raw pointers. As mentioned above, I have little experience with smart pointers and so I wanted to clarify their usage and ensure I wasn't misunderstanding anything. The example I gave was clearly subpar given the strong guarantee of texture persistence, but this discussion has definitely helped my understanding. (Further, GameObjects aren't intended to own their textures -- that's a mess in and of itself and is another fault of my example). – SamuelMS Nov 12 '13 at 13:22
  • 1
    @SChepurin Or at least when storing pointers in containers. It's too easy to forget to deallocate when the container goes out of scope. – Fred Foo Nov 12 '13 at 13:26
  • @bstamour, fair point. I was too vague in my description; I meant solely with regard to shared and weak pointers. – SamuelMS Nov 12 '13 at 13:28
  • This sounds like a text-book use-case for a plain (const-) reference. Have you considered this? – Björn Pollex Nov 13 '13 at 20:47
  • 1
    _"I understand that smart pointers are inherently going to be more complex than raw pointers"_ -- no, you understand wrong. If you use them for managing object ownership and lifetime (as they're intended to be used) then they're simpler, because they automatically clean up. If you misuse them for representing semantics of non-ownership where you don't want to manage object lifetimes or have automatic cleanup, then yes, it will be more complex. Don't do that :-) – Jonathan Wakely Nov 13 '13 at 23:54
  • 2
    Also, regarding your second option, if you don't want the overhead of locking the `weak_ptr` every time you access it ... don't use a `weak_ptr`! You could have just stored a `shared_ptr` as a member of `GameObject`. (However, as others have correctly said, if object lifetime is managed by the `TextureManager` then you don't need to use smart pointers in `GameObject` anyway.) – Jonathan Wakely Nov 13 '13 at 23:59
  • @JonathanWakely Sorry, when I said complex I meant computationally/memory intensive -- not that they're harder to use or understand. I decided not to go with smart pointers in situations like these, because I have a strong guarantee for the data's lifetime and I don't want the game objects to own their textures/other resources. Thanks for the comment! – SamuelMS Nov 14 '13 at 21:16

4 Answers4

31

Even in C++11, raw pointers are still perfectly valid as non-owning references to objects. In your case, you're saying "Let's assume the textures are guaranteed to outlive their pointers." Which means you're perfectly safe to use raw pointers to the textures in the game objects. Inside the texture manager, store the textures either automatically (in a container which guarantees constant location in memory), or in a container of unique_ptrs.

If the outlive-the-pointer guarantee was not valid, it would make sense to store the textures in shared_ptr in the manager and use either shared_ptrs or weak_ptrs in the game objects, depending on the ownership semantics of the game objects with regards to the textures. You could even reverse that - store shared_ptrs in the objects and weak_ptrs in the manager. That way, the manager would serve as a cache - if a texture is requested and its weak_ptr is still valid, it will give out a copy of it. Otherwise, it will load the texture, give out a shared_ptr and keep a weak_ptr.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • 5
    Raw pointers should be used when you required a *reseatable* non-owning plain reference. Otherwise, you should just use a reference. In fact, the example given by the OP seems like a text-book use-case of plain references. Especially since he explicitly says that lifetime is not an issue. – Björn Pollex Nov 13 '13 at 20:49
  • 3
    @BjörnPollex Reseatable ... which you need if you want your objects to be assignable (even just move-assignable), for example. Having references as member variables is possible, but comes with its own set of headaches. I believe it's best reserved for special use cases. – Angew is no longer proud of SO Nov 14 '13 at 07:58
11

To summarize your use case: *) Objects are guaranteed to outlive their users *) Objects, once created, are not modified (I think this is implied by your code) *) Objects are reference-able by name and guaranteed to exist for any name your app will ask for (I'm extrapolating -- I'll deal below with what to do if this is not true.)

This is a delightful use case. You can use value semantics for textures throughout your application! This has the advantages of great performance and being easy to reason about.

One way to do this is have your TextureManager return a Texture const*. Consider:

using TextureRef = Texture const*;
...
TextureRef TextureManager::texture(const std::string& key) const;

Because the underling Texture object has the lifetime of your application, is never modified, and always exists (your pointer is never nullptr) you can just treat your TextureRef as simple value. You can pass them, return them, compare them, and make containers of them. They are very easy to reason about and very efficient to work on.

The annoyance here is that you have value semantics (which is good), but pointer syntax (which can be confusing for a type with value semantics). In other words, to access a member of your Texture class you need to do something like this:

TextureRef t{texture_manager.texture("grass")};

// You can treat t as a value. You can pass it, return it, compare it,
// or put it in a container.
// But you use it like a pointer.

double aspect_ratio{t->get_aspect_ratio()};

One way to deal with this is to use something like the pimpl idiom and create a class that is nothing more than a wrapper to a pointer to a texture implementation. This is a bit more work because you'll end up creating an API (member functions) for your texture wrapper class that forward to your implementation class's API. But the advantage is that you have a texture class with both value semantics and value syntax.

struct Texture
{
  Texture(std::string const& texture_name):
    pimpl_{texture_manager.texture(texture_name)}
  {
    // Either
    assert(pimpl_);
    // or
    if (not pimpl_) {throw /*an appropriate exception */;}
    // or do nothing if TextureManager::texture() throws when name not found.
  }
  ...
  double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();}
  ...
  private:
  TextureImpl const* pimpl_; // invariant: != nullptr
};

...

Texture t{"grass"};

// t has both value semantics and value syntax.
// Treat it just like int (if int had member functions)
// or like std::string (except lighter weight for copying).

double aspect_ratio{t.get_aspect_ratio()};

I've assumed that in the context of your game, you'll never ask for a texture that isn't guaranteed to exist. If that is the case, then you can just assert that the name exists. But if that isn't the case, then you need to decide how to handle that situation. My recommendation would be to make it an invariant of your wrapper class that the pointer can't be nullptr. This means that you throw from the constructor if the texture doesn't exist. That means you handle the problem when you try to create the Texture, rather than to have to check for a null pointer every single time you call a member of your wrapper class.

In answer to your original question, smart pointers are valuable to lifetime management and aren't particularly useful if all you need is to pass around references to object whose lifetime is guaranteed to outlast the pointer.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
Jon Kalb
  • 266
  • 1
  • 5
3

You could have a std::map of std::unique_ptrs where the textures are stored. You could then write a get method that returns a reference to a texture by name. That way if each model knows the name of its texture(which it should) you can simple pass the name into the get method and retrieve a reference from the map.

class TextureManager 
{
  public:
    Texture& get_texture(const std::string& key) const
        { return *textures_.at(key); }
  private:
    std::unordered_map<std::string, std::unique_ptr<Texture>> textures_;
};

You could then just use a Texture in the game object class as opposed to a Texture*, weak_ptr etc.

This way texture manager can act like a cache, the get method can be re-written to search for the texture and if found return it from the map, else load it first, move it to the map and then return a ref to it

const_ref
  • 4,016
  • 3
  • 23
  • 38
1

Before I get going, as I accidentally a novel...

TL;DR Use shared pointers for figuring out responsibility issues, but be very cautious of cyclical relationships. If I were you, I would use a table of shared pointers to store your assets, and everything that needs those shared pointers should also use a shared pointer. This eliminates the overhead of weak pointers for reading (as that overhead in game is like creating a new smart pointer 60 times a second per object). It's also the approach my team and I took, and it was super effective. You also say your textures are guaranteed to outlive the objects, so your objects cannot delete the textures if they use shared pointers.

If I could throw my 2 cents in, I'd like to tell you about an almost identical foray I took with smart pointers in my own video game; both the good and the bad.

This game's code takes an almost identical approach to your solution #2: A table filled with smart-pointers to bitmaps.

We had some differences though; we had decided to split our table of bitmaps into 2 pieces: one for "urgent" bitmaps, and one for "facile" bitmaps. Urgent bitmaps are bitmaps that are constantly loaded into memory, and would be used in the middle of battle, where we needed the animation NOW and didn't want to go to the hard disk, which had a very noticeable stutter. The facile table was a table of strings of file paths to the bitmaps on the hdd. These would be large bitmaps loaded at the beginning of a relatively long section of gameplay; like your character's walking animation, or the background image.

Using raw pointers here has some problems, specifically ownership. See, our assets table had a Bitmap *find_image(string image_name) function. This function would first search the urgent table for the entry matching image_name. If found, great! Return a bitmap pointer. If not found, search the facile table. If we find a path matching your image name, create the bitmap, then return that pointer.

The class to use this the most was definitely our Animation class. Here's the ownership problem: when should an animation delete its bitmap? If it came from the facile table then there's no problem; that bitmap was created specifically for you. It's your duty to delete it!

However, if your bitmap came from the urgent table, you could not delete it, as doing so would prevent others from using it, and your program goes down like E.T. the game, and your sales follow suit.

Without smart pointers, the only solution here is to have the Animation class clone its bitmaps no matter what. This allows for safe deletion, but kills the speed of the program. Weren't these image supposed to be time sensitive?

However, if the assets class were to return a shared_ptr<Bitmap>, then you have nothing to worry about. Our assets table was static you see, so those pointers were lasting until the end of the program no matter what. We changed our function to be shared_ptr<Bitmap> find_image (string image_name), and never had to clone a bitmap again. If the bitmap came from the facile table, then that smart pointer was the only one of its kind, and was deleted with the animation. If it was an urgent bitmap, then the table still held a reference upon Animation destruction, and the data was preserved.

That's the happy part, here's the ugly part.

I've found shared and unique pointers to be great, but they definitely have their caveats. The largest one for me is not having explicit control over when your data gets deleted. Shared pointers saved our asset lookup, but killed the rest of the game on implementation.

See, we had a memory leak, and thought "we should use smart pointers everywhere!". Huge mistake.

Our game had GameObjects, which were controlled by an Environment. Each environment had a vector of GameObject *'s, and each object had a pointer to its environment.

You should see where I'm going with this.

Objects had methods to "eject" themselves from their environment. This would be in case they needed to move to a new area, or maybe teleport, or phase through other objects.

If the environment was the only reference holder to the object, then your object couldn't leave the environment without getting deleted. This happens commonly when creating projectiles, especially teleporting projectiles.

Objects also were deleting their environment, at least if they were the last ones to leave it. The environment for most game states was a concrete object as well. WE WERE CALLING DELETE ON THE STACK! Yeah we were amateurs, sue us.

In my experience, use unique_pointers when you're too lazy to call delete and only one thing will ever own your object, use shared_pointers when you want multiple objects to point to one thing, but can't decide who has to delete it, and be very wary of cyclical relationships with shared_pointers.

DeepDeadpool
  • 1,441
  • 12
  • 36