Movatterモバイル変換


[0]ホーム

URL:




Chapter 7: Classes

TheC programming language offers two methods for structuring data ofdifferent types. TheCstruct holds data members of various types, andtheCunion also defines data members of various types. However, aunion's data members all occupy the same location in memory and the programmermay decide on which one to use.

In this chapter classes are introduced. Aclass is a kind ofstruct,but its content is by default inaccessible to the outside world, whereas thecontent of aC++struct is by default accessible to the outsideworld. InC++structs find little use: they are mainly used toaggregate data within the context of classes or to define elaborate returnvalues. Often aC++struct merely containsplain old data (POD,cf. section9.10). InC++ theclass is the main data structuringdevice, by default enforcing two core concepts of current-day softwareengineering:data hiding andencapsulation (cf. sections3.2.1and7.1.1).

Theunion is another data structuring device the language offers. ThetraditionalC union is still available, butC++ also offersunrestricted unions. Unrestricted unions are unions whose data fields maybe of class types. TheC++ Annotations covers these unrestricted unions insection9.9, after having introduced several other new concepts ofC++,

C++ extends theCstruct andunion concepts by allowing thedefinition ofmember functions (introduced in this chapter) within these data types. Memberfunctions are functions that can only be used with objects of these data typesor within the scope of these data types. Some of these member functions arespecial in that they are always, usually automatically, called when an objectstarts its life (the so-calledconstructor) or ends its life (theso-calleddestructor). These and other types of member functions, as wellas the design and construction of, and philosophy behind, classes areintroduced in this chapter.

We step-by-step construct a classPerson, which could be used in adatabase application to store a person's name, address and phone number.

Let's start by creating aclass Person right away. From theonset, it is important to make the distinction between the classinterface and itsimplementation. A class may loosely be defined as`a set of data and all the functions operating on those data'. This definitionis later refined but for now it is sufficient to get us started.

A class interface is a definition, defining the organization of objects ofthat class. Normally a definition results in memory reservation. E.g., whendefiningint variable the compiler ensures that some memory is reserved inthe final program storingvariable's values. Although it is a definitionno memory is set aside by the compiler once it has processed the classdefinition. But a class definition follows theone definition rule: inC++ entities may be defined only once. As aclass definition does notimply that memory is being reserved the termclass interface is preferredinstead.

Class interfaces are normally contained in a classheader file, e.g.,person.h. We'll start ourclass Person interface here (cf section7.7 for an explanation of theconst keywords behind someof the class's member functions):

    #include <string>    class Person    {        std::string d_name;         // name of person        std::string d_address;      // address field        std::string d_phone;        // telephone number        size_t      d_mass;         // the mass in kg.        public:                     // member functions            void setName(std::string const &name);            void setAddress(std::string const &address);            void setPhone(std::string const &phone);            void setMass(size_t mass);            std::string const &name()    const;            std::string const &address() const;            std::string const &phone()   const;            size_t mass()                const;    };
The member functions that aredeclared in the interface must still beimplemented. The implementation of these members is properly called theirdefinition.

In addition to memberfunctions classes also commonly define thedatathat are manipulated by those member functions. These data are called thedata members. InPerson they ared_name, d_address,d_phone andd_mass. Data members should be given privateaccess rights. Since the class uses private access rights by defaultthey are usually simply listed at the top of the class interface.

All communication between the outer world and the class data is routed throughthe class's member functions. Data members may receive new values (e.g., usingsetName) or they may be retrieved for inspection (e.g., usingname). Functions merely returning values stored inside the object, notallowing the caller to modify these internally stored values, are calledaccessors.

Syntactically there is only a marginal difference between a class and astruct. Classes by default defineprivate members, structs definepublic members. Conceptually, though, there are differences. InC++structs are used in the way they are used inC: to aggregate data, whichare all freely accessible. Classes, on the other hand, hide their data fromaccess by the outside world (which is aptly calleddata hiding)and offer member functions to define the communication between the outer worldand the class's data members.

FollowingLakos (Lakos, J., 2001)Large-Scale C++ Software Design (Addison-Wesley) Isuggest the following setup of class interfaces:

Style conventions usually take a long time to develop. There is nothingobligatory about them, though. I suggest that readers who have compellingreasonsnot to follow the above style conventions use their own. Allothers are strongly advised to adopt the above style conventions.

Finally, referring back to section3.1.2 that

    using namespace std;

must be used in most (if not all) examples of source code. Asexplained in sections7.11 and7.11.1 theusingdirective should follow the preprocessor directive(s) including the headerfiles, using a setup like the following:

    #include <iostream>    #include "person.h"    using namespace std;    int main()    {        ...    }

7.1: The constructor

C++ classes usually contain two special categories of member functionswhich are essential to the proper working of classes. These categories are theconstructors and thedestructor. Thedestructor's primary task is toreturn memory allocated by an object to the common pool when an object goes`out of scope'. Allocation of memory is discussed in chapter9, andan in-depth coverage of destructors is therefore postponed until we reach thatchapter. In the current chapter the emphasis is on the class's internalorganization and on its constructors.

Constructors are recognized by their names which are equal to their classnames. Constructors do not specify return values, not evenvoid. E.g.,the classPerson may define a constructorPerson::Person(). TheC++ run-time system ensures that the constructor of a class is called whena variable of the class is defined. It is possible to define a class lackingany constructor. In that case the compiler defines a default constructor that is called when an object ofthat class is defined. What actually happens in that case depends on the datamembers that are defined by that class (cf. section7.3.1).

Objects may be defined locally or globally. However, inC++ most objectsare defined locally. Globally defined objects are hardly ever required and aresomewhat deprecated.

When a function defines a local object, that object's constructor is calledevery time the function is called. The object's constructor is activated atthe point where the object is defined (a subtlety is that an object may bedefined implicitly as, e.g., a temporary variable in an expression).

When an object is defined as a static object it is constructed whenthe program starts. In this case its constructor is called evenbefore the functionmain starts. Example:

    #include <iostream>    using namespace std;    class Demo    {        public:            Demo();    };    Demo::Demo()    {        cout << "Demo constructor called\n";    }    Demo d;    int main()    {}    /*        Generated output:    Demo constructor called    */
The program contains one global object of the classDemo withmainhaving an empty body. Nonetheless, the program produces some output generatedby the constructor of the globally definedDemo object.

Constructors have a very important and well-defined role. They must ensurethat all the class's data members have sensible or at least well-definedvalues once the object has been constructed. We'll get back to this importanttask shortly. Thedefault constructor has noargument. It is defined by the compiler unless another constructor is definedand unless its definition is suppressed (cf. section7.6). If adefault constructor is required in addition to another constructor then thedefault constructor must explicitly be defined as well.C++ providesspecial syntax to realize that without much effort, which is also covered bysection7.6.

7.1.1: A first application

Our example classPerson has three string data members and asize_td_mass data member. Access to these data members is controlled byinterface functions.

