Jump to content

  • Log In with Google      Sign In   
  • Create Account


C++ Constructors vs. Init() Functions


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

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

#1 blueskies9041   Members   -  Reputation: 135

Like
0Likes
Like

Posted 25 February 2014 - 12:41 PM

I'm learning game programming at AIE US - I've reviewed a lot of source code while developing my projects, I've noticed that some people use either a constructor or a plan old void (or bool) Init() function to initialize their object's member variables.

 

Is there a good reason to use one or the other? I know constructors can use initialization lists which has a few benefits that I don't fully understand yet .

 

Thanks :]



Sponsor:

#2 Danicco   Members   -  Reputation: 449

Like
2Likes
Like

Posted 25 February 2014 - 01:08 PM

The difference between them (obvious example inc!):

class MyClassCons
{
    public:
        MyClassCons()
        {
            Pointer* p = new Pointer();
        }
};

class MyClassInit
{
    public:
        MyClassInit();
        void Init()
        {
            Pointer* p = new Pointer();
        }
};

int main()
{
    MyClassConsc aClass;
    //p* already exists!

    MyClassInit bClass;
    //p* doesn't exist yet!
};

The problem with this is that sometimes p* could be a variable that depends on something ELSE to work.

Using a really bad example, say you put some OpenGL commands in your class' default constructor. Just by definining the class somewhere you'll get a runtime error because when the variable was created, right at the start of your program, the OpenGL context which is required before issuing glCommands wasn't active/didn't exist.

 

But you do could use Init(); since it would ONLY be called when you explicitly say so:

int main()
{
    MyClassInit aClass; //it's okay

    InitOpenGL();
    aClass.Init(); //okay, no runtime error
}

But you couldn't do the otherway around:

int main()
{
    InitOpenGL(); 

    MyClassCons aClass; //Still runtime error
}

Other than this situation (which I found to be sorta common), I think everything else is due preference...



#3 Aliii   Members   -  Reputation: 1311

Like
0Likes
Like

Posted 25 February 2014 - 01:28 PM


The problem with this is that sometimes p* could be a variable that depends on something ELSE to work.

 

Especially if you have static objects form "MyClass". You cant really solve that by creating the objects in the right order.



#4 BeerNutts   Crossbones+   -  Reputation: 2515

Like
7Likes
Like

Posted 25 February 2014 - 01:37 PM

Well, constructor's don't have any kind of return value, so, many people use an Init() type function that returns an error code of some sort.


My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

#5 Aliii   Members   -  Reputation: 1311

Like
0Likes
Like

Posted 25 February 2014 - 01:56 PM


Well, constructor's don't have any kind of return value, so, many people use an Init() type function that returns an error code of some sort.

 

The standard way would be to throw an exception from the constructor. (I dont do that either.)



#6 cozzie   Members   -  Reputation: 1408

Like
-3Likes
Like

Posted 25 February 2014 - 02:36 PM

A few other remarks on this choice:
- if you define your own constructor, the 'default' constructor, destructor and assignment operator are no longer applied from the compiler, meaning you need to write them yourself to (look up "the rule of three")
- when deciding this you should also think ahead if you want objects of the class to be reinitialized/ reused later and how you'll handle this (doing a own init function might need a cleanup partner :))

#7 achild   Crossbones+   -  Reputation: 1588

Like
2Likes
Like

Posted 25 February 2014 - 03:45 PM

In C++, constructors are really important for RAII. Note that they are required if you wish to follow the objects-are-never-in-an-invalid-state thought process.

 

Personally, if I have to have confirmation that "initialization was a success", I'll add an IsInit or IsValid function. These cases are very, very rare in real life situations. Also... once you start using smart pointers it will be silly to explicitly Init after creating the object...



#8 ferrous   Members   -  Reputation: 1445

Like
0Likes
Like

Posted 25 February 2014 - 09:30 PM

One setup I see often is having a private constructor and then some sort of static create function that returns a pointer to the class.



#9 Trienco   Crossbones+   -  Reputation: 2039

Like
2Likes
Like

Posted 25 February 2014 - 11:06 PM

- if you define your own constructor, the 'default' constructor, destructor and assignment operator are no longer applied from the compiler, meaning you need to write them yourself to (look up "the rule of three")

 

Unless somebody made really weird changes to the language, that's simply not true. Creating your own constructor will never prevent default implementations of anything but the default constructor.

 

The "Rule of Three " doesn't say "you must implement all of them, because the compiler doesn't create them", it says "if you need one, you probably need all three". In fact, the big problem IS that the default implementations don't do everything they should in that case.

 

Obvious example:

 


class Demo()

{

    Demo() : ptr(new Thing) {}

    ~Demo() { delete ptr; }

 

    Thing* ptr;

}

 

