Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 7.0.0) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
When programming in C, programming problems are commonly approached using
a top-down structured approach: functions and actions of the program are
defined in terms of sub-functions, which again are defined in
sub-sub-functions, etc.. This yields a
hierarchy of code: main()
at
the top, followed by a level of functions which are called from main()
,
etc..
In C++ the dependencies between code and data is also frequently defined in terms of dependencies among classes. This looks like composition (see section 6.4), where objects of a class contain objects of another class as their data. But the relation described here is of a different kind: a class can be defined in terms of an older, pre-existing, class. This produces a new class having all the functionality of the older class, and additionally introducing its own specific functionality. Instead of composition, where a given class contains another class, we here refer to derivation, where a given class is another class.
Another term for derivation is inheritance: the new class inherits the functionality of an existing class, while the existing class does not appear as a data member in the definition of the new class. When discussing inheritance the existing class is called the base class, while the new class is called the derived class.
Derivation of classes is often used when the methodology of C++ program development is fully exploited. In this chapter we will first address the syntactical possibilities offered by C++ for deriving classes from other classes. Then we will address some of the resulting possibilities.
As we have seen in the introductory chapter (see section 2.4), in the object-oriented approach to problem solving classes are identified during the problem analysis, after which objects of the defined classes represent entities of the problem at hand. The classes are placed in a hierarchy, where the top-level class contains the least functionality. Each new derivation (and hence descent in the class hierarchy) adds new functionality compared to yet existing classes.
In this chapter we shall use a simple vehicle classification system to build a
hierarchy of classes. The first class is Vehicle
, which implements as its
functionality the possibility to set or retrieve the weight of a vehicle. The
next level in the object hierarchy are land-, water- and air vehicles.
The initial object hierarchy is illustrated in Figure 12.
Auto
is a special case of a Land
vehicle, which in turn is a
special case of a Vehicle
.
The class Vehicle
is thus the `
greatest common denominator' in the
classification system. For the sake of the example in this class we implement
the functionality to store and retrieve the vehicle's weight:
class Vehicle { size_t d_weight; public: Vehicle(); Vehicle(size_t weight); size_t weight() const; void setWeight(size_t weight); };Using this class, the vehicle's weight can be defined as soon as the corresponding object has been created. At a later stage the weight can be re-defined or retrieved.
To represent vehicles which travel over land, a new class Land
can be
defined with the functionality of a Vehicle
, while adding its own specific
information and functionality. Assume that we are interested in the speed of
land vehicles and in their weights. The relationship between Vehicle
s
and Land
s could of course be represented using composition, but that would
be awkward: composition would suggest that a Land
vehicle contains a
vehicle, while the relationship should be that the Land
vehicle is a
special case of a vehicle.
A relationship in terms of composition would also needlessly bloat our code.
E.g., consider the following code fragment which shows a class Land
using
composition (only the setWeight()
functionality is shown):
class Land { Vehicle d_v; // composed Vehicle public: void setWeight(size_t weight); }; void Land::setWeight(size_t weight) { d_v.setWeight(weight); }Using composition, the
setWeight()
function of the class Land
only
serves to pass its argument to Vehicle::setWeight()
. Thus, as far as
weight handling is concerned, Land::setWeight()
introduces no extra
functionality, just extra code. Clearly this code duplication is superfluous:
a Land
should be a Vehicle
; it should not contain
a Vehicle
.
The intended relationship is achieved better by
inheritance:
Land
is derived from Vehicle
, in which Vehicle
is the
derivation's
base class:
class Land: public Vehicle { size_t d_speed; public: Land(); Land(size_t weight, size_t speed); void setspeed(size_t speed); size_t speed() const; };By postfixing the class name
Land
in its definition by : public
Vehicle
the
derivation is realized: the class Land
now contains all
the functionality of its base class Vehicle
plus its own specific
information and functionality. The extra functionality consists of a
constructor with two arguments and interface functions to access the speed
data member. In the above example
public derivation is used.
C++ also supports
private derivation and
protected derivation. In section
13.6 their differences are discussed. A simple example
showing the possibilities of of the derived class Land
is:
Land veh(1200, 145); int main() { cout << "Vehicle weighs " << veh.weight() << endl << "Speed is " << veh.speed() << endl; }This example shows two features of derivation. First,
weight()
is not
mentioned as a member in Land
's interface. Nevertheless it is used in
veh.weight()
. This member function is an implicit part of the class,
inherited from its `
parent' vehicle.
Second, although the derived class Land
now contains the functionality of
Vehicle
, the
private fields of Vehicle
remain private: they can
only be accessed by Vehicle
's own member functions. This means that
Land
's member functions must use interface functions (like
weight()
and setWeight()
) to address the weight
field, just as any
other code outside the Vehicle
class. This restriction is necessary to
enforce the principle of
data hiding. The class Vehicle
could, e.g., be
recoded and recompiled, after which the program could be relinked. The class
Land
itself could remain unchanged.
Actually, the previous remark is not quite right: If the
internal organization of Vehicle
changes, then the internal
organization of Land
objects, containing the data of Vehicle
, changes
as well. This means that objects of the Land
class, after changing
Vehicle
, might require more (or less) memory than before the
modification. However, in such a situation we still don't have to worry about
member functions of the parent class (Vehicle
) in the class Land
. We
might have to recompile the Land
sources, though, as the relative
locations of the
data members within the Land
objects will have changed
due to the modification of the Vehicle
class.
As a rule of thumb, classes which are derived from other classes must be fully recompiled (but don't have to be modified) after changing the data organization, i.e., the data members, of their base classes. As adding new member functions to the base class doesn't alter the data organization, no recompilation is needed after adding new member functions. (A subtle point to note, however, is that adding a new member function that happens to be the first virtual member function of a class results in a new data member: a hidden pointer to a table of pointers to virtual functions. So, in this case recompilation is also necessary, as the class's data members have been silently modified. This topic is discussed further in chapter 14).
In the following example we assume that the class Auto
, representing
automobiles, should contain the weight, speed and name of a car.
This class is conveniently derived from Land
:
class Auto: public Land { char *d_name; public: Auto(); Auto(size_t weight, size_t speed, char const *name); Auto(Auto const &other); ~Auto(); Auto &operator=(Auto const &other); char const *name() const; void setName(char const *name); };In the above class definition,
Auto
is derived from Land
, which in
turn is derived from Vehicle
. This is called
nested derivation:
Land
is called Auto
's
direct base class, while Vehicle
is
called the
indirect base class.
Note the presence of a
destructor, a
copy constructor and an
overloaded assignment operator in the class Auto
. Since this class
uses a pointer to reach
dynamically allocated memory, these members should
be part of the
class interface.
As will be clear from the definition of the class Land
, a
constructor
exists to set both the weight
and the speed
of an object. The
poor-man's implementation of this constructor could be:
Land::Land (size_t weight, size_t speed) { setWeight(weight); setspeed(speed); }This implementation has the following disadvantage. The C++ compiler will generate code calling the base class's default constructor from each constructor in the derived class, unless explicitly instructed otherwise. This can be compared to the situation we encountered in composed objects (see section 6.4).
Consequently, in the above implementation the
default constructor of Vehicle
is called, which probably
initializes the weight of the vehicle, only to be redefined immediately
thereafter by the function setWeight()
.
A more efficient approach is of course to call the constructor of
Vehicle
expecting an size_t weight
argument directly. The syntax
achieving this is to mention the constructor to be called (supplied with its
arguments) immediately following the argument list of the constructor of the
derived class itself. Such a
base class initializer is shown in the next
example. Following the constructor's head a colon appears, which is then
followed by the base class constructor. Only then any member initializer may
be specified (using commas to separate multiple initializers), followed by the
constructor's body:
Land::Land(size_t weight, size_t speed) : Vehicle(weight) { setspeed(speed); }
class Base { public: ~Base(); }; class Derived: public Base { public: ~Derived(); }; int main() { Derived derived; }At the end of the
main()
function, the derived
object ceases to
exists. Hence, its
destructor (~Derived()
) is called. However, since
derived
is also a Base
object, the ~Base()
destructor is called as
well. It is not neccessary to call the base class destructor explicitly
from the derived class destructor.
Constructors
and
destructors are called in a stack-like fashion: when derived
is
constructed, the appropriate base class constructor is called first, then the
appropriate derived class constructor is called. When the object derived
is destroyed, its destructor is called first, automatically followed by the
activation of the Base
class destructor. A
derived class destructor is
always called before its
base class destructor is called.
Let's assume that the vehicle classification system should be able to
represent trucks, consisting of two parts: the front engine, pulling the
second part, a trailer. Both the front engine and the trailer have their own
weights, and the weight()
function should return the combined weight.
The definition of a Truck
therefore starts with the class definition,
derived from Auto
but it is then expanded to hold one more size_t
field
representing the additional weight information. Here we choose to represent
the weight of the front part of the truck in the Auto
class and to store
the weight of the trailer in an additional field:
class Truck: public Auto { size_t d_trailer_weight; public: Truck(); Truck(size_t engine_wt, size_t speed, char const *name, size_t trailer_wt); void setWeight(size_t engine_wt, size_t trailer_wt); size_t weight() const; }; Truck::Truck(size_t engine_wt, size_t speed, char const *name, size_t trailer_wt) : Auto(engine_wt, speed, name) { d_trailer_weight = trailer_wt; }Note that the class
Truck
now contains two functions already
present in the base class Auto
: setWeight()
and weight()
.
setWeight()
poses no problems: this
function is simply redefined to perform actions which are specific to a
Truck
object.
setWeight()
, however, will
hide
Auto::setWeight()
: for a Truck
only the setWeight()
function
having two size_t
arguments can be used.
Vehicle
's setWeight()
function remains available for a
Truck
, but it must now be
called explicitly, as Auto::setWeight()
is now hidden from view.
This latter function is hidden,
even though Auto::setWeight()
has only one size_t
argument. To
implement Truck::setWeight()
we could write:
void Truck::setWeight(size_t engine_wt, size_t trailer_wt) { d_trailer_weight = trailer_wt; Auto::setWeight(engine_wt); // note: Auto:: is required }
Auto
-version of setWeight()
is
accessed using the
scope resolution operator. So, if a Truck t
needs
to set its Auto
weight, it must use
t.Auto::setWeight(x);
class Truck
:
// in the interface: void setWeight(size_t engine_wt); // below the interface: inline void Truck::setWeight(size_t engine_wt) { Auto::setWeight(engine_wt); }Now the single argument
setWeight()
member function can be used by
Truck
objects without having to use the scope resolution operator. As the
function is defined inline, no overhead of an additional function call is
involved.
weight()
is also already defined in Auto
, as
it was inherited from Vehicle
. In this case, the class Truck
should
redefine this member function to allow for the extra (trailer) weight in
the Truck
:
size_t Truck::weight() const { return ( // sum of: Auto::weight() + // engine part plus d_trailer_weight // the trailer ); }
Truck
, displaying several weights:
int main() { Land veh(1200, 145); Truck lorry(3000, 120, "Juggernaut", 2500); lorry.Vehicle::setWeight(4000); cout << endl << "Truck weighs " << lorry.Vehicle::weight() << endl << "Truck + trailer weighs " << lorry.weight() << endl << "Speed is " << lorry.speed() << endl << "Name is " << lorry.name() << endl; }Note the explicit call of
Vehicle::setWeight(4000)
: assuming
setWeight(size_t engine_wt)
is not part of the interface of the class
Truck
, it must be called explicitly, using the Vehicle::
scope
resolution, as the single argument function setWeight()
is hidden from
direct view in the class Truck
.
With Vehicle::weight()
and Truck::weight()
the situation is
somewhat different: here the function Truck::weight()
is a
redefinition of Vehicle::
weight()
, so in order to reach
Vehicle::weight()
a scope resolution operation (Vehicle::
) is
required.
How can we construct a `Swiss army knife' in C++? First we need (at
least) two base classes. For example, let's assume we are designing a toolkit
allowing us to construct an instrument panel of an aircraft's cockpit. We
design all kinds of instruments, like an artifical horizon and an
altimeter. One of the components that is often seen in aircraft is a
nav-com set: a combination of a navigational beacon receiver (the `nav'
part) and a radio communication unit (the `com'-part). To define the nav-com
set, we first design the NavSet
class. For the time being, its data
members are omitted:
class NavSet { public: NavSet(Intercom &intercom, VHF_Dial &dial); size_t activeFrequency() const; size_t standByFrequency() const; void setStandByFrequency(size_t freq); size_t toggleActiveStandby(); void setVolume(size_t level); void identEmphasis(bool on_off); };In the class'ss contructor we assume the availability of the classes
Intercom
, which is used by the pilot to listen to the information
transmitted by the navigational beacon, and a class VHF_Dial
which is used
to represent visually what the NavSet
receives.
Next we construct the ComSet
class. Again, omitting the data members:
class ComSet { public: ComSet(Intercom &intercom); size_t frequency() const; size_t passiveFrequency() const; void setPassiveFrequency(size_t freq); size_t toggleFrequencies(); void setAudioLevel(size_t level); void powerOn(bool on_off); void testState(bool on_off); void transmit(Message &message); };Using objects of this class we can receive messages, transmitted though the
Intercom
, but we can also transmit messages, using a
Message
object that's passed to the ComSet
object using its
transmit()
member function.
Now we're ready to construct the NavCom
set:
class NavComSet: public ComSet, public NavSet { public: NavComSet(Intercom &intercom, VHF_Dial &dial); };Done. Now we have defined a
NavComSet
which is both a NavSet
and a ComSet
: the possibilities of either
base class are now
available in the
derived class, using
multiple derivation.
With multiple derivation, please note the following:
public
is present before both base class names
(NavSet
and ComSet
). This is so because the default derivation
in C++ is
private
: the keyword public
must be repeated before each
base class specification. The base classes do not have to have the same kind
of derivation: one base class could have public
derivation, another base
class could use
protected
derivation, yet another base class could use
private
derivation.
NavComSet
introduces no
additional functionality of its own, but merely combines two existing
classes into a new
aggregate class. Thus, C++ offers the possibility to
simply sweep multiple simple classes into one more complex class.
This feature of C++ is often used. Usually it pays to develop `simple' classes each having a simple, well-defined functionality. More complex classes can always be constructed from these simpler building blocks.
NavComSet
constructor:
NavComSet::NavComSet(Intercom &intercom, VHF_Dial &dial) : ComSet(intercom), NavSet(intercom, VHF_Dial) {}The constructor requires no extra code: Its only purpose is to activate the constructors of its base classes. The order in which the base class initializers are called is not dictated by their calling order in the constructor's code, but by the ordering of the base classes in the class interface.
NavComSet
class definition needs no extra data members or
member functions: here (and often) the inherited interfaces provide all the
required functionality and data for the multiply derived class to operate
properly.
setVolume()
in the NavSet
class and a function setAudioLevel()
in
the ComSet
class. A bit
cheating, since we could expect that both units
in fact have a composed object Amplifier
, handling the volume setting. A
revised class might then either use a Amplifier &lifier() const
member
function, and leave it to the application to set up its own interface to the
amplifier, or access functions for, e.g., the volume are made available
through the NavSet
and ComSet
classes as, normally, member functions
having the same names (e.g., setVolume()
). In situations where two base
classes use the same member function
names, special provisions need to
be made to prevent
ambiguity:
NavComSet navcom(intercom, dial); navcom.NavSet::setVolume(5); // sets the NavSet volume level navcom.ComSet::setVolume(5); // sets the ComSet volume level
inline
:
class NavComSet: public ComSet, public NavSet { public: NavComSet(Intercom &intercom, VHF_Dial &dial); void comVolume(size_t volume); void navVolume(size_t volume); }; inline void NavComSet::comVolume(size_t volume) { ComSet::setVolume(volume); } inline void NavComSet::navVolume(size_t volume) { NavSet::setVolume(volume); }
NavComSet
class is obtained from a third party, and should
not be altered, a
wrapper class could be used, which does the previous
explicitation for us in our own programs:
class MyNavComSet: public NavComSet { public: MyNavComSet(Intercom &intercom, VHF_Dial &dial); void comVolume(size_t volume); void navVolume(size_t volume); }; inline MyNavComSet::MyNavComSet(Intercom &intercom, VHF_Dial &dial) : NavComSet(intercom, dial); {} inline void MyNavComSet::comVolume(size_t volume) { ComSet::setVolume(volume); } inline void MyNavComSet::navVolume(size_t volume) { NavSet::setVolume(volume); }
public
, implying that the
access rights of the base class's interface is unaltered in the derived class.
Apart from public derivation, C++ also supports protected derivation and private derivation
To use protected derivation. the keyword protected
is specified in the
inheritance list:
class Derived: protected BaseWith protected derivation all the base class's public and protected members receive protected access rights in the derived class. Members having protected access rights are available to the class itself and to all classes that are (directly or indirectly) derived from it.
To use private derivation. the keyword private
is specified in the
inheritance list:
class Derived: private BaseWith private derivation all the base class's members receive private access rights in the derived class. Members having private access rights are only available to the class itself.
Combinations of inheritance types do occur. For example, when designing a
stream-class it is usually derived from std::istream
or
std::ostream
. However, before a stream can be constructed, a
std::streambuf
must be available. Taking advantage of the fact that the
inheritance order is taken seriously by the compiler, we can use multiple
inheritance (see section 13.5) to derive the class from both
std::streambuf
and (then) from, e.g., std::ostream
. As our class faces
its clients as a std::ostream
and not as a std::streambuf
, we use
private derivation for the latter, and public derivation for the former class:
class Derived: private std::streambuf, public std::ostream
In some situations this scheme is too
restrictive. Consider a class RandStream
derived privately from a
class RandBuf
which is itself derived from std::streambuf
and publicly
from istream
:
class RandBuf: public std::streambuf { // implements a buffer for random numbers }; class RandStream: private std::streambuf, public std::istream { // implements a stream to extract random values from };Such a class could be used to extract, e.g., random numbers using the standard
istream
interface.
Although the RandStream
class is constructed with the
functionality of istream
objects in mind, some of the members of the class
std::streambuf
may be considered useful by themselves. E.g., the function
streambuf::in_avail()
returns a lower bound on the number of characters
that can be read immediately. The standard way to make this function available
is to define a
shadow member calling the base class's member:
class RandStream: private std::streambuf, public std::istream { // implements a stream to extract random values from public: std::streamsize in_avail(); }; inline std::streamsize RandStream::in_avail() { return std::streambuf::in_avail(); }This looks like a lot of work for just making available a member from the protected or private base classes. If the intent is to make available the
in_avail
member
access promotion can be used: by declaring the
protected or private base class in the public interface that specific base
class member becomes available to the class's users. Here is the above
example, now using access promotion:
class RandStream: private std::streambuf, public std::istream { // implements a stream to extract random values from public: using std::streambuf::in_avail; };It should be noted that access promotion makes available all overloaded versions of the declared base class member. So, if
streambuf
would offer
not only in_avail()
but also, e.g., in_avail(size_t *)
both
members would become part of the public interface.
NavCom
class, introduced in section
13.5 We start by defining two objects, a base class and a derived
class object:
ComSet com(intercom); NavComSet navcom(intercom2, dial2);The object
navcom
is constructed using an Intercom
and a Dial
object. However, a NavComSet
is at the same time a ComSet
, allowing
the
assignment from navcom
(a derived class object) to com
(a base class object):
com = navcom;The effect of this assignment should be that the object
com
will now
communicate with intercom2
. As a ComSet
does not have a VHF_Dial
,
the navcom
's dial
is ignored by the assignment: when assigning a
base class object from a derived class object only the base class data members
are assigned, other data members are ignored.
The assignment from a base class object to a derived class object, however, is problematic: In a statement like
navcom = com;it isn't clear how to reassign the
NavComSet
's VHF_Dial
data
member as they are missing in the ComSet
object com
. Such an
assignment
is therefore refused by the
compiler. Although derived class objects are also base class objects, the
reverse does not hold true: a base class object is not also a derived class
object.
The following general rule applies: in assignments in which base class objects and derived class objects are involved, assignments in which data are dropped is legal. However, assignments in which data would remain unspecified is not allowed. Of course, it is possible to redefine an overloaded assignment operator to allow the assignment of a derived class object by a base class object. E.g., to achieve compilability of a statement
navcom = com;the class
NavComSet
must have an overloaded assignment operator
function accepting a ComSet
object for its argument. It would be the
responsibility of the programmere constructing the assignment operator to
decide what to do with the missing data.
Vehicle
classes, and define the following objects and
pointer variable:
Land land(1200, 130); Auto auto(500, 75, "Daf"); Truck truck(2600, 120, "Mercedes", 6000); Vehicle *vp;Now we can assign the addresses of the three objects of the derived classes to the
Vehicle
pointer:
vp = &land; vp = &auto; vp = &truck;Each of these assignments is acceptable. However, an implicit conversion of the derived class to the base class
Vehicle
is used, since vp
is defined as a pointer to a
Vehicle
. Hence, when using vp
only the member functions manipulating
weight
can be called as this is the Vehicle
's only functionality.
As far as the compiler can tell this is the object vp
points to.
The same reasoning holds true for
references to
Vehicles
. If, e.g., a function is defined having a Vehicle
reference
parameter, the function may be passed an object of a class derived from
Vehicle
. Inside the function, the specific Vehicle
members remain
accessible. This analogy between pointers and references holds true in
general. Remember that a reference is nothing but a
pointer in disguise: it
mimics a plain variable, but actually it is a pointer.
This
restricted functionality furthermore has an important consequence
for the class Truck
. After the statement vp = &truck
, vp
points to
a Truck
object. So, vp->weight()
will return 2600 instead of
8600 (the combined weight of the cabin and of the trailer: 2600 + 6000),
which would have been returned by truck.weight()
.
When a function is called using a pointer to an object, then the type of the pointer (and not the type of the object) determines which member functions are available and executed. In other words, C++ implicitly converts the type of an object reached through a pointer to the pointer's type.
If the actual type of the object to which a pointer points is known, an explicit type cast can be used to access the full set of member functions that are available for the object:
Truck truck; Vehicle *vp; vp = &truck; // vp now points to a truck object Truck *trp; trp = reinterpret_cast<Truck *>(vp); cout << "Make: " << trp->name() << endl;Here, the second to last statement specifically casts a
Vehicle *
variable to a Truck *
. As is usually the case with type casts, this code
is not without risk: it will only work if vp
really points to a
Truck
. Otherwise the program may behave unexpectedly.
new[]
calls
the default constructor
of a class to initialize the
allocated objects. For example, to allocate an array of 10 strings we can do
new string[10];but it is not possible to use another constructor. Assuming that we'd want to initialize the strings with the text
hello world
, we can't write
something like:
new string("hello world")[10];
Such an initialization is usually accomplished in a two-step process: first the array is allocated (implicitly calling the default constructor); second the array's elements are initialized, as in the following little example:
string *sp = new string[10]; fill(sp, sp + 10, string("hello world"));These approaches all suffer from `double initializations', comparable to not using member initializers in constructors.
Fortunately inheritance can profitably be used to call non-default
constructors in combination with operator new[]
. The approach capitalizes
on the following:
The above also suggest the prototypical form of the approach:
new[]
's return expression to a pointer to base class objects.
hello world
:
#include <iostream> #include <string> #include <algorithm> #include <iterator> using namespace std; struct Xstr: public string { Xstr() : string("hello world") {} }; int main() { string *sp = new Xstr[10]; copy(sp, sp + 10, ostream_iterator<string>(cout, "\n")); }Of course, the above example is fairly unsophisticated, but it's easy to polish the example: the class
Xstr
can be defined in
an anonymous namespace, accessible only to a function getString()
which
may be given a size_t nObjects
parameter, allowing users to specify the
number of hello world
-initialized strings they would like to allocate.
Instead of hard-coding the base class arguments it's also possible to use
variables or functions providing the appropriate values for the base class
constructor's arguments. In the next example a
local class
Xstr
is defined inside a function
nStrings(size_t nObjects, char const *fname)
, expecting the number of
string
objects to allocate and the name of a file whose subsequent lines
are used to initialize the objects. The local class
is invisible outside of the function nStrings
, so no special namespace
safeguards are required.
As discussed in section 6.5, members of local classes cannot access local variables from their surrounding function. However, they can access global and static data defined by the surrounding function.
Using a local class neatly allows us to hide the implementation details
within the function nStrings
, which simply opens the file, allocates the
objects, and closes the file again. Since the local class is derived from
string
, it can use any string
constructor for its base class
initializer. In this particular case it calls the string(char const *)
constructor, providing it with subsequent lines of the just opened stream via
its static member function nextLine()
. This latter function is, as it is a
static member function, available to Xstr
default constructor's member
initializers even though no Xstr
object is available by that time.
#include <fstream> #include <iostream> #include <string> #include <algorithm> #include <iterator> using namespace std; string *nStrings(size_t size, char const *fname) { static ifstream in; struct Xstr: public string { Xstr() : string(nextLine()) {} static char const *nextLine() { static string line; getline(in, line); return line.c_str(); } }; in.open(fname); string *sp = new Xstr[10]; in.close(); return sp; } int main() { string *sp = nStrings(10, "nstrings.cc"); copy(sp, sp + 10, ostream_iterator<string>(cout, "\n")); }When this program is run, it displays the first 10 lines of the file
nstrings.cc
.
Note that the above implementation can't safely be used in a multithreaded environment. In that case a mutex should be used to protect the three statements just before the function's return statement.