Whenever an object is defined the class's constructor(s) ensure that its datamembers are given `sensible' values. Thus, objects never suffer fromuninitialized values. Data members may be given new values, but that shouldnever be directly allowed. It is a core principle (calleddata hiding) of good class design that its data members are private. Themodification of data members is therefore fully controlled by member functionsand thus, indirectly, by the class-designer. The classencapsulates allactions performed on its data members and due to thisencapsulation the class object may assume the `responsibility' for itsown data-integrity. Here is a minimal definition ofPerson's manipulatingmembers:

    #include "person.h"                 // given earlier    using namespace std;    void Person::setName(string const &name)    {        d_name = name;    }    void Person::setAddress(string const &address)    {        d_address = address;    }    void Person::setPhone(string const &phone)    {        d_phone = phone;    }    void Person::setMass(size_t mass)    {        d_mass = mass;    }
It's a minimal definition in that no checks are performed. But it should beclear that checks are easy to implement. E.g., to ensure that a phone numberonly contains digits one could define:
    void Person::setPhone(string const &phone)    {        if (phone.empty())            d_phone = " - not available -";        else if (phone.find_first_not_of("0123456789") == string::npos)            d_phone = phone;        else            cout << "A phone number may only contain digits\n";    }

Note the double negation in this implementation. Double negations are veryhard to read, and an encapsulating memberbool hasOnly handles the test,and improvessetPhone's readability:

    bool Person::hasOnly(char const *characters, string const &object)    {                        // object only contains 'characters'        return object.find_first_not_of(characters) == string::npos;    }

andsetPhone becomes:

    void Person::setPhone(string const &phone)    {        if (phone.empty())            d_phone = " - not available -";        else if (hasOnly("0123456789", phone))            d_phone = phone;        else            cout << "A phone number may only contain digits\n";    }

SincehasOnly is an encapsulated member function we can ensure thatit's only used with non-empty string objects, sohasOnly itself doesn'thave to check for that.

Access to the data members is controlled byaccessormembers. Accessors ensure that data members cannot suffer from uncontrolledmodifications. Since accessors conceptually do not modify the object's data(but only retrieve the data) these member functions are given the predicateconst. They are calledconst memberfunctions,which, as they are guaranteed not to modify their object's data, are availableto both modifiable and constant objects (cf. section7.7).

To preventbackdoors we must also make sure that the data member isnot modifiable through an accessor's return value. For values of built-inprimitive types that's easy, as they are usually returned by value, which arecopies of the values found in variables. But since objects may be fairlylarge making copies is usually prevented by returning objects by reference. Abackdoor is created by returning a data member by reference, as in thefollowing example, showing the allowed abuse below the function definition:

    string &Person::name() const    {        return d_name;    }    Person somebody;    somebody.setName("Nemo");    somebody.name() = "Eve";    // Oops, backdoor changing the name

To prevent the backdoor objects are returned asconst references fromaccessors. Here are the implementations ofPerson's accessors:

    #include "person.h"                 // given earlier    using namespace std;    string const &Person::name() const    {        return d_name;    }    string const &Person::address() const    {       return d_address;    }    string const &Person::phone() const    {       return d_phone;    }    size_t Person::mass() const    {       return d_mass;    }

ThePerson class interface remains the starting point for the classdesign: its member functions define what can be asked of aPersonobject. In the end the implementation of its members merely is a technicalityallowingPerson objects to do their jobs.

The next example shows how the classPerson may be used. An object isinitialized and passed to a functionprintperson(), printing the person'sdata. Note the reference operator in the parameter list of the functionprintperson. Only a reference to an existingPerson object is passedto the function, rather than a complete object. The fact thatprintperson does not modify its argument is evident from the fact thatthe parameter is declaredconst.

    #include <iostream>    #include "person.h"                 // given earlier    using namespace std;    void printperson(Person const &p)    {        cout << "Name    : " << p.name()     << "\n"                "Address : " << p.address()  << "\n"                "Phone   : " << p.phone()    << "\n"                "Mass  : " << p.mass()   << '\n';    }    int main()    {        Person p;        p.setName("Linus Torvalds");        p.setAddress("E-mail: Torvalds@cs.helsinki.fi");        p.setPhone("");        p.setMass(75);           // kg.        printperson(p);    }/*    Produced output:Name    : Linus TorvaldsAddress : E-mail: Torvalds@cs.helsinki.fiPhone   :  - not available -Mass  : 75*/

7.1.2: Constructors: with and without arguments

The classPerson's constructor so far has not received anyparameters.C++ allows constructors to be defined with or withoutparameter lists. The arguments are supplied when an object is defined.

For the classPerson a constructor expecting three strings and asize_t might be useful. Representing, respectively, the person's name,address, phone number and mass. This constructor can be implemented like this(but see also section7.3.1):

    Person::Person(string const &name, string const &address,                   string const &phone, size_t mass)    {        d_name = name;        d_address = address;        setPhone(phone);        d_mass = mass;    }

It must of course also be declared in the class interface:

    class Person    {        // data members (not altered)        public:            Person(std::string const &name, std::string const &address,                   std::string const &phone, size_t mass);            // rest of the class interface (not altered)    };

Now that this constructor has been declared, the default constructor mustexplicitly be declared as well if we still want to be able to construct aplainPerson object without any specific initial values for its datamembers. The classPerson would thus support two constructors, and thepart declaring the constructors now becomes:

    class Person    {        // data members        public:            Person();            Person(std::string const &name, std::string const &address,                   std::string const &phone, size_t mass);            // additional members    };

In this case, the default constructor doesn't have to do very much, as itdoesn't have to initialize thestring data members of thePersonobject. As these data members are objects themselves, they are initialized toempty strings by their own default constructor. However, there is also asize_t data member. That member is a variable of a built-in type and suchvariables do not have constructors and so are not initialized automatically.Therefore, unless the value of thed_mass data member is explicitlyinitialized its value is:

The 0-value might not be too bad, but normally we don't want arandomvalue for our data members. So, even the default constructor has a job to do:initializing the data members which are not initialized to sensible valuesautomatically. Its implementation can be:
    Person::Person()    {        d_mass = 0;    }

Using constructors with and without arguments is illustrated next. Theobjectkarel is initialized by the constructor defining a non-emptyparameter list while the default constructor is used for theanonobject. When constructing objects using constructors requiring arguments youare advised to surround the arguments by curly braces. Parentheses can oftenalso be used, and sometimes evenhave to be used (cf. section12.3.2), but mindlessly using parentheses instead of curly braces mayeasily result in unexpected problems (cf. section7.2). Hence theadvice to prefer curly braces rather than parentheses. Here's theexample showing two constructor-calls:

    int main()    {        Person karel{ "Karel", "Rietveldlaan 37", "542 6044", 70 };        Person anon;    }

The twoPerson objects are defined whenmain starts as they arelocal objects, living only for as long asmain is active.

IfPerson objects must be definable using other arguments,corresponding constructors must be added toPerson's interface. Apart fromoverloading class constructors it is also possible to provide constructorswith default argument values. These default arguments must be specified withthe constructor declarations in the class interface, like so:

    class Person    {        public:            Person(std::string const &name,                   std::string const &address = "--unknown--",                   std::string const &phone   = "--unknown--",                   size_t mass = 0);    };

Often, constructors use highly similar implementions. This results fromthe fact that the constructor's parameters are often defined for convenience:a constructor not requiring aphone number but requiring amass cannotbe defined using default arguments, sincephone is not the constructor'slast parameter. Consequently a special constructor is required not havingphone in its parameter list. However, this doesn't necessarily mean thatconstructors must duplicate their code, as constructors may call each other(calledconstructor delegation). Constructor delegation is illustrated insection7.4.1 below.

7.1.2.1: The order of construction

The possibility to pass arguments to constructors allows us to monitor theconstruction order of objects during program execution. This is illustratedby the next program using a classTest. The program defines a globalTest object and two localTest objects. The order of construction isas expected: first global, then main's first local object, thenfunc'slocal object, and then, finally,main's second local object:
    #include <iostream>    #include <string>    using namespace std;    class Test    {        public:            Test(string const &name);   // constructor with an argument    };    Test::Test(string const &name)    {        cout << "Test object " << name << " created" << '\n';    }    Test globaltest("global");    void func()    {        Test functest("func");    }    int main()    {        Test first{ "main first" };        func();        Test second{ "main second" };    }/*    Generated output:Test object global createdTest object main first createdTest object func createdTest object main second created*/

7.2: Ambiguity resolution

Calling constructors using parentheses may result in unwelcomesurprises. Assume the following class interface is available:
    class Data    {        public:            Data();            Data(int one);            Data(int one, int two);            void display();    };

The intention is to define two objects of the class Data, using, respectively,the first and second constructors, while using parentheses in the objectdefinitions. Your code looks like this (and, ignoring the 'not used' warnings,compiles correctly):

    #include "data.h"    int main()    {        Data d1();        Data d2(argc);    }

Let's get rid of the 'not used' warnings by adding two statements tomain:

        d1.display();        d2.display();

But, surprise, the compiler complains about the first of these two:

error: request for member 'display' in 'd1', which is of non-class type'Data()'

What's going on here? First of all, notice the data type the compiler refersto:Data(), rather thanData. What are those() doing there?

Before answering that question, let's broaden our story somewhat. We know thatsomewhere in a library afactory functiondataFactory exists. Afactory function creates and returns an object of a certain type. ThisdataFactory function returns aData object, constructed usingData's default constructor. Hence,dataFactory needs no arguments. Wewant to usedataFactory in our program, but must declare the function. Sowe add the declaration tomain, as that's the only location wheredataFactory will be used. It's a function, not requiring arguments,returning aData object:

        Data dataFactory();

This, however, looks remarkably similar to ourd1 object definition:

        Data d1();

Here we found the solution of our problem:Data d1() apparently isnotthe definition of ad1 object, but thedeclaration of a function,returning aData object. So, what's happening here and how should wedefine aData object usingData's default constructor?

First: what's happening here is that the compiler, when confronted withData d1(), actually had a choice. It could either define aDataobject, or declare a function. It declares a function.

Here we're encountering anambiguity inC++'s grammar which is solved,according to the language's standard, by always letting a declaration prevailover a definition. We'll encounter more situations where this ambiguity occurslater on in this section.

Second: there are several ways we can solve this ambiguity in the way wewant it to be solved. To define an object using its default constructor:

7.2.1: Types `Data' vs. `Data()'

