correct class member initialisation

Hi all,

First post here, thanks for reading !

Coming from python, I am trying to get my head around the usage of "new", specifically in relation to objects and constructors.
After reading up on "new", I do understand that one of it's main uses is to allow objects to exist after changing scope (It allocates heap memory and objects therefore do not get auto deleted when leaving scope).

So maybe I don't understand scope correctly yet, but let's look at some code.


A. Class member using stack memory:

1
2
3
4
5
6
7
8
9
class Car {
	Engine my_engine;
	
public:
	Car( int hp ) {
		my_engine = Engine(hp);
	}
	Engine get_engine() { return my_engine; }
};


B. Class member using heap memory:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Car {
	Engine * my_engine;
	
public:
	Car( int hp ) {
		my_engine = new Engine(hp);
	}
	~Car() {
		delete my_engine;
	}
	
	Engine get_engine() { return *my_engine; }
};



Either way I go, I can access engine the same way:

1
2
3
4
5
6
7
int main() {
	Car my_slow_car = Car(50);
	
	Engine my_engine = my_slow_car.get_engine();
	cout << my_engine.get_hp();
        return 0;
}



One thing I noticed is that with version B, I can get away without a default constructor for the Engine class, as I only have to declare a pointer at the top of the class.

So I guess my question is, when would you use B. over A. ?



PS: The engine class, just for completeness sake:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Engine {
	int hp;
	
public:	
	
	Engine( ) {
		hp = 10;
	}
	
	Engine( int a_hp ) {
		hp = a_hp;
	}
	
	int get_hp() {
		return hp;
	}
};
my_engine is declared inside Car and instantiated inside Car, or more accurately, it is instantiated within an instantiation of Car. Therefore, my_engine's scope is the same as the scope of the instance of Car. Car is instantiated in main(), therefore my_engine's scope in this case is main().

You would use B only if the constructed Engine instance needs to live beyond the lifetime of the Car instance that contains it.

(Having said that, there are times when pointers need to be used for other reasons, but I won't go into those. The bottom line is that pointers should be used only when you can't, for some reason (such as the lifetime issue), not use them).

One small correction/performance improvement to your code: Use initializer lists. Here is your constructor rewritten to use initializer lists:

1
2
3
4
5
6
7
8
9
class Car {
	Engine my_engine;
	
public:
	Car( int hp ) : my_engine( hp ) {
	}

	Engine get_engine() { return my_engine; }
};


This is more efficient because the compiler will, unless you tell it otherwise, run the default constructor of every data member of Car prior to entering the body of Car's constructor. In your code, you then call the constructor of Engine a second time, and then call operator= on Engine to assign the newly constructed instance to my_engine, making a total of three function calls. In the above code, the syntax after the colon is how you tell the compiler to run a different constructor for your data members. In this case, I'm telling it to call Engine( int ) instead of Engine(), making only one function call.

Just one other thing to be aware of, if you aren't already: get_engine() returns a copy of the Car's engine (ie, it run's Engine's copy constructor). Not sure if this is what you want. If you want the caller to be able to modify the Car's engine directly, you could return a reference. If you don't, you could return a const reference. In either case, returning a reference does not copy the Engine.
Hi jsmith,

thank you very much for the thorough explanation and the hints about the initializer list and the copied engine object, it clears up a few things.

So if I got it right, B would actually never make sense, as my_engine is private and it's not possible to access my_engine, only a copy of it.

You are mentioning that B would be required if my_engine was to "live on" after Car instances go out of scope, but then I don't really understand the use of the destructor ? Deleting my_engine because car gets deleted would be problematic if t's still used from outside the object instance.


Maybe it would be possible to post an example that actually requires B ? I'll try to come up with one later tonight myself ...


Thanks !
I would not expect to see either

1
2
3
4
5
6
7
8
9
class Car {
	Engine my_engine;
	
public:
	Car( int hp ) {
		my_engine = Engine(hp);
	}
	Engine get_engine() { return my_engine; }
};


or

1
2
3
4
5
6
7
int main() {
	Car my_slow_car = Car(50);
	
	Engine my_engine = my_slow_car.get_engine();
	cout << my_engine.get_hp();
        return 0;
}


in C++ code

jsmith's form (with the ref fix he also mentioned) is the C++ way to rework your first example.

For exactly the same reason (to call the constructor with the int param, rather than the default constructor followed by operator=), your second example should be

1
2
3
4
5
6
7
int main() {
	Car my_slow_car(50);
	
	Engine my_engine = my_slow_car.get_engine();
	cout << my_engine.get_hp();
        return 0;
}


(this is without the ref fix)

Andy

P.S. Modern optimising compilers might work out you mean

Car my_slow_car(50);

when you write

Car my_slow_car = car(50);

but you shouldn't count on it.



Last edited on
@Tom:

B could make sense in some scenarios. I alluded to there being cases where you must use pointers -- those would be some of those scenarios. Since you are just learning C++, I won't go into those details now. But here's a somewhat relevant scenario: suppose you could construct a Car object without an Engine, or suppose you could remove the Engine from the Car (say to replace it with a new one). In those cases, you have a Car with no Engine. But your Car object in A says a Car always has an Engine.

You are mentioning that B would be required if my_engine was to "live on" after Car instances go out of scope, but then I don't really understand the use of the destructor ? Deleting my_engine because car gets deleted would be problematic if t's still used from outside the object instance.


You are correct. But see my previous example: I might want to remove the Engine from the Car before destroying the Car, in which case Car no longer owns the Engine (I own it now).

@andy:

Actually, the standard requires compilers to implement Car my_car = Car(50); as one call to a constructor and no assignments. At least part of the reason is that because of the "most vexing parse" problem, you have no choice but to write the declaration/initialization like that, which is essentially saying "sorry programmer, but you have no choice but to write non-optimal code in this scenario".
@jsmith

Thanks for the info

I was aware that

Car my_car = 50;

would trigger the int constructor and not the default constructor + operator= (assuming it had been defined). But I hadn't picked up that this was guaranteed to extend to optimising out the extra constructor at the end.

I prob. read about it at some point, but I've been lucky enough to avoid the "vexing" proble so far (but not the confusion caused by fixing a copy constructor but not the operator=, or vice versa).
Topic archived. No new replies allowed.