Initializing Base and Derived Classes
Pass arguments from derived constructors to base class constructors.
How Do Constructors Initialize Derived Classes?
A derived object is two objects stacked into one allocation: the base portion and the portion the derived class adds. Two constructors run to fill it, and the language is strict about which one is allowed to touch which members. Every member is initialized by a constructor of the class that declares it, and never by anyone else.
That single rule is what makes a derived constructor different from an ordinary one. An ordinary constructor initializes its members. A derived constructor initializes its own members and chooses which base constructor runs before it, because choosing is the only influence it has over the inherited half.
Every Member Has Exactly One Constructor That May Initialize It
Take a pair of classes for a music catalogue. Track holds a title and a running time; RemixTrack inherits from it and adds the person who produced the remix:
| Member | Declared in | May be initialized by | How RemixTrack's constructor influences it |
|---|---|---|---|
m_title |
Track |
a Track constructor |
indirectly, by picking which Track constructor runs and what it is passed |
m_seconds |
Track |
a Track constructor |
indirectly, the same way |
m_remixer |
RemixTrack |
a RemixTrack constructor |
directly, in its own member initializer list |
Read the third column as a prohibition, not a convention. RemixTrack's member initializer list cannot name m_title at all, even though every RemixTrack object plainly contains one. The only handle it has on the inherited half is the base constructor call.
Naming a Base Constructor in the Initializer List
The syntax is the class name, followed by the arguments, in the derived constructor's member initializer list:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
private:
std::string m_title;
int m_seconds{};
public:
Track(std::string_view title, int seconds)
: m_title{title}
, m_seconds{seconds}
{
std::cout << "Track portion ready: " << m_title << '\n';
}
const std::string& getTitle() const { return m_title; }
int getSeconds() const { return m_seconds; }
};
class RemixTrack : public Track
{
private:
std::string m_remixer;
public:
RemixTrack(std::string_view title, int seconds, std::string_view remixer)
: Track{title, seconds} // pick the Track constructor that takes these two values
, m_remixer{remixer}
{
std::cout << "RemixTrack portion ready: " << m_remixer << '\n';
}
const std::string& getRemixer() const { return m_remixer; }
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", 284, "Sable Lane"};
std::cout << nightHarbour.getTitle() << ", "
<< nightHarbour.getSeconds() << " seconds, remixed by "
<< nightHarbour.getRemixer() << '\n';
return 0;
}
This prints:
Track portion ready: Night Harbour
RemixTrack portion ready: Sable Lane
Night Harbour, 284 seconds, remixed by Sable Lane
The first two lines are the whole construction sequence made visible. Constructing nightHarbour allocates enough memory for both portions, enters RemixTrack's constructor, and immediately suspends it to run Track{title, seconds}. Track's member initializer list sets m_title and m_seconds, Track's body runs, and only then does RemixTrack initialize m_remixer and run its own body.
Notice that both members are private and nothing about that is inconvenient. The derived class never needed access to them: it passes values to the constructor that owns them and reads them back through getTitle() and getSeconds().
Name the base constructor explicitly in the derived constructor's member initializer list whenever the base has anything worth initializing. Relying on the default base constructor is only appropriate when the base's default state is genuinely the state you want.
When No Base Constructor Is Named
Leaving the base out of the initializer list is not an error by itself. It is a request for the base's default constructor, made silently. When the base has no default constructor, that request fails. This program will not compile:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
private:
std::string m_title;
int m_seconds{};
public:
Track(std::string_view title, int seconds)
: m_title{title}
, m_seconds{seconds}
{
}
const std::string& getTitle() const { return m_title; }
};
class RemixTrack : public Track
{
private:
std::string m_remixer;
public:
RemixTrack(std::string_view remixer)
: m_remixer{remixer} // no Track constructor named, so Track() is required
{
}
};
int main()
{
RemixTrack nightHarbour{"Sable Lane"};
std::cout << nightHarbour.getTitle() << '\n';
return 0;
}
The compiler reports:
s.cpp: In constructor 'RemixTrack::RemixTrack(std::string_view)':
s.cpp:28:28: error: no matching function for call to 'Track::Track()'
The error is raised against a call the code never wrote, which is the point worth taking away: the base constructor call is always there. Omitting it does not skip it, it just picks the argument-free one.
The Two Detours Learners Try First
Faced with "I need to set m_title when I build a RemixTrack", there are two obvious moves before the base constructor call occurs to anyone. Both are worth walking through, because the second one compiles.
| Attempt | Outcome | What goes wrong |
|---|---|---|
| Name the inherited member in the derived initializer list | compile error | a member initializer list may only name members of its own class |
| Assign to the inherited member in the derived constructor body | compiles, and appears to work | the member is set twice, the base cannot see the intended value, and it fails outright on const or reference members |
| Pass the value to a base constructor | correct | the class that owns the member initializes it, once, with the right value |
The first attempt looks reasonable and reads well. It does not compile:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
public:
std::string m_title;
int m_seconds{};
Track(std::string_view title, int seconds)
: m_title{title}
, m_seconds{seconds}
{
}
};
class RemixTrack : public Track
{
public:
std::string m_remixer;
RemixTrack(std::string_view title, int seconds, std::string_view remixer)
: m_title{title} // will not compile
, m_seconds{seconds} // will not compile
, m_remixer{remixer}
{
}
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", 284, "Sable Lane"};
std::cout << nightHarbour.m_title << '\n';
return 0;
}
s.cpp: In constructor 'RemixTrack::RemixTrack(std::string_view, int, std::string_view)':
s.cpp:24:11: error: class 'RemixTrack' does not have any field named 'm_title'
24 | : m_title{title} // will not compile
| ^~~~~~~
s.cpp:25:11: error: class 'RemixTrack' does not have any field named 'm_seconds'
25 | , m_seconds{seconds} // will not compile
| ^~~~~~~~~
"Does not have any field named m_title" is worth reading literally. As far as the initializer list is concerned, RemixTrack really has no such field. m_title belongs to the base subobject, and the base subobject is initialized as a unit by a Track constructor, not member by member from the outside. The members here are public and it still fails, so access has nothing to do with it.
The second attempt moves the same idea into the constructor body, where it becomes an assignment rather than an initialization. That does compile:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
public:
std::string m_title;
int m_seconds{};
Track()
{
std::cout << "Track constructor sees m_seconds = " << m_seconds << '\n';
}
};
class RemixTrack : public Track
{
public:
std::string m_remixer;
RemixTrack(std::string_view title, int seconds, std::string_view remixer)
: m_remixer{remixer}
{
m_title = title; // assignment, not initialization
m_seconds = seconds; // assignment, not initialization
std::cout << "RemixTrack constructor sets m_seconds = " << m_seconds << '\n';
}
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", 284, "Sable Lane"};
std::cout << nightHarbour.m_title << '\n';
return 0;
}
This prints:
Track constructor sees m_seconds = 0
RemixTrack constructor sets m_seconds = 284
Night Harbour
The first line is the damage. m_seconds was already initialized to 0 by the time Track's constructor ran, and 284 did not arrive until the base was finished and out of the picture. Everything the base might have wanted to do with that value, validating it, deriving another member from it, or logging it, happened against a placeholder. The member is also written twice for no benefit, and if it were declared const or as a reference, the assignment would not compile at all.
Why the Restriction Exists
The const case is the clearest justification for the rule, and it is easy to see once the member is spelled that way. This will not compile:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
public:
std::string m_title;
const int m_seconds{}; // const: there is exactly one chance to give this a value
Track()
{
}
};
class RemixTrack : public Track
{
public:
std::string m_remixer;
RemixTrack(std::string_view title, int seconds, std::string_view remixer)
: m_remixer{remixer}
{
m_title = title;
m_seconds = seconds; // will not compile
}
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", 284, "Sable Lane"};
std::cout << nightHarbour.m_title << '\n';
return 0;
}
s.cpp: In constructor 'RemixTrack::RemixTrack(std::string_view, int, std::string_view)':
s.cpp:25:19: error: assignment of read-only member 'Track::m_seconds'
A const member gets its value once, at the moment it is created, and it is created by its own class's constructor. Now imagine the language did permit derived classes to initialize inherited members. Track would set m_seconds while creating it, and then every derived class in the hierarchy would get a turn at the same variable afterwards, each with its own idea of the right value. References raise the same problem: they must be bound where they are created and can never be rebound.
Restricting initialization to the owning class guarantees each member is initialized exactly once, whatever the depth of the hierarchy. The derived class influences the result by choosing a base constructor and supplying its arguments, which happens before the base member exists rather than after.
Written Order Versus Execution Order
The base constructor call is not a statement that runs where it sits. It runs first no matter where it appears. Listing it after a member on purpose demonstrates this, and also shows what the compiler thinks of the idea:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
private:
std::string m_title;
public:
Track(std::string_view title)
: m_title{title}
{
std::cout << "Track portion ready\n";
}
const std::string& getTitle() const { return m_title; }
};
class RemixTrack : public Track
{
private:
std::string m_remixer;
public:
RemixTrack(std::string_view title, std::string_view remixer)
: m_remixer{remixer} // written first
, Track{title} // written last, still runs first
{
std::cout << "RemixTrack portion ready\n";
}
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", "Sable Lane"};
std::cout << nightHarbour.getTitle() << '\n';
return 0;
}
Compiling and running it gives:
s.cpp: In constructor 'RemixTrack::RemixTrack(std::string_view, std::string_view)':
s.cpp:23:17: warning: 'RemixTrack::m_remixer' will be initialized after [-Wreorder]
23 | std::string m_remixer;
| ^~~~~~~~~
s.cpp:28:22: warning: base 'Track' [-Wreorder]
28 | , Track{title} // written last, still runs first
| ^
s.cpp:26:5: warning: when initialized here [-Wreorder]
26 | RemixTrack(std::string_view title, std::string_view remixer)
| ^~~~~~~~~~
Track portion ready
RemixTrack portion ready
Night Harbour
Track still went first. The initializer list is a set of instructions, not a sequence: bases are initialized before members, and members in the order they are declared in the class, regardless of the order they are written in. Because a list that reads in a different order than it executes is a reliable source of bugs, GCC warns about the mismatch under -Wall. Write the base call first and the warning goes away along with the confusion.
Private Base Members Stay Private
Inheritance does not widen access. A derived class member function has no more right to a private base member than any unrelated code does. This member function will not compile:
#include <iostream>
#include <string>
#include <string_view>
class Track
{
private:
std::string m_title;
public:
Track(std::string_view title)
: m_title{title}
{
}
const std::string& getTitle() const { return m_title; }
};
class RemixTrack : public Track
{
private:
std::string m_remixer;
public:
RemixTrack(std::string_view title, std::string_view remixer)
: Track{title}
, m_remixer{remixer}
{
}
void describe() const
{
std::cout << m_title << " remixed by " << m_remixer << '\n'; // will not compile
}
};
int main()
{
RemixTrack nightHarbour{"Night Harbour", "Sable Lane"};
nightHarbour.describe();
return 0;
}
s.cpp: In member function 'void RemixTrack::describe() const':
s.cpp:33:22: error: 'std::string Track::m_title' is private within this context
33 | std::cout << m_title << " remixed by " << m_remixer << '\n'; // will not compile
| ^~~~~~~
The fix is the public access function the base already provides: write getTitle() instead of m_title. This is why keeping base members private costs a derived class nothing. Values go in through a base constructor and come back out through accessors, and neither route requires the derived class to reach into the base's storage.
Longer Chains Pass the Job Upward
Nothing changes when the hierarchy is three deep, because each class deals only with the class directly above it. Adding a destructor to each class also makes the return trip visible:
#include <iostream>
class Enclosure
{
public:
Enclosure(int serial)
{
std::cout << "Enclosure " << serial << " built\n";
}
~Enclosure()
{
std::cout << "Enclosure torn down\n";
}
};
class Cabinet : public Enclosure
{
public:
Cabinet(int serial, int watts)
: Enclosure{serial}
{
std::cout << "Cabinet " << watts << "W built\n";
}
~Cabinet()
{
std::cout << "Cabinet torn down\n";
}
};
class StageAmp : public Cabinet
{
public:
StageAmp(int serial, int watts, char channel)
: Cabinet{serial, watts}
{
std::cout << "StageAmp channel " << channel << " built\n";
}
~StageAmp()
{
std::cout << "StageAmp torn down\n";
}
};
int main()
{
StageAmp mainStack{7301, 60, 'B'};
return 0;
}
This prints:
Enclosure 7301 built
Cabinet 60W built
StageAmp channel B built
StageAmp torn down
Cabinet torn down
Enclosure torn down
main calls StageAmp's constructor, which calls Cabinet's, which calls Enclosure's. Enclosure inherits from nothing, so it is the first constructor body to finish, and construction then completes downward: Enclosure, Cabinet, StageAmp.
StageAmp had to route the serial number through Cabinet to get it there. It could not shortcut to its grandparent, because a constructor may only name a direct base. This will not compile:
#include <iostream>
class Enclosure
{
public:
Enclosure(int serial)
{
std::cout << "Enclosure " << serial << " built\n";
}
};
class Cabinet : public Enclosure
{
public:
Cabinet(int serial, int watts)
: Enclosure{serial}
{
std::cout << "Cabinet " << watts << "W built\n";
}
};
class StageAmp : public Cabinet
{
public:
StageAmp(int serial, int watts, char channel)
: Enclosure{serial} // will not compile: Enclosure is not the immediate base
, Cabinet{serial, watts}
{
std::cout << "StageAmp channel " << channel << " built\n";
}
};
int main()
{
StageAmp mainStack{7301, 60, 'B'};
return 0;
}
s.cpp: In constructor 'StageAmp::StageAmp(int, int, char)':
s.cpp:26:11: error: type 'Enclosure' is not a direct base of 'StageAmp'
Each class is responsible for its own parent and no further. That keeps the chain composable: Cabinet can change how it initializes Enclosure without a single StageAmp needing to know.
Destruction Unwinds in the Opposite Order
The last three lines of the amplifier output are destructors, and they run most-derived first: StageAmp, then Cabinet, then Enclosure. The ordering is not arbitrary. A derived destructor may still need the base portion intact while it cleans up, so the base is kept alive until the derived class is finished with it. Construction builds from the base outward; destruction dismantles from the outside in.
If a base class has virtual functions, give it a virtual destructor. Without one, destroying a derived object through a base class pointer is undefined behaviour, and in practice the derived destructor never runs. Virtual destructors are covered in their own lesson.
Looking Forward
Access specifiers are the subject of the next lesson, which introduces protected and explains what public, private, and protected inheritance each do to the accessibility of inherited members. After that come the lessons on adding, overriding, and hiding inherited functions, which is where a derived class starts changing behaviour rather than just adding data.
Key Terminology
- Base portion: the subobject inside a derived object that corresponds to the base class
- Direct base: the class a derived class inherits from immediately, the only base its constructor may name
- Member initializer list: the clause after the colon in a constructor, where members and the base are initialized
- Access function: a public member function such as
getTitle()that exposes a private member in a controlled way
Summary
- A derived object contains a base portion, and each class's constructor initializes only the members its own class declares
- A derived constructor's extra job is to choose a base constructor and supply its arguments, written as
Derived(params) : Base{args}, m_member{value} { } - A member initializer list cannot name an inherited member, whatever its access level, because the base subobject is initialized as a unit
- Omitting the base from the initializer list requests the base's default constructor; if there is no default constructor, the code will not compile
- Assigning to an inherited member in the derived constructor body compiles, but sets the member twice, hides the intended value from the base during its own construction, and is impossible for
constor reference members - The base is constructed first regardless of where its call appears in the initializer list, and GCC warns when the written order does not match the execution order
- A derived class cannot touch private base members directly, so it reads them through the base's public access functions
- In a chain, a constructor may only name its direct base, and each class is responsible for initializing its own parent
- Destructors run in reverse order of construction, most-derived first, and a base with virtual functions needs a virtual destructor
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Initializing Base and Derived Classes - Quiz
Test your understanding of the lesson.
Practice Exercises
Vehicle Registration System
Create a Vehicle base class with registration number and year. Create a Car derived class that adds the number of doors. The Car constructor must properly initialize both base and derived class members.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!