Data() in the last example of the previous section defines a defaultconstructed anonymousData object. This takes us back to the compilererror. According to the compiler, our originald1 apparently was not oftypeData, but of typeData(). So what's that?

Let's first have a look at our second constructor. It expects anint. We would like to define anotherData object, using the secondconstructor and want to pass the defaultint value to the constructor,usingint(). We know this defines a defaultint value, ascout <<int() << '\n' nicely displays 0, andint x = int() also initialized x to 0. So we try`Data di(int())' inmain.

Not good: again the compiler complains when we try to usedi. After`di.display()' the compiler tells us:

error: request for member 'display' in 'di', which is ofnon-class type 'Data(int (*)())'

Oops, again not as expected.... Didn't we pass 0? Why the sudden pointer?Well, we're once again encountering the compiler's same `use a declarationwhen possible' strategy. The notationType() not only represents thedefault value of typeType, but it's also a shorthand notation for ananonymous pointer to a function, not expecting arguments, and returning aType value, which you can verify by defining`int (*ip)() = nullptr',and passingip as argument todi:di(ip) compiles fine.

So why doesn't the error occur when insertingint() or assigningint()toint x? In these latter cases nothing is declared. Rather, `cout'and `int x =' require expressions resulting in values, which is providedbyint()'s `natural' interpretation. But with`Data di(int())' thecompiler again has a choice, and (by design) it chooses a declaration becausethe declaration takes priority. Nowint()'s interpretation as an anonymouspointer is available and therefore used.

Likewise, ifint x has been defined,`Data b1(int(x))' declaresb1as a function, expecting anint (asint(x) represents a type), while`Data b2((int)x)' definesb2 as aData object, using theconstructor expecting a singleint value.

Again, to use default entities, values or objects, prefer{} over():Data di{ int{} } definesdi of typeData, calling theData(intx) constructor and usesint's default value 0.

But what about the compiler's original complaint? The compiler told us thatour originald1 was not of typeData, but of typeData(). Butwhat's that? The parentheses in the error message are important: it indicatesa function:d1, because of the subsequent parentheses, declares afunction, not expecting arguments, and returning aData object. Shorthand:Data(), more verbosely:Data (*)(). As a final illustration: thefollowing program writeshello world twice, andData d1() inmainsimply is an explicit local declaration of the globally defined functiond1, further illustrated by the assignment ofd1 to the explicitlydeclared pointer to a functionpf, returning aData object:

    #include <iostream>    #include "data.h"        Data::Data()    {}    Data d1()    {        std::cout << "hello world\n";        return Data{};    }        int main()    {        Data d1();            d1();        Data (*pf)() = d1;            pf();   // or (same): (*pf)()    }
Also, asd1() returns aData object, a statement liked1().display() correctly compiles.

7.2.2: Superfluous parentheses

Let's play some more. At some point in our program we definedint b. Then,in a compound statement we need to construct ananonymousData object,initialized byb, and then we're supposed to displayb itself:
    int b = 18;    {        Data(b);        cout << b;    }

About thatcout statement the compiler tells us (I modified theerror message to reveal its meaning):

error: cannot bind `std::ostream & << Data const &'

Here we didn't insertint b butData b. Had we omitted the compoundstatement, the compiler would have complained about a doubly definedbentity, asData(b) simply meansData b, aData object constructedby default. Here, the parentheses aroundb are superfluous and may beomitted by the compiler when parsing a definition or declaration.

Of course, the question now becomes how a temporary objectData,initialized withint bcan be defined. Remember that the compiler mayremove superfluous parentheses. So, what we need to do is to pass anintto the anonymousData object, without using theint's name.

Values and types make big differences. Consider the following definitions:

    Data (*d4)(int);    // 1    Data (*d5)(3);      // 2

Definition 1 should cause no problems: it's a pointer to a function,expecting anint, returning aData object. Hence,d4 is a pointervariable.

Definition 2 is slightly more complex. Yes, it's a pointer. But it hasnothing to do with a function. So what's that argument list containing 3 doingthere? Well, it's not an argument list. It's an initialization that looks likean argument list. Remember: variables can be initialized using assignmentstatements, using parentheses, or using curly braes. In general, they'reinterchangeable. So instead of`(3)' we could have written`= 3' or`{3}'. Let's pick the first alternative, resulting in:

    Data (*d5) = 3;

Now we get to `play compiler' again. Removing some more superfluousparentheses we get:

    Data *d5 = 3;

It's a pointer to aData object, initialized to 3. This issyntactically correct, butsemantically incorrect: at address 3there's noData object. If we had initially written

     Data (*d5)(&d1);      // 2

the fun resulting from contrastingint and3 would most likelyhave been masked.

7.2.3: Existing types

Once a type name has been defined it prevails over identifiers representingvariables if the compiler is given a choice. This, too, may result ininteresting constructions.

Assume a functionprocess expecting anint exists in a library. Wewant to use this function to process someint data values. So inmainprocess is declared and called:

    int process(int Data);    process(argc);