And you probably don't want to know how often I'm seeing this kind of thing, always hand-waved away with "nah, it's never going to be copied anyway". Unless it suddenly is and "someone" (typically me) gets to spend hours debugging and tracking down the double delete introduced months ago.

 

If the compiler actually would stop creating defaults for assignment and _copy_ constructor (which is the one relevant to the Rule of Three) the code would have the decency to stop compiling. You'd also have an army of C++ coders lynching the person that came up with it.

 

 

Another common rule is that the constructor should only do minimal work to get the object in a valid state. Any heavy lifting that isn't absolutely needed would then go into an init() function.


Edited by Trienco, 25 February 2014 - 11:06 PM.

f@dzhttp://festini.device-zero.de

#10 frob   Moderators   -  Reputation: 18371

Like
15Likes
Like

Posted 25 February 2014 - 11:24 PM

This question has been coming up a lot lately, so sorry if I sound too repetitive for this answer.

RAII is about creating objects that are ready to use.

Being ready to use does NOT mean that it is hooked up to resources, that it has allocated blobs of memory, that it is attached to other objects, or otherwise integrated into the rest of the system.

When I create a std::vector I expect an empty object that is ready instantly that I can fill up at my convenience. When I create a std::ifstream I expect an object that is not yet attached to a file, but can be attached later, and I expect it to be instantly available, ready for use.

From another thread, when I create a network socket I expect an unconnected and empty stream to be available instantly, ready for use.

From another thread, when I create a logger I expect a fully-built logging class, but I expect it to come back instantly; it is initially connected to zero output streams and requires no resources, but is ready for use.

From another thread, when I create a generic game object I expect a fully built but completely empty game object, instantly available for use.

Going back through each:

When I create a vector there might be an optional constructor that does more work, but it isn't strictly necessary; by default I instantly get an empty object that is fully initialized to empty.

When I create a network socket there might be an optional constructor that attaches to the server and does some work, but it isn't strictly necessary; by default I instantly get an object that is fully initialized to a disconnected state.

When I create a logger there might be an optional constructor that accepts existing listening streams and hooks them up, but it isn't strictly necessary; by default I instantly get an object that is fully initialized to an empty listener state.

When I create a specific game object or an array of game objects, I absolutely DO NOT WANT each of those constructors to spawn off calls to load models from disk and into the cache system, to load textures from disk and into the cache, to load animations from disk, to load audio from disk, to integrate all of them into the rendering engine, to position them in the spatial trees, and so on. If I created an array of 50 game objects I might be looking at multiple seconds before the constructor returns to me, and it is doing an awful lot more than just constructing an empty object immediately. By default I instantly get an object that is fully initialized to an empty state.

Object lifetimes are generally more complex than a simple creation and destruction process. Often there is a fully constructed empty state (the default constructor) that is available instantly. There are often various levels of connectedness within the system. Maybe an SQL connection has states of disconnected, waiting for connection to the server, logging in, waiting for credentials, transmitting commands, waiting for results, and busy with communications. Maybe your game object has empty, proxied and out of world, proxied and in world, loaded out of world, loaded in world, active, disabled, in use, and assorted other states.

RAII means to initialize things to be ready to use. "Empty" and "Disconnected" are perfectly valid definitions of ready to use, and for non-trivial objects are usually the best default.
Check out my personal indie blog at bryanwagstaff.com.

#11 slayemin   Members   -  Reputation: 2015

Like
0Likes
Like

Posted 26 February 2014 - 01:25 AM

I don't know if its the same in C++ as it is in C#, but I hit this problem today: If your class is inheriting from a base class, your default constructor will invoke the base class default constructor first. If your base class is depending on a specific variable to be allocated by the inheriting class object, you're going to get a null reference error in the base class.

-> create new ClassObject
	-> Set any variables to preset constant values
	-> ClassObject.CTOR(Settings)
		-> Set any base class variables to preset constant values
		-> base.CTOR() code run
	<- ClassObject.CTOR code run
<- ClassObject reference returned to caller

Where is the problem?

If any of the base.CTOR() fields need to be initialized with the settings values passed into the ClassObject, you are out of luck because the class constructor code is evaluated AFTER the base class code is run. You can not change the calling order of constructors.

However, if you include an "Initialize(Settings)" method, you can specify the call order:

1. -> create new ClassObject
	-> Set any variables to preset constant values
	-> ClassObject.CTOR()
		-> Set any base variables to preset constant values
		-> empty base CTOR run
	<- empty class object CTOR code run
   <- ClassObject reference returned to caller
	
2. -> ClassObject.Initialize(Settings)
	-> Set any object memory/variables
	-> base.initialize(Settings)
		->Set any base level memory/variables

3. -> ClassObject.LoadContent()
	-> Run any code necessary to pull external assets from disk into main memory 

