Jump to content

  • Log In with Google      Sign In   
  • Create Account

Banner advertising on our site currently available from just $5!


1. Learn about the promo. 2. Sign up for GDNet+. 3. Set up your advert!


How to Best Implement Objects with "Optional" Functionality?

  • You cannot reply to this topic
13 replies to this topic

#1 Sean_Seanston   Members   -  Reputation: 522

Like
0Likes
Like

Posted Yesterday, 02:39 PM

I have a few ideas of how this might be implemented, but because I'm not sure if some ways are obviously better than others and because it could involve pointers, I thought I'd ask...

 

Let's say we have a situation in a game where one object may possess a certain feature or may not. An example that's very close to what I'm actually trying to do would be something like:

- We have a Location class.

- A Location may contain(or reference, w/e) zero or exactly one Safehouse object.

- Depending on whether or not the Location has a Safehouse when the player is interacting with the Location, the game/UI/menu/whatever will display different options to the player.

i.e. Right now I'm working off the assumption that if a Menu should display certain things based on the existence of a Safehouse or not, then the Menu should be able to query somehow whether or not a valid Safehouse actually exists.

 

I can see some different ways of accomplishing this, but I'm not sure which are reasonable and which should be avoided:

 

1. What I would have done in the past, and the first idea to come to me, was to use a simple pointer. I'd point it to a Safehouse if it existed, otherwise it would be set to null, and so the game could check if it was null and know whether or not a valid Safehouse existed in that way.

My problem with this is that I'm afraid of it being bad practice, and of memory leaks where the risk is probably not necessary.

 

2. I could use smart pointers, but I don't currently have a compiler that supports C++11 due to being on Vista, so it would have to be auto_ptr. Does that sound like a potentially good idea in this situation?

 

3. Use a vector. Then if the vector is empty, there are no valid Safehouse objects and there's no fear of trying to call a member function of a null pointer or dealing with pointer allocation/deletion.

My problems with this are that it would seem to complicate matters such as dealing with encapsulation (accessing the elements without it being long-winded), and technically... I know it's not a big problem to just use a container and leave the possibility open of eventually having more than one, but as a learning experience I'm somewhat avoiding the issue.

What IF I definitely only wanted a single object? What would be good practice then?

 

4. Use a static Safehouse object in each Location. Use some sort of flag to denote whether or not it's valid. Seems ugly to me, and wasteful though that probably won't be a practical concern here anyway. Then again, maybe this is the only real solution if I demand a single object only and no pointers?

 

Looking at these, I'm leaning towards 3. Only thing is, something that always trips me up in situations like this: I'm not sure what's good practice for accessing a container outside the containing class.

e.g. Let's say we have a container of purchasable items, and another class needs to display information about these items. Is it best to just have the whole container returned by const reference?

Perhaps we're meant to avoid needing to access a container like this from outside... but then we can't decouple the item data from the user interface code, which to me sounds like it would be more important.



Sponsor:

#2 braindigitalis   Crossbones+   -  Reputation: 3959

Like
4Likes
Like

Posted Yesterday, 02:54 PM

1. What I would have done in the past, and the first idea to come to me, was to use a simple pointer. I'd point it to a Safehouse if it existed, otherwise it would be set to null, and so the game could check if it was null and know whether or not a valid Safehouse existed in that way.


Go with this and wrap the allocation and deletion of the pointer in your class ctor and dtor which contain the safehouse object.

It's the simplest solution and simple is always best.

Games Currently In Development:
Currently rewriting Firework Factory - Casual Puzzler for PC in Direct3D 11. Latest Journal Entry: Level Completion Animation (10-Mar-2015).


#3 Buckeye   GDNet+   -  Reputation: 8315

Like
4Likes
Like

Posted Yesterday, 03:37 PM


wrap the allocation and deletion of the pointer in your class ctor and dtor

 

^^^ This.

 

Further, rather than exposing the pointer itself:

 

// Location.h
class SafeHouse; // expose only that there exists a class SafeHouse

class Location
{
public:
   ...
   bool HasSafeHouse() { return mySafeHouse != nullptr; }
   void SetSafeHouse(SafeHouse *newSafeHouse);
private:
   SafeHouse *mySafeHouse;
}

// Location.cpp
#include "safehouse.h"
#include "location.h"
Location::Location() : mySafeHouse(nullptr) { ... }
Location::~Location() { SafeDelete( mySafeHouse ); }
void Location::SetSafeHouse(SafeHouse *newSafeHouse)
{
   SafeDelete( mySafeHouse );
   mySafeHouse = newSafeHouse;
}

Edited by Buckeye, Yesterday, 03:42 PM.

Please don't PM me with questions. Post them in the forums for everyone's benefit, and I can embarrass myself publicly.


#4 SeanMiddleditch   Crossbones+   -  Reputation: 9646

Like
2Likes
Like

Posted Yesterday, 03:55 PM

2. I could use smart pointers, but I don't currently have a compiler that supports C++11 due to being on Vista, so it would have to be auto_ptr. Does that sound like a potentially good idea in this situation?


Visual Studio 2010 runs on Vista, supports enough of C++11 for smart pointers, and the standard C++11 smart pointers (unique_ptr, shared_ptr, and weak_ptr) are part of 2010's standard library.

#5 Sean_Seanston   Members   -  Reputation: 522

Like
1Likes
Like

Posted Yesterday, 08:16 PM

Go with this and wrap the allocation and deletion of the pointer in your class ctor and dtor which contain the safehouse object.

It's the simplest solution and simple is always best.

 

 

 


wrap the allocation and deletion of the pointer in your class ctor and dtor

 

^^^ This.

Really? Kewl. Maybe I wasn't so naive after all... making sure the destructor deleted the pointer was what I originally had assumed was the right (and only necessary) thing to do.

 

That's encouraging...

 

Visual Studio 2010 runs on Vista, supports enough of C++11 for smart pointers, and the standard C++11 smart pointers (unique_ptr, shared_ptr, and weak_ptr) are part of 2010's standard library.

 

K, that's good to know. I'll have a look at that. Some of the C++11 features look really nice... auto in particular for sheer laziness.



#6 Strewya   Members   -  Reputation: 1755

Like
3Likes
Like

Posted Today, 12:57 AM

 

Go with this and wrap the allocation and deletion of the pointer in your class ctor and dtor which contain the safehouse object.

It's the simplest solution and simple is always best.

 

 

 


wrap the allocation and deletion of the pointer in your class ctor and dtor

 

^^^ This.

Really? Kewl. Maybe I wasn't so naive after all... making sure the destructor deleted the pointer was what I originally had assumed was the right (and only necessary) thing to do.

In which case you will have to make sure you also implement the assignment operator and copy constructor, due to the rule of three.

 

You have to think about lifetime management and ownership. Does the Location own the Safehouse, and determine it's lifetime (a Safehouse cannot exist without a Location), or can a Safehouse exist without a Location? This will determine if you need to allocate the Safehouse object in Location or just supply a pointer from the outside and have the Location reference it without having to care about the lifetime of it.


devstropo.blogspot.com - Random stuff about my gamedev hobby


#7 braindigitalis   Crossbones+   -  Reputation: 3959

Like
1Likes
Like

Posted Today, 03:50 AM

If you're talking C++11, what about the move constructor?

 

Should this be implemented in this case also?


Edited by braindigitalis, Today, 03:51 AM.

Games Currently In Development:
Currently rewriting Firework Factory - Casual Puzzler for PC in Direct3D 11. Latest Journal Entry: Level Completion Animation (10-Mar-2015).


#8 Strewya   Members   -  Reputation: 1755

Like
1Likes
Like

Posted Today, 04:26 AM

If you're talking C++11, what about the move constructor?

 

Should this be implemented in this case also?

If you have C++11, then yes, you need to implement move assignment and move constructor due to the rule of five which is just an extension of the Ro3.

 

And you can always try the rule of zero, where you don't implement either of these, let the compiler generate them for you, and handle the lifetime of objects manually through init/shutdown/create/destroy functions. It's much more explicit and requires good programmer discipline with some very specific design choices, but i find it's much nicer to never have to worry whether i'm implementing copy and move semantics correctly or not.


devstropo.blogspot.com - Random stuff about my gamedev hobby


#9 dmatter   Crossbones+   -  Reputation: 3428

Like
3Likes
Like

Posted Today, 04:48 AM

Go with this and wrap the allocation and deletion of the pointer in your class ctor and dtor which contain the safehouse object.

It's the simplest solution and simple is always best.

 

wrap the allocation and deletion of the pointer in your class ctor and dtor

 
^^^ This.

Really? Kewl. Maybe I wasn't so naive after all... making sure the destructor deleted the pointer was what I originally had assumed was the right (and only necessary) thing to do.


Raw pointers are for when you want a nullable, non-owning reference. The nullable property allows you to implement the optional semantic. But the non-owning property doesn't sound like it's what you want. So I doubt that a raw pointer is really the best solution here.