No problems here. But unfortunately we once decided to `beautify' ourcode, by throwing in some superfluous parentheses, like so:

    int process(int (Data));    process(argc);

we're in trouble. Now the compiler generates an error, caused by the rule tolet declarations prevail over definitions.Data now becomes the name oftheclass Data, and analogous toint (x) the parameterint (Data)is parsed asint (*)(Data): a pointer to a function, expecting aDataobject, returning anint.

Here is another example. When, instead of declaring

    int process(int Data[10]);

we declare, e.g., to emphasize the fact that an array is passed toprocess:

    int process(int (Data[10]));

theprocess function does not expect a pointer toint values, buta pointer to a function expecting a pointer toData elements, returning anint. The size 10 is considered just a number, and is not taken literally,as it's part of a function parameter specification.

To summarize the findings in the `Ambiguity Resolution' section:

7.3: Objects inside objects: composition

In the classPerson objects are used as data members. This constructiontechnique is calledcomposition.

Composition is neither extraordinary norC++ specific: inCastruct orunion field is commonly used in other compound types. InC++ it requires some special thought as their initialization sometimes issubject to restrictions, as discussed in the next few sections.

7.3.1: Composition and (const) objects: (const) member initializers

Unless specified otherwise object data members of classes are initialized bytheir default constructors. Using the default constructor might not always bethe optimal way to intialize an object and it might not even be possible: aclass might simply not define a default constructor.

Earlier we've encountered the following constructor of thePerson:

    Person::Person(string const &name, string const &address,                   string const &phone, size_t mass)    {        d_name = name;        d_address = address;        d_phone = phone;        d_mass = mass;    }

Think briefly about what is going on in this constructor. In theconstructor's body we encounter assignments to string objects. Sinceassignments are used in the constructor's body their left-hand side objectsmust exist. But when objects are coming into existence constructorsmusthave been called. The initialization of those objects is thereupon immediatelyundone by the body ofPerson's constructor. That isnot only inefficient but sometimes downright impossible. Assume that the classinterface mentions astring const data member: a data member whose valueis not supposed to change at all (like a birthday, which usually doesn'tchange very much and is therefore a good candidate for astring const datamember). Constructing a birthday object and providing it with an initial valueis OK, but changing the initial value isn't.

The body of a constructor allows assignments to data members. Theinitialization of data members happens before that.C++ defines themember initializer syntax allowing us to specify the way data membersare initialized at construction time. Member initializers are specified as alist of constructor specifications between a colon following a constructor'sparameter list and the opening curly brace of a constructor's body, asfollows:

    Person::Person(string const &name, string const &address,                   string const &phone, size_t mass)    :        d_name(name),        d_address(address),        d_phone(phone),        d_mass(mass)    {}

In this example the member initialization used parentheses surrounding theintialization expression.Instead ofparenthesescurly braces may also be used. E.g.,d_name could also beinitialized this way:

        d_name{ name },

Member initializationalways occurs when objects are composed inclasses: ifno constructors are mentioned in the member initializer listthe default constructors of the objects are called. Note that this only holdstrue forobjects. Data members of primitive data types arenotinitialized automatically.

Member initialization can, however, also be used for primitive data members,likeint anddouble. The above example shows the initialization of thedata memberd_mass from the parametermass. When memberinitializers are used the data member could even have the same name as theconstructor's parameter (although this is deprecated) as there is no ambiguityand the first (left) identifier used in a member initializer is always a datamember that is initialized whereas the identifier between parentheses isinterpreted as the parameter.

Theorder in which class type data members are initialized is defined bythe order in which those members are defined in the composing classinterface. If the order of the initialization in the constructor differs fromthe order in the class interface, the compiler complains, and reorders theinitialization so as to match the order of the class interface.

Member initializers should be used as often as possible. As shown it may berequired to use them (e.g., to initialize const data members, or to initializeobjects of classes lacking default constructors) butnot using memberinitializers also results in inefficient code as the default constructor of adata member is always automatically called unless an explicit memberinitializer is specified. Reassignment in the constructor's body followingdefault construction is then clearly inefficient. Of course, sometimes it isfine to use the default constructor, but in those cases the explicit memberinitializer can be omitted.

As arule of thumb: if a value is assigned to a data member in theconstructor's body then try to avoid that assignment in favor of using amember initializer.

7.3.2: Composition and reference objects: reference member initializers

Apart from using member initializers to initialize composed objects (be theyconst objects or not), there is another situation where memberinitializers must be used. Consider the following situation.

A program uses an object of the classConfigfile, defined inmainto access the information in a configuration file. The configuration filecontains parameters of the program which may be set by changing the values inthe configuration file, rather than by supplying command line arguments.

Assume another object used inmain is an object of the classProcess,doing `all the work'. What possibilities do we have to tell the object of theclassProcess that an object of the classConfigfile exists?

But a reference variable cannot be initialized using an assignment, and sothe following is incorrect:
    Process::Process(Configfile &conf)    {        d_conf = conf;        // wrong: no assignment    }

The statementd_conf = conf fails, because it is not aninitialization, but an assignment of oneConfigfile object (i.e.,conf), to another (d_conf). An assignment to a reference variable isactually an assignment to the variable the reference variable refers to. Butwhich variable doesd_conf refer to? To no variable at all, since wehaven't initializedd_conf. After all, the whole purpose of the statementd_conf = conf was to initialized_conf....

How to initialized_conf? We once again use the member initializersyntax. Here is the correct way to initialized_conf:

    Process::Process(Configfile &conf)    :        d_conf(conf)      // initializing reference member    {}

The above syntax must be used in all cases where reference data membersare used. E.g., ifd_ir would have been anint reference data member,a construction like

    Process::Process(int &ir)    :        d_ir(ir)    {}

would have been required.

7.4: Data member initializers

Non-static data members of classes are usually initialized by the class'sconstructors. Frequently (but not always) the same initializations are used bydifferent constructors, resulting in multiple points where the initializationsare performed, which in turn complicates class maintenance.

Consider a class defining several data members: a pointer to data, a datamember storing the number of data elements the pointer points at, a datamember storing the sequence number of the object. The class alsooffer a basic set of constructors, as shown in the following class interface:

    class Container    {        Data *d_data;        size_t d_size;        size_t d_nr;        static size_t s_nObjects;        public:            Container();            Container(Container const &other);            Container(Data *data, size_t size);            Container(Container &&tmp);    };

The initial values of the data members are easy to describe, but somewhathard to implement. Consider the initial situation and assume the defaultconstructor is used: all data members should be set to 0, except ford_nrwhich must be given the value++s_nObjects. Since these arenon-default actions, we can't declare the default constructor using=default, but we must provide an actual implementation:

    Container()    :        d_data(0),        d_size(0),        d_nr(++s_nObjects)    {}

In fact,all constructors require us to state thed_nr(++s_nObjects) initialization. So ifd_data's type would have beena (move aware) class type, we would still have to provide implementations forall of the above constructors.

C++, however, also supportsdata member initializers,simplifying the initialization of non-static data members. Data memberinitializers allow us to assign initial values to data members. The compilermust be able to compute these initial values from initialization expressions,but the initial values do not have to be constant expressions. So++s_nObjects can be an initial value.

Using data member initializers for the classContainer we get:

    class Container    {        Data *d_data = 0;        size_t d_size = 0;        size_t d_nr = ++s_nObjects;        static size_t s_nObjects;        public:            Container() = default;            Container(Container const &other);            Container(Data *data, size_t size);            Container(Container &&tmp);    };

Note that the data member initializations are recognized by the compiler,and are applied to its implementation of the default constructor. In fact, allconstructors will apply the data member initializations, unless explicitlyinitialized otherwise. E.g., the move-constructor may now be implemented likethis:

    Container(Container &&tmp)    :        d_data(tmp.d_data),        d_size(tmp.d_size)    {        tmp.d_data = 0;    }

Althoughd_nr's intialization is left out of the implementation itis initialized due to the data member initialization provided in theclass's interface.

Anaggregate is an array or aclass (usually astruct with nouser-defined constructors, no private or protected non-static data members,no base classes (cf. chapter13), and no virtual functions(cf. chapter14)). E.g.,

    struct POD      // defining aggregate POD    {        int first = 5;         double second = 1.28;         std::string hello{ "hello" };    };

To initialize such aggregatesbraced initializer lists can be used. Infact, their use is preferred over using the older form (using parentheses), asusing braces avoids confusion with function declarations. E.g.,

    POD pod{ 4, 13.5, "hi there" };

When using braced-initializer lists not all data members need to beinitialized. Specification may stop at any data member, in which case thedefault (or explicitly defined initialization values) of the remaining datamembers are used. E.g.,

    POD pod{ 4 };   // uses second: 1.28, hello: "hello"

7.4.1: Delegating constructors

Often constructors are specializations of each other, allowing objects to beconstructed specifying only subsets of arguments for all of its data members,using default argument values for the remaining data members.

Before the C++11 standard common practice was to define a member likeinitperforming all initializations common to constructors. Such aninitfunction, however, cannot be used to initializeconst or reference datamembers, nor can it be used to perform so-calledbase classinitializations (cf. chapter13).

Here is an example where such aninit function might have been used. AclassStat is designed as a wrapper class aroundC'sstat(2)function. The class might define three constructors: one expecting noarguments and initializing all data members to appropriate values; a secondone doing the same, but it callsstat for the filename provided to theconstructor; and a third one expecting a filename and a search path for theprovided file name. Instead of repeating the initialization code in eachconstructor, the common code can be factorized into a memberinit whichis called by the constructors.

C++ offers an alternative by allowing constructors to call eachother. This is calleddelegating constructors which is illustrated by the next example:

    class Stat    {        public:            Stat()            :                Stat("", "")        // no filename/searchpath            {}            Stat(std::string const &fileName)            :                Stat(fileName, "")  // only a filename            {}            Stat(std::string const &fileName, std::string const &searchPath)            :                d_filename(fileName),                d_searchPath(searchPath)            {                // remaining actions to be performed by the constructor            }    };

C++ allows static const integral data members to be initialized withinthe class interfaces (cf. chapter8). TheC++11 standard adds to this the facility to definedefault initializations for plain data members in class interfaces (these datamembers may or may not beconst or of integral types, but (of course) theycannot be reference data members).

These default initializations may be overruled by constructors. E.g., ifthe classStat uses a data memberbool d_hasPath which isfalse bydefault but the third constructor (see above) should initialize it totruethen the following approach is possible:

    class Stat    {        bool d_hasPath = false;        public:            Stat(std::string const &fileName, std::string const &searchPath)            :                d_hasPath(true)     // overrule the interface-specified            {}                      // value    };

Hered_hasPath receives its value only once: it's always initializedtofalse except when the shown constructor is used in which case it isinitialized totrue.

7.5: Uniform initialization

When defining variables and objects they may immediately be giveninitial values. Class type objects are always initialized by one of theiravailable constructors.C already supports the array and structinitializer list consisting of a list of constantexpressions surrounded by a pair of curly braces.

C++ supports a comparable initialization, calleduniform initialization. It uses the following syntax:

    Type object{ value list };

When defining objects using a list of objects each individual objectmay use its own uniform initialization.

The advantage of uniform initialization over using constructors is that usingconstructor arguments sometimes results in an ambiguity as constructing anobject may sometimes be confused with using the object's overloaded functioncall operator (cf. section11.11). As initializer lists can only be usedwithplain old data (POD) types (cf. section9.10) and with classesthat are `initializer list aware' (likestd::vector) the ambiguity doesnot arise when initializer lists are used.