I decided that I could consolidate all of my class initialization code into a constructor. As I found out, this was a bad design decision.

As Frob suggests above, you want to be able to create "empty" objects.

Here are the principles I try to follow:
-I should be able to create a new object which sets the object properties to initial values. The object is empty and ready to be initialized.

-When an object is initialized with settings, memory is allocated and variables are set.

-I should be able to call a "LoadContent()" method which pulls any external assets from disk into main memory. I should be able to call this as many times as I want, but content is pulled from disk only once.

-I should be able to call an "UnloadContent()" method which releases the external assets from main memory. The object state is returned to the same state it was in before LoadContent() was called.

-To prove this, I should be able to call LoadContent() -> UnloadContent() -> LoadContent() as many times as I want without breaking the objects usability.

 

-Optional: You can use lazy initialization if you keep a boolean flag to keep track of whether or not object content is loaded. The game tries to use the object, realizes the external assets aren't loaded, goes to load the assets, and then resumes usage of the object (I think the RIFT MMORPG uses this technique).


Edited by slayemin, 26 February 2014 - 01:25 AM.

Eric Nevala

Currently a self-employed indie game dev


#12 DarkRonin   Members   -  Reputation: 600

Like
0Likes
Like

Posted 26 February 2014 - 02:38 AM

As BeerNutts pointed out - 'return codes'.

If you have initialisation code in your constructor that requires some sort of error handling it is harder to determine if the call failed.

#13 cdoubleplusgood   Members   -  Reputation: 780

Like
2Likes
Like

Posted 26 February 2014 - 03:32 AM


If your base class is depending on a specific variable to be allocated by the inheriting class object, you're going to get a null reference error in the base class.

This seems to be a strange design. Can you give a more concrete example? Why would the base class know about variables of the derived class?



#14 Hodgman   Moderators   -  Reputation: 26990

Like
5Likes
Like

Posted 26 February 2014 - 04:13 AM

I've noticed that some people use either a constructor or a plan old void (or bool) Init() function to initialize their object's member variables.

Is there a good reason to use one or the other?

Some people know C better than they know C++, so they use C idioms instead of using C++ properly tongue.png 
 

As BeerNutts pointed out - 'return codes'.

If you have initialisation code in your constructor that requires some sort of error handling it is harder to determine if the call failed.

1) Out params. e.g. new Foo(1,2,3, &error);
2) Exceptions.
3) Factory methods e.g. Foo::New(1,2,3) may return NULL.
4) This usually isn't even a concern -- most objects can't fail initialization.
 

If your base class is depending on a specific variable to be allocated by the inheriting class object, you're going to get a null reference error in the base class.

This seems to be a strange design. Can you give a more concrete example? Why would the base class know about variables of the derived class?

Indeed. This is either an abuse of inheritance for the purposes of extension/code-reuse (that's the job of composition), or it's solved by:

//old
class Base { Base() { use buffer here, its uninitialized, crash! } int* buffer; }
class Derived { Derived() { buffer = new int; } } -- Derived initializes bases members. Thats ugly.

//fixed
class Base { Base( int* buffer, int size ) : buffer(buffer) { use of buffer here is fine } int* buffer; }
class Derived { Derived() : Base( new int[2], 2 ); }


#15 DarkRonin   Members   -  Reputation: 600

Like
0Likes
Like

Posted 26 February 2014 - 04:27 AM

I've noticed that some people use either a constructor or a plan old void (or bool) Init() function to initialize their object's member variables.
Is there a good reason to use one or the other?

Some people know C better than they know C++, so they use C idioms instead of using C++ properly :P

As BeerNutts pointed out - 'return codes'.
If you have initialisation code in your constructor that requires some sort of error handling it is harder to determine if the call failed.

1) Out params. e.g. new Foo(1,2,3, &error);
2) Exceptions.
3) Factory methods e.g. Foo::New(1,2,3) may return NULL.
4) This usually isn't even a concern -- most objects can't fail initialization.

I was thinking more of the DirectX point of view. In my case, I have a lot of DX initialisation in my class and, as we know, there is plenty that can go wrong there.

#16 slayemin   Members   -  Reputation: 2015

Like
0Likes
Like

Posted 26 February 2014 - 04:36 AM

 


If your base class is depending on a specific variable to be allocated by the inheriting class object, you're going to get a null reference error in the base class.

This seems to be a strange design. Can you give a more concrete example? Why would the base class know about variables of the derived class?

 

Yeah, I kind of thought so to. Here is an abbreviated code sample:

 

public class AssetDB
{
   ContentManager m_content;
   public AssetDB(ContentManager content)
   {
       m_content = content;
   }
}

public class WorldSettings
{
   public ContentManager m_content;
}