The rule-of-3 (or rule-of-5) has already been mentioned so I will discuss a separate problem with the above solution: You should only apply that solution once per class! So it's not a general-purpose solution by any means.
 
Let's take a small example example:
class Location
{
public:
    Location()
      : _safehouse(new Safehouse())
      , _bank(new Bank())
    { }

    Location(Safehouse * safehouse, Bank * bank)
      : _safehouse(safehouse)
      , _bank(bank)
    { }

    ~Location() {
        delete _safehouse;
        delete _bank;
    }

private:
    Safehouse * _safehouse;
    Bank * _bank;
};
(Note: I am ignoring copy/move/assignment in this example and focussing on the construction and deallocation issues).
 
The scary thing here is that to the uninitiated this looks completely safe and reasonable. In reality this class is not exception-safe because it is manually managing the lifetime of two resources using raw pointers. So what could happen?...
 
Scenario 1: You use the default constructor
First the constructor allocates Safehouse. This goes smoothly and the private _safehouse pointer is successfully initialised. Next the constructor attempts to allocate a Bank. For whatever reason this fails with an exception (e.g. out-of-memory, or the Bank's constructor decided to throw). At this point the stack begins to unwind and the ~Location destructor is never called (because the constructor never completed). Think about what happens to your _safehouse pointer - It's going to leak.
 
Scenario 2: You use the 2nd constructor
Now the allocation of Safehouse and Bank is outside of the class. The scope of manual memory management (and therefore errors) has only increased! But at least the constructor can't fail now. Unfortunately use of the constructor can still fail:
 
Location * location = new Location(new Safehouse(), new Bank());
 
This fails for exactly the same reason as Scenario 1. The Safehouse might be successfully created, but the Bank can fail. This leaves a Safehouse object without anyone to delete it.


The lesson is that raw pointers are really not so simple to use!
Manually managing >1 resource per object is fraught with pain.
 

Visual Studio 2010 runs on Vista, supports enough of C++11 for smart pointers, and the standard C++11 smart pointers (unique_ptr, shared_ptr, and weak_ptr) are part of 2010's standard library.

 
K, that's good to know. I'll have a look at that.

Please do!

Smart-pointers are really the here-and-now of modern C++. You want to be using them unless you have a good reason not to. You could easily use them to implement your optional references without memory leaks.

I would also like to point out that if your compiler doesn't support smart-pointers then you should definitely be using Boost to get them. So either way you will have smart-pointers available to you!

In fact, if you go with Boost then you'll have boost::optional which perfectly expresses the "one or none" semantic and is frequently the right solution. It is a shame that std::optional has not been promoted into a C++ standard yet (although probably will make it into C++17).
 
If you need both optional and shared-ownership semantics then I typically dispense with boost::optional and just use a nullable shared_ptr to achieve that.

Edited by dmatter, Today, 05:01 AM.


#10 Sean_Seanston   Members   -  Reputation: 522

Like
0Likes
Like

Posted Today, 01:05 PM

That's a very interesting and in-depth explanation... shows why people make such a big deal of pointers.

 

So basically... do it like I was going to with the raw pointer, and just use a smart pointer instead?

 

Think I'll just use Boost then, since I've used some of it in the past and I have it all set up in my project anyway from when I was going to use something that didn't pan out in the end.



#11 DoctorGlow   Members   -  Reputation: 914

Like
0Likes
Like

Posted Today, 02:29 PM

Of course it's up to you, but just for smart pointers I would not bring boost into a codebase, since C++ already provides them, YMMV.



#12 frob   Moderators   -  Reputation: 26773

Like
1Likes
Like

Posted Today, 04:25 PM

Raw pointers are for when you want a nullable, non-owning reference. The nullable property allows you to implement the optional semantic. But the non-owning property doesn't sound like it's what you want. So I doubt that a raw pointer is really the best solution here.

I read his discussion, use your same initial logic, and come to the opposite conclusion.
 
The Location may or may not own the object, perhaps the world map owns the object.  Either way, that is the parent of the object.
 
I'm looking at the ownership based on this:
 

Depending on whether or not the Location has a Safehouse when the player is interacting with the Location, the game/UI/menu/whatever will display different options to the player. i.e. Right now I'm working off the assumption that if a Menu should display certain things based on the existence of a Safehouse or not, then the Menu should be able to query somehow whether or not a valid Safehouse actually exists.

 
Effectively I see code that looks like this:
 
Safehouse *safehouse = location->GetSafehouse();
if(safehouse) {...}
 
The object greatly outlives the very short immediate usage, and the option of being null is a strong possibility. That is both nullable and non-owning, so a raw pointer is perfect.

 

For rendering, in all the engines and products I've worked with those pointers are completely unrelated to what is rendered. If you need to get something to display on a minimap or UI or menu, the code reaches in to a resource pool and retrieves a proxy for the object (which can be streamed in/out, cached, reused, translated, and so on). The proxy is owned by the corresponding resource store and also greatly outlives its usage, so the pointer to the proxy is passed to the minimap or UI or menu as needed.

 

 

 

Since the OP mentioned the option of returning a collection of objects, I would select that option only if the design allowed for more than one object. As described you can either return a single object or null.  If you need multiple you would return a collection with zero or more Safehouse pointers.


Check out my book, Game Development with Unity, aimed at beginners who want to build fun games fast.

Also check out my personal website at bryanwagstaff.com, where I write about assorted stuff.


#13 Pink Horror   Members   -  Reputation: 1541

Like
0Likes
Like

Posted Today, 05:07 PM

class Location
{
public:
    Location()
      : _safehouse(new Safehouse())
      , _bank(new Bank())
    { }

    Location(Safehouse * safehouse, Bank * bank)
      : _safehouse(safehouse)
      , _bank(bank)
    { }

    ~Location() {
        delete _safehouse;
        delete _bank;
    }

private:
    Safehouse * _safehouse;
    Bank * _bank;
};

 

I'm imagining a Location might eventually end up like this:

class Location
{
    Safehouse*  _safehouse;
    Bank*       _bank;
    Farm*       _farm;
    Barracks*   _barracks;
    Watchtower* _watchtower;
    // etc...
};

I'm curious, will this be a problem in your game? How many different things might be in a Location? Maybe if there are just SafeHouses it doesn't matter, but I hope this is not something that will be used for dozens of other optional buildings.



#14 dmatter   Crossbones+   -  Reputation: 3428

Like
1Likes
Like

Posted Today, 05:19 PM

 

Raw pointers are for when you want a nullable, non-owning reference. The nullable property allows you to implement the optional semantic. But the non-owning property doesn't sound like it's what you want. So I doubt that a raw pointer is really the best solution here.

I read his discussion, use your same initial logic, and come to the opposite conclusion.
 
The Location may or may not own the object, perhaps the world map owns the object.  Either way, that is the parent of the object.

 

Yeah, as you note, it is hard to know who owns what from the original post. But the OP gives us a clue us in a later post, as I quoted:
 

Really? Kewl. Maybe I wasn't so naive after all... making sure the destructor deleted the pointer was what I originally had assumed was the right (and only necessary) thing to do.

Based on that I came to the conclusion that if the OP was planning to have the destructor delete the pointer then the Location must be the owner of the object. So a non-owning pointer/reference would be inappropriate.

 

Effectively I see code that looks like this:
 
Safehouse *safehouse = location->GetSafehouse();
if(safehouse) {...}
 
The object greatly outlives the very short immediate usage, and the option of being null is a strong possibility. That is both nullable and non-owning, so a raw pointer is perfect.

Arguably the Location object could be sharing ownership of the Safehouse (rather than lacking any ownership) - For me it isn't clear either way from such a small snippet.
If you're saying that the lifetime of the Location object is always "within" the lifetime of the Shafehouse object then I can agree that Location doesn't absolutely need shared ownership so non-owning raw-pointers would suffice. However unless there's a good reason not to (e.g. necessary optimizations) then I would still prefer using shared_ptrs since shared ownership is easier to reason about and will be more resilient to future refactorings.

 

For rendering, in all the engines and products I've worked with those pointers are completely unrelated to what is rendered. If you need to get something to display on a minimap or UI or menu, the code reaches in to a resource pool and retrieves a proxy for the object (which can be streamed in/out, cached, reused, translated, and so on). The proxy is owned by the corresponding resource store and also greatly outlives its usage, so the pointer to the proxy is passed to the minimap or UI or menu as needed.

I'm on-board with the idea that if the overall design makes it readily apparent that an object has extreme longevity well beyond all uses then the rest of the system can deal in non-owning pointers without any risk. This typically applies to pool-managed resources like you say. The way I see it a pool is a centralised memory management approach where there is only 1 owner (the pool) and many non-owners (the rest of the system using non-owning raw-pointers) , whereas shared_ptrs are a de-centralised approach in which everybody must opt into the approach by using the shared_ptr instead of a raw pointer.


Edited by dmatter, Today, 05:19 PM.






PARTNERS