Uniform initialization can be used to initialize an object or variable, butalso to initialize data members in a constructor or implicitly in the returnstatement of functions. Examples (in-class implementations for brevity):

    class Person    {        // data members        public:            Person(std::string const &name, size_t mass)            :                d_name {name},                d_mass {mass}            {}            Person copy() const            {                return {d_name, d_mass};            }    };

Object definitions may be encountered in unexpected places, easily resulting in(human) confusion. Consider a function `func' and a very simpleclassFun (struct is used, as data hiding is not an issue here):

    void func();    struct Fun    {        Fun(void (*f)())        {            std::cout << "Constructor\n";        };        void process()        {            std::cout << "process\n";        }    };

Assume that inmain aFun object is defined as follows:

    Fun fun(func);

Running this program displaysConstructor, confirming that the objectfun is constructed. Next we change this line of code, callingprocessfrom an anonymousFun object:

    Fun(func).process();

As expected,Constructor appears, followed by the textprocess.

What about merely defining an anonymousFun object? We do:

    Fun(func);

Now we're in for a surprise. The compiler complains thatFun's defaultconstructor is missing. Why's that? Insert a blank immediately afterFun and you getFun (func). Parentheses around an identifier are OK,and are removed while the parenthesized expression is parsed. Inthis case:(func) equalsfunc, and so we haveFun func: thedefinition of aFun func object, usingFun's default constructor (whichisn't declared inFun's class interface).

So why doesFun(func).process() compile? In this case we have a memberselector operator, whose left-hand operand must be an class-type object. Theobject must exist, andFun(func) represents that object. It's not the nameof an existing object, but a constructor expecting a function (likefunc)as its argument. So the compiler creates ananonymousFun object,which receivesfunc as its argument.

Clearly, withFun(func) parentheses cannot be used to create an anonymousFun object. However, a uniform initializationcan be used. To definethe anonymousFun object we use this syntax:

    Fun{ func };

(which can also be used to immediately call one of its members. E.g.,Fun{ func }.process()).

Although the uniform intialization syntax is slightly different from thesyntax of an initializer list (the latter using the assignment operator) thecompiler nevertheless uses the initializer list if a constructorsupporting an initializer list is available. As an example consider:

    class Vector    {        public:            Vector(size_t size);            Vector(std::initializer_list<int> const &values);    };    Vector vi = {4};

When definingvi the constructor expecting the initializer list iscalled rather than the constructor expecting asize_t argument. If thelatter constructor is required the standard constructorsyntax must be used. I.e.,Vector vi(4).

Initializer lists are themselves objects that may be constructed usinganother initializer list. However, values stored in an initializer list areimmutable. Once the initializer list has been defined their values remainas-is.

Initializer lists support a basic set of member functions and constructors:

7.6: Defaulted and deleted class members

In everyday class design two situations are frequently encountered: Once a class defines at least one constructor its default constructor isnot automatically defined by the compiler.C++ relaxes thatrestriction somewhat by offering the`= default'syntax. A class specifying `= default' with its default constructordeclaration indicates that thetrivial default constructor should beprovided by the compiler. A trivial default constructor performs the followingactions: Trivial implementations can also be provided for thecopy constructor,theoverloaded assignment operator, and thedestructor. Those membersare introduced in chapter9.

Conversely, situations exist where some (otherwise automatically provided)members shouldnot be made available. This is realized by specifying`= delete'. Using= default and= delete is illustratedby the following example. The default constructor receives its trivialimplementation, copy-construction is prevented:

    class Strings    {        public:            Strings() = default;            Strings(std::string const *sp, size_t size);            Strings(Strings const &other) = delete;    };

7.7: Const member functions and const objects

The keywordconst is often used behind the parameter list of memberfunctions. This keyword indicates that a member function does not alter thedata members of its object. Such member functions are calledconst member functions. In the classPerson, we see that theaccessor functions were declaredconst:
    class Person    {        public:            std::string const &name()    const;            std::string const &address() const;            std::string const &phone()   const;            size_t mass()              const;    };

The rule of thumb given in section3.1.1 applies here too:whichever appears to theleft of the keywordconst, is notaltered. With member functions this should be interpreted as `doesn't alterits own data'.

When implementing a const member function theconst attribute must berepeated:

    string const &Person::name() const    {        return d_name;    }

The compiler prevents the data members of a class from being modifiedby one of its const member functions. Therefore a statement like

    d_name[0] = toupper(static_cast<unsigned char>(d_name[0]));

results in a compiler error when added to the above function's definition.

Const member functions are used to prevent inadvertent datamodification. Except for constructors and the destructor (cf. chapter9) only const member functions can be used with (plain, referencesor pointers to)const objects.

Const objects are frequently encountered asconst & parameters offunctions. Inside such functions only the object's const members may beused. Here is an example:

    void displayMass(ostream &out, Person const &person)    {        out << person.name() << " weighs " << person.mass() << " kg.\n";    }

Sinceperson is defined as aPerson const & the functiondisplayMass cannot call, e.g.,
person.setMass(75).

Theconst member function attribute can be used to overload member functions. When functions areoverloaded by theirconst attribute the compiler uses the memberfunction matching most closely theconst-qualification of the object:

The next example illustrates how (non)const member functions areselected:
    #include <iostream>    using namespace std;    class Members    {        public:            Members();            void member();            void member() const;    };    Members::Members()    {}    void Members::member()    {        cout << "non const member\n";    }    void Members::member() const    {        cout << "const member\n";    }    int main()    {        Members const constObject;        Members       nonConstObject;        constObject.member();        nonConstObject.member();    }    /*            Generated output:        const member        non const member    */
As a general principle of design: member functions should always be giventheconst attribute, unless they actually modify the object's data.

7.7.1: Anonymous objects

Sometimes objects are used because they offer a certain functionality. Theobjects only exist because of their functionality, and nothing in the objectsthemselves is ever changed. The following classPrint offers a facility toprint a string, using a configurable prefix and suffix. A partial classinterface could be:
    class Print    {        public:            Print(ostream &out);            void print(std::string const &prefix, std::string const &text,                     std::string const &suffix) const;    };

An interface like this would allow us to do things like:

    Print print{ cout };    for (int idx = 0; idx != argc; ++idx)        print.print("arg: ", argv[idx], "\n");

This works fine, but it could greatly be improved if we could passprint's invariant arguments toPrint's constructor. This wouldsimplifyprint's prototype (only one argument would need to be passedrather than three) and we could wrap the above code in a functionexpecting aPrint object:

    void allArgs(Print const &print, int argc, char **argv)    {        for (int idx = 0; idx != argc; ++idx)            print.print(argv[idx]);    }

The above is a fairly generic piece of code, at least it is with respecttoPrint. Sinceprefix andsuffix don't change they can be passedto the constructor which could be given the prototype:

    Print(ostream &out, string const &prefix = "", string const &suffix = "");

NowallArgs may be used as follows:

    Print p1{ cout, "arg: ", "\n" };    // prints to cout    Print p2{ cerr, "err: --", "--\n" };// prints to cerr    allArgs(p1, argc, argv);            // prints to cout    allArgs(p2, argc, argv);            // prints to cerr

But now we note thatp1 andp2 are only used inside theallArgs function. Furthermore, as we can see fromprint's prototype,print doesn't modify the internal data of thePrint object it isusing.

In such situations it is actually not necessary to define objects beforethey are used. Insteadanonymous objects may beused. Anonymous objects can be used:

When passing anonymous objects as arguments ofconst & parameters offunctions they are considered constant as they merely exist for passing theinformation of (class type) objects to those functions. This way, they cannotbe modified, nor may their non-const member functions be used. Of course, aconst_cast could be used to cast away the const reference's constness, butthat's considered bad practice on behalf of the function receiving theanonymous objects. Also, any modification to the anonymous object is lost oncethe function returns as the anonymous object ceases to exist after calling thefunction. These anonymous objects used to initialize const references shouldnot be confused with passing anonymous objects to parameters defined as rvaluereferences (section3.3.2) which have a completely different purpose inlife. Rvalue references primarily exist to be `swallowed' by functionsreceiving them. Thus, the information made available by rvalue referencesoutlives the rvalue reference objects which are also anonymous.

Anonymous objects are defined when a constructor is used without providinga name for the constructed object. Here is the corresponding example:

    allArgs(Print{ cout, "arg: ", "\n" }, argc, argv);    // prints to cout    allArgs(Print{ cerr, "err: --", "--\n" }, argc, argv);// prints to cerr

In this situation thePrint objects are constructed and immediatelypassed as first arguments to theallArgs functions, where they areaccessible as the function'sprint parameter. While theallArgsfunction is executing they can be used, but once the function has completed,the anonymousPrint objects are no longer accessible.

7.7.1.1: Subtleties with anonymous objects

Anonymous objects can be used to initialize function parametersthat areconst references to objects. These objects are created justbefore such a function is called, and are destroyed once the function hasterminated.C++'s grammar allows us to use anonymous objects in othersituations as well. Consider the following snippet of code:
    int main()    {        // initial statements        Print{ "hello", "world" };      // assume a matching constructor                                        // is available        // later statements    }

In this example an anonymousPrint object is constructed, and it isimmediately destroyed thereafter. So, following the `initialstatements' ourPrint object is constructed. Then it is destroyed againfollowed by the execution of the `later statements'.

The example illustrates that the standard lifetime rules do not apply toanonymous objects. Their lifetimes are limited to thestatements, rather than to theend of the block in which they aredefined.

Plain anonymous object are at least useful in one situation. Assume wewant to putmarkers in our code producing some output when the program'sexecution reaches a certain point. An object's constructor could beimplemented so as to provide that marker-functionality allowing us to putmarkers in our code by defining anonymous, rather than named objects.

C++'s grammar contains another remarkable characteristic illustratedby the next example:

    int main(int argc, char **argv)    {              // assume a matching constructor is available:        Print p{ cout, "", "" };            // 1        allArgs(Print{ p }, argc, argv);    // 2    }

In this example a non-anonymous objectp is constructed in statement1, which is then used in statement 2 toinitialize an anonymousobject. The anonymous object, in turn, is then used to initializeallArgs'sconst reference parameter. This use of an existing objectto initialize another object is common practice, and is based on the existenceof a so-calledcopy constructor. A copy constructor creates an object (as it is aconstructor) using an existing object's characteristics to initialize the dataof the object that's created. Copy constructors are discussed in depth inchapter9, but presently only the concept of a copy constructor isused.

In the above example a copy constructor is used to initialize an anonymousobject. The anonymous object was then used to initialize a parameter of afunction. However, when we try to apply the same trick (i.e., using anexisting object to initialize an anonymous object) to a plain statement, thecompiler generates an error: the objectp can't be redefined (in statement3, below):

    int main(int argc, char *argv[])    {        Print p{ "", "" };                  // 1        allArgs(Print(p), argc, argv);      // 2        Print(p);                           // 3 error!    }

Does this mean that using an existing object to initialize an anonymousobject that is used as function argument is OK, while an existing object can'tbe used to initialize an anonymous object in a plain statement?

The compiler actually provides us with the answer to this apparentcontradiction. About statement 3 the compiler reports something like:

    error: redeclaration of 'Print p'

which solves the problem when realizing that within a compound statementobjects and variables may be defined. Inside a compound statement, atype name followed by avariable name is the grammatical form of avariable definition.Parentheses can be used to break priorities, but ifthere are no priorities to break, they have no effect, and are simply ignoredby the compiler. In statement 3 the parentheses allowed us to get rid of theblank that's required between a type name and the variable name, but to thecompiler we wrote

        Print (p);

which is, since the parentheses are superfluous, equal to

        Print p;

thus producingp's redeclaration.

As a further example: when we define a variable using a built-in type (e.g.,double) using superfluous parentheses the compiler quietly removesthese parentheses for us:

    double ((((a))));       // weird, but OK.

To summarize our findings about anonymous variables:

7.8: The keyword `inline'

Let us take another look at the implementation of the functionPerson::name():
    std::string const &Person::name() const    {        return d_name;    }

This function is used to retrieve the name field of an object of the classPerson. Example:

    void showName(Person const &person)    {        cout << person.name();    }

To insertperson's name the following actions are performed:

Especially the first part of these actions causes some time loss, since anextra function call is necessary to retrieve the value of thename field.Sometimes a faster procedure immediately making thed_name data memberavailable is preferred without ever actually calling a functionname. This can be realized usinginline functions. An inlinefunction is a request to the compiler to insert the function's code at thelocation of the function's call. This may speed up execution by avoiding afunction call, which typically comes with some (stack handling and parameterpassing) overhead. Note thatinline is arequest to the compiler: thecompiler may decide to ignore it, andwill probably ignore it when thefunction's body contains much code. Good programming discipline suggests to beaware of this, and to avoidinline unless the function's body is small (e.g., at most one statement and it's highly unlikely that thatstatement will ever change). More on this in section7.8.2.

7.8.1: Defining members inline

Inline functions may be implementedin the class interface itself. For theclassPerson this results in the following implementation ofname:
    class Person    {        public:            std::string const &name() const            {                return d_name;            }    };

Note that theinline code of the functionname now literallyoccurs inline in the interface of the classPerson. The keywordconstis again added to the function's header.

Although members can be definedin-class (i.e.,inside the class interface itself), it is considered bad practicefor the following reasons:

Because of the above considerations inline members should not be definedin-class. Rather, they should be defined following the class interface. ThePerson::name member is therefore preferably defined as follows:
    class Person    {        public:            std::string const &name() const;    };    inline std::string const &Person::name() const    {        return d_name;    }

If it is ever necessary to cancelPerson::name's inlineimplementation, then this becomes its non-inline implementation:

    #include "person.ih"    std::string const &Person::name() const    {        return d_name;    }

Only theinline keyword needs to be removed to obtain the correctnon-inline implementation.

Defining members inline has the following effect: whenever an inline-definedfunction is called, the compiler mayinsert the function's body at thelocation of the function call. It may be that the function itself is neveractually called.

This construction, where the function code itself is inserted rather than acall to the function, is called aninline function. Note that using inlinefunctions may result in multiple occurrences of the code of those functions ina program: one copy for each invocation of the inline function. This isprobably OK if the function is a small one, and needs to be executedfast. It's not so desirable if the code of the function is extensive. Thecompiler knows this too, and handles the use of inline functions as arequest rather than acommand. If the compiler considers the functiontoo long, it will not grant the request. Instead it will treat the function asa normal function.

Note thatconstexpr functions (cf. section8.1.4) are implicitlydefined as inline functions.

7.8.2: When to use inline functions

When shouldinline functions be used, and when not? There are some rules of thumb which may be followed: All inline functions have one disadvantage: theactual code is inserted by the compiler and must therefore be known atcompile-time. Therefore, as mentioned earlier, an inline function can never belocated in a run-time library. Practically this means that an inline functionis found near the interface of a class, usually in the same header file. Theresult is a header file which not only shows thedeclaration of a class,but also part of itsimplementation, thus always blurring the distinctionbetween interface and implementation.

7.8.2.1: A prelude: when NOT to use inline functions

As a prelude to chapter14 (Polymorphism), there is onesituation in which inline functions should definitely be avoided. At thispoint in theC++ Annotations it's a bit too early to expose the full details,but since the keywordinline is the topic of this section this isconsidered the appropriate location for the advice.

There are situations where the compiler is confronted with so-calledvague linkage
(cf.http://gcc.gnu.org/onlinedocs/gcc-4.6.0/gcc/Vague-Linkage.html). Thesesituations occur when the compiler does not have a clear indication in whatobject file to put its compiled code. This happens, e.g., with inlinefunctions, which are usually encountered in multiple source files. Since thecompiler may insert the code of ordinary inline functions in places wherethese functions are called, vague linking is usually no problem with theseordinary functions.

However, as explained in chapter14, when using polymorphismthe compiler must ignore theinline keyword and define so-calledvirtual members as true (out-of-line) functions. In this situation the vague linkage may cause problems, as thecompiler must decide in what object files(s) to put their code. Usually that'snot a big problem as long as the function is at least called once. But virtualfunctions are special in the sense that they may very well never be explicitlycalled. On some architectures (e.g., armel) the compiler may fail to compilesuch inline virtual functions. This may result in missing symbols in programsusing them. To make matters slightly more complex: the problem may emerge whenshared libraries are used, but not when static libraries are used.

To avoid all of these problems virtual functions shouldnever be definedinline, but they should always be definedout-of-line. I.e., they shouldbe defined in source files.

7.8.3: Inline variables

In addition to inline functions, inline variables can be defined (and identically initialized) in multipletranslation units. E.g., a header file could contain
    inline int value = 15;                      // OK    class Demo    {        // static int s_value = 15;             // ERROR        static int constexpr s_value = 15;      // OK        static int s_inline;                    // OK: see below: the inline                                                 //   definition follows the                                                 //   class declaration    };    inline int Demo::s_inline = 20;             // OK

7.9: Local classes: classes inside functions

Classes are usually defined at the global or namespace level. However, it isentirely possible to define a local class, i.e., inside afunction. Such classes are calledlocal classes.

Local classes can be very useful in advanced applications involvinginheritance or templates (cf. section13.8). At this point in theC++ Annotations they have limited use, although their main features can bedescribed. At the end of this section an example is provided.

#include <iostream>#include <string>using namespace std;int main(int argc, char **argv){    static size_t staticValue = 0;    class Local    {        int d_argc;             // non-static data members OK        public:            enum                // enums OK            {                VALUE = 5            };            Local(int argc)     // constructors and member functions OK            :                   // in-class implementation required                d_argc(argc)            {                                // global data: accessible                cout << "Local constructor\n";                                // static function variables: accessible                staticValue += 5;            }            static void hello() // static member functions: OK            {                cout << "hello world\n";            }    };    Local::hello();             // call Local static member    Local loc{ argc };          // define object of a local class.}

7.10: The keyword `mutable'

Earlier, in section7.7, the concepts of const memberfunctions and const objects were introduced.

C++ also allows the declaration of data members which may be modified,even by const member function. Declarations of such data members start withthe keywordmutable.

Mutable should be used for those data members that may be modified withoutlogically changing the object, which might therefore still be considered aconstant object.

An example of a situation wheremutable is appropriately used is found inthe implementation of a string class. Consider thestd::string'sc_stranddata members. The actual data returned by the two members areidentical, butc_str must ensure that the returned string is terminated byan 0-byte. As a string object has both a length and a capacity an easyway to implementc_str is to ensure that the string's capacity exceeds itslength by at least one character. This invariant allowsc_str to beimplemented as follows:

    char const *string::c_str() const    {        d_data[d_length] = 0;        return d_data;    }

This implementation logically does not modify the object's data as thebytes beyond the object's initial (length) characters have undefinedvalues. But in order to use this implementationd_data must be declaredmutable:

    mutable char *d_data;

The keywordmutable is also useful in classes implementing, e.g.,reference counting. Consider a class implementing reference counting forstrings. The object doing the reference counting might be a const object,but the class may define a copy constructor. Since const objects can't bemodified, how would the copy constructor be able to increment the referencecount? Here themutable keyword may profitably be used, as it can beincremented and decremented, even though its object is a const object.

The keywordmutable should sparingly be used. Data modified by constmember functions should never logically modify the object, and it should beeasy to demonstrate this. As arule of thumb: do not usemutableunless there is a very clear reason (the object is logically not altered) forviolating this rule.

7.11: Header file organization

In section2.5.10 the requirements for header files when aC++program also usesC functions were discussed. Header files containing class interfaces have additionalrequirements.

First, source files. With the exception of the occasional classlessfunction, source files contain the code of member functions of classes.Basically, there are two approaches:

The first alternative has the advantage of economy for the compiler: itonly needs to read the header files that are necessary for a particular sourcefile. It has the disadvantage that the program developer must include multipleheader files again and again in source files: it both takes time to type theinclude-directives and to think about the header files which are needed ina particular source file.

The second alternative has the advantage of economy for the program developer:the header file of the class accumulates header files, so it tends to becomemore and more generally useful. It has the disadvantage that the compilerfrequently has to process many header files which aren't actually used by thefunction to compile.

With computers running faster and faster (and compilers getting smarter andsmarter) I think the second alternative is to be preferred over the firstalternative. So, as a starting point source files of a particular classMyClass could be organized according to the following example:

    #include <myclass.h>    int MyClass::aMemberFunction()    {}

There is only oneinclude-directive. Note that the directive refers toa header file in a directory mentioned in theINCLUDE-file environmentvariable. Local header files (using#include "myclass.h") could be usedtoo, but that tends to complicate the organization of the class header fileitself somewhat.

The organization of the header file itself requires some attention. Considerthe following example, in which two classesFile andString areused.

Assume theFile class has a membergets(String &destination), whilethe classString has a member functiongetLine(File &file). The(partial) header file for theclass String is then:

    #ifndef STRING_H_    #define STRING_H_    #include <project/file.h>   // to know about a File    class String    {        public:            void getLine(File &file);    };    #endif

Unfortunately a similar setup is required for the classFile:

    #ifndef FILE_H_    #define FILE_H_    #include <project/string.h>   // to know about a String    class File    {        public:            void gets(String &string);    };    #endif

Now we have created a problem. The compiler, trying to compile the sourcefile of the functionFile::gets proceeds as follows:

The solution to this problem is to use aforward class referencebefore the class interface, and to includethe corresponding class header filebeyond the class interface. So we get:
    #ifndef STRING_H_    #define STRING_H_    class File;                 // forward reference    class String    {        public:            void getLine(File &file);    };    #include <project/file.h>   // to know about a File    #endif

A similar setup is required for the classFile:

    #ifndef FILE_H_    #define FILE_H_    class String;               // forward reference    class File    {        public:            void gets(String &string);    };    #include <project/string.h>   // to know about a String    #endif

This works well in all situations where either references or pointers toother classes are involved and with (non-inline) member functions havingclass-type return values or parameters.

This setup doesn't work withcomposition, nor with in-class inlinemember functions. Assume the classFile has acomposed data member ofthe classString. In that case, the class interface of the classFilemust include the header file of the classString before the classinterface itself, because otherwise the compiler can't tell how big aFileobject is. AFile object contains aString member, but the compilercan't determine the size of thatString data member and thus, byimplication, it can't determine the size of aFile object.

In cases where classes contain composed objects (or are derived from otherclasses, see chapter13) the header files of the classes of thecomposed objects must have been readbefore the class interface itself.In such a case theclass File might be defined as follows:

    #ifndef FILE_H_    #define FILE_H_    #include <project/string.h>     // to know about a String    class File    {        String d_line;              // composition !        public:            void gets(String &string);    };    #endif

The classString can't declare aFile object as a composed member:such a situation would again result in an undefined class while compiling thesources of these classes.

All remaining header files (appearing below the class interface itself)are required only because they are used by the class's source files.

This approach allows us to introduce yet another refinement:

7.11.1: Using namespaces in header files

When entities from namespaces are used in headerfiles, nousing directive should be specified in those headerfiles if they are to be used as general header files declaring classes orother entities from alibrary. When theusing directive is used in aheader file then users of such a header file are forced to accept and use thedeclarations in all code that includes the particular header file.

For example, if in a namespacespecial an objectInserter cout isdeclared, thenspecial::cout is of course a different object thanstd::cout. Now, if a classFlaw is constructed, in which theconstructor expects a reference to aspecial::Inserter, then the classshould be constructed as follows:

    class special::Inserter;    class Flaw    {        public:            Flaw(special::Inserter &ins);    };

Now the person designing the classFlaw may be in a lazy mood, andmight get bored by continuously having to prefixspecial:: before everyentity from that namespace. So, the following construction is used:

    using namespace special;    class Inserter;    class Flaw    {        public:            Flaw(Inserter &ins);    };

This works fine, up to the point where somebody wants to includeflaw.h in other source files: because of theusing directive, thislatter person is now by implication alsousing namespace special, whichcould produce unwanted or unexpected effects:

    #include <flaw.h>    #include <iostream>    using std::cout;    int main()    {        cout << "starting\n";       // won't compile    }

The compiler is confronted with two interpretations forcout: first,because of theusing directive in theflaw.h header file, it considerscout aspecial::Inserter, then, because of theusing directive inthe user program, it considerscout astd::ostream. Consequently,the compiler reports an error.

As arule of thumb, header files intended for general useshould not containusing declarations. This rule does not hold true forheader files which are only included by the sources of a class: here theprogrammer is free to apply as manyusing declarations as desired, asthese directives never reach other sources.

7.12: Sizeof applied to class data members

InC++ the well-knownsizeof operator can be applied to datamembers of classes without the need to specify an object as well. Consider:
    class Data    {        std::string d_name;        ...    };

To obtain the size ofData'sd_name member the followingexpression can be used:

    sizeof(Data::d_name);

Note, however, that the compiler observes data protection here as well. It'sonly possible to usesizeof(Data::d_name) whend_name is visible,i.e., it can be used byData's member functions and friends.




[8]ページ先頭

©2009-2025 Movatter.jp