public abstract class GenericGameWorld
{
   AssetDB m_assetDB;
   WorldSettings m_settings;

   public GenericGameWorld()
   {
       m_assetDB = new AssetDB(m_settings.m_content);  //<-- not set
   }
}

public class TacticalGameWorld : GenericGameWorld
{
   public TacticalWorld(WorldSettings settings)
   {
       m_settings = settings;   //base constructor called before this runs
   }
}

public class GameMain : Game
{
    TacticalWorld m_tacticalWorld;
    WorldSettings m_settings;

    public GameMain()
    {
        m_settings.m_content = this.Content;
        m_tacticalWorld = new TacticalWorld(m_settings);
    }
}

In hindsight, I *could* just new the assetDB in the classes which implement the GenericGameWorld class, but logically it seems that if a class owns a field, then it should also manage it so that there is loose coupling between classes.


Eric Nevala

Currently a self-employed indie game dev


#17 Hodgman   Moderators   -  Reputation: 26990

Like
4Likes
Like

Posted 26 February 2014 - 04:53 AM

I was thinking more of the DirectX point of view. In my case, I have a lot of DX initialisation in my class and, as we know, there is plenty that can go wrong there.

The vast majority of D3D errors are usage errors from invalid code, rather than actual problems to be expected normally. These kinds of errors should be fed into an assert statement that crashes in debug/development builds and omitted completely in tested/shipping builds. ;)

#18 Juliean   GDNet+   -  Reputation: 2206

Like
0Likes
Like

Posted 26 February 2014 - 05:22 AM

One reason why an Init could be better is that you can safely call virtual functions here, whereas that is not possibly in the constructor/destructor. I had a issue just a few days ago where for the first time I kind of regreted using constructors:

class BaseEffect : IEffect
{
public:
            BaseEffect(ApiEffect& effect): m_pEffect
            {
                  // OnChangeEffect should be called here, but can't
            }

            ~BaseEffect()
            {
                   // OnDeleteEffect should be called here but once again, can't.
            }

            virtual void OnChangeEffect(ApiEffect&effect) = 0;
            virtual void OnDeleteEffect(ApiEffect& effect) = 0;

private:

           ApiEffect* m_pEffect;
}

class DX9Effect final: BaseEffect
{ 
             DX9Effect(ApiEffect& effect) : BaseEffect(effect)
             {
                   // due to this, I have to call OnChangeEffect explicitely in each implementation here,
                   // which is prone to error and produces dublicated code
                   OnChangeEffect(effect);
             }

             ~DX9Effect()
             {
                   // same here. I even have to speificially ad a getter for m_pEffect so that I can delete it here
                   OnDeleteEffect(EffectUnsafeGetter());
             }

             void OnChangeEffect(ApiEffect& effect)  override
             {
                      m_states.Add<BindShader>(effect.GetShader());
             }

             void OnDeleteEffect(ApiEffect& effect) override
             {
                       delete &effect;
             }
}

Since ApiEffect is a typedef that is only forward-declared at "BaseEffect", I have to have a method for implementations to specifiy deletion, and also each effect derived class  needs specific behaviour on initialization, but unfortunately this cannot be realized via ctor/dtor, since the vtable is not fully utilized at this point, forcing me to write some unnecessary code for every implementation. One reason why you could prefer "Init/Deinit" in a specific situation.



#19 cdoubleplusgood   Members   -  Reputation: 780

Like
0Likes
Like

Posted 26 February 2014 - 06:40 AM

In hindsight, I *could* just new the assetDB in the classes which implement the GenericGameWorld class, but logically it seems that if a class owns a field, then it should also manage it so that there is loose coupling between classes.


What about this:

    public abstract class GenericGameWorld
    {
       AssetDB m_assetDB;

       protected GenericGameWorld(WorldSettings settings)
       {
           m_assetDB = new AssetDB(settings.m_content);
       }
    }

    public class TacticalGameWorld : GenericGameWorld
    {
        WorldSettings m_settings;

        public TacticalGameWorld(WorldSettings settings)
            : base(settings)
       {
           m_settings = settings;   //base constructor called before this runs
       }
    }


#20 aregee   Members   -  Reputation: 676

Like
0Likes
Like

Posted 26 February 2014 - 10:01 AM

 


Well, constructor's don't have any kind of return value, so, many people use an Init() type function that returns an error code of some sort.

 

The standard way would be to throw an exception from the constructor. (I dont do that either.)

 

 

I would never do anything "complicated" inside the constructor, like loading images, setting up textures, etc.  I would just initialise member variables to a known state and leave the other stuff for an init function/method or using getters/setters, depending on the situation.  I wouldn't do anything that could throw exceptions inside a constructor.  Throwing an exception inside a constructor sounds like a bad thing to me.  Is this really the standard way?






Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS