Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

You Can Program In C++ (2006) [eng]

.pdf
Скачиваний:
80
Добавлен:
16.08.2013
Размер:
4.33 Mб
Скачать

188

CHAPTER 9

Initializer Lists

C++ has a special syntax to provide for initializing data members. When defining a constructor, a colon after the closing parenthesis of the parameter list for a constructor introduces a list of initializers. This list ends at the opening brace of the body of the definition. We place the individual members that are to be initialized explicitly in a comma-separated list; each data member name is followed by the initializing expression in parentheses.

Members are always initialized in their order of declaration in the class; there is no significance to changing the order of the initializer list.

Some data members must be initialized (reference members, const members, and members that do not have a default initializer); these must be explicitly provided for in an initializer list. In a few cases, members cannot be fully initialized, and the final values must be assigned from within the body of the constructor. The main example of this is where a data member is a raw C-style array.

Unless you know that the member in question has a default initializer that will do a correct job, it is good practice to initialize data (where possible) in an initializer list.

Special Members

The C++ standard specifies four special member functions that will be implicitly declared unless some action of the programmer inhibits this behavior. These are:

default constructor: the function that specifies how to create a new instance without any provided data. The explicit declaration of any constructor in a class definition suppresses implicit declaration of a default constructor.

copy constructor: the function that specifies how to create a new instance of a class as a copy of an existing instance. The compiler implicitly declares a copy constructor unless the programmer explicitly declares a copy constructor in the class definition.

destructor: The compiler will implicitly declare a destructor unless the programmer explicitly declares one in the class definition.

copy-assignment operator: This is an overload of operator= that specifies how the state/value of an instance of the class appearing as the right-hand operand of an = is used to replace the state/value of the left-hand operand of the same type. The compiler will implicitly declare a copy assignment operator unless the programmer explicitly declares one. Note that declaring some other overload of operator= does not suppress implicit declaration and compiler generation of a copy-assignment version.

Implementation

We call the choice of data members and the definitions of the member functions the implementation of a class. Wherever possible, we place the implementation of a class in a separate file of source code. As the compiler needs to know about memory requirements for class instances, the data members have to be visible to the compiler when dealing with application-level source code, and so the declarations of data members are part of the class definition. Though the data members are visible, we usually make them inaccessible by placing the relevant declarations in the private interface of the class.

Elaborated Versus Simple Names

When you are inside the scope of a class, either because you are in the definition of the class itself or because you are defining a class member, the simple name as declared in the class definition is enough. However, outside the scope of a class, you need to identify what class is using the name. When defining member functions or static class data, that requires that you prefix the simple name with the class name and scope operator (::).

C H A P T E R 10

User-Defined Types, Part 3: Simple classes

(homogeneous entity types)

In the last chapter, we focused on using a class to provide a new user-defined value type. That is a type whose instances we would freely copy, for example, to provide arguments for function calls and returns from functions. In this chapter, we are going to look at using classes to provide types where we would want to use the same instance in most of our code – a copy will not do.

Another way of looking at the difference between a value type and an entity type is by considering what we mean when we ask whether two instances are the same. When we are thinking in terms of values, we can fairly say that two distinct instances (in the programming sense that they occupy different memory or have different addresses) are the same if they can be used interchangeably without affecting a program. When we are thinking in terms of entities, we only regard two ‘instances’ as the same if they actually occupy the same memory (i.e. have the same address).

Many people refer to ‘entity’ types as ‘object’ types. This leads to confusion in the context of C++, where the term ‘object’ is much closer in meaning to ‘instance’. When I use the term ‘object’ in this book, I am referring to a specific instance of a type, regardless of whether it is semantically a value, an entity, or something else.

Examples of Value and Entity Types

Many people have difficulty with the dichotomy between value and entity types. In the last chapter, the isbn10 type was a clear example of a value type. So too the title of a book, the publisher’s name, and the copyright date would be represented by value types. An actual book would normally be an entity. Contrast telling a friend the ISBN, title, authorship, and publisher of a book with lending them a book. I also wrote about playing cards, and developed a card value type to handle the values of playing cards. You would be surprised if you were dealt two aces of spades when playing poker (the cards are entities), but there is no problem in several people recording a hand of cards (they are only interested in the values, not the actual cards).

Here are a few other examples of value–entity pairs:

A credit card number versus a credit card. The credit card number is one of several attributes of a credit card, one that is often copied for use when purchasing items by mail order. However, copying credit cards themselves is normally a criminal activity.

A car number versus a car number plate. A car number is something that we can, for example, note down when we witness an accident. A car number plate is something that is normally one of a pair, and further copying would be suspect.

190

CHAPTER 10

An address versus a house. Houses have addresses (some other things do as well). We can easily pass copies of an address around, but copying a house is hard.

One of the properties of an entity is that it is something that has some form of existence. That existence has some significance that makes copies distinct from the original. On the other hand, a value is something where any copy is as good as (and indistinguishable from) any other.

A Simple Playing-Card Entity

This example uses the card value type from the last chapter to provide a card type that represents playing cards as entities. Here is my definition for card:

class card{ public:

explicit card(card_value); explicit card(std::istream &); ~card( );

void send_to(std::ostream &)const; bool is_same(card const &)const; card_value get_value( )const;

private:

card(card const &); // disable copy constructor void operator=(card const &); // disable assignment card_value const value_;

};

I have not provided a default constructor (i.e. one that does not need arguments). The compiler will not generate one for me because I have declared at least one other constructor (in fact I have declared three). Compiler-generation of the default constructor only happens if no constructors are declared explicitly.

I have provided two public ways to create a card instance. The first is to create a card with a specific value, which means that there are no ‘blank’ cards. The second one allows us to create a card from data provided by an input stream. As I am not allowing a card to change its value magically, I need to provide a way to create a card from data provided externally.

This type does not need an explicit destructor, but I am providing one for the time being. This will allow me to instrument it so that its use is visible during the execution of programs.

The next three member functions provide for sending a card’s data to an output stream, checking to see whether two names refer to the same card instance, and extracting the value of a card. The last of those will allow us to check whether two cards have the same value as distinct from checking whether two names refer to the same card.

There are various alternative declarations that would provide us with the same abilities; however, we should try to keep class interfaces lean.

The first two declarations in the private interface are the idiomatic way to suppress copy semantics. We declare the two relevant special member functions (the copy constructor and the copy-assignment function) as private members. Consequently, these members are inaccessible to code outside the implementation. We usually do not provide definitions for these members; that protects us against accidental usage within the class implementation.

I have chosen to declare value as const because I consider a playing card to have an immutable value. Some compilers may be able to make use of this design choice to produce better-optimized code. However, that is not in itself a reason for making the value immutable, and we will see that such a design

USER-DEFINED TYPES, PART 3: SIMPLE CLASSES (HOMOGENEOUS ENTITY TYPES)

191

choice can have unforeseen consequences (which is a reason for doing it here: you will have a chance to see the consequences).

Implementation of card

Most of the implementation is trivial. When you come to try code, place the class definition in the same header file as card value (but after it) and the implementation in the implementation file for card value.

card::card(card_value cv):value_(cv){

std::clog << "Card instance created from " << cv << ".\n";

}

card::~card( ){

std::clog << "The " << value_ << " card destroyed.\n";

}

void card::send_to(std::ostream & out)const{ out << value_;

}

bool card::is_same(card const & c)const{ return this == &c;

}

card_value card::get_value( )const{ return value_;

}

One of the constructors is problematical, because const data members must be initialized in the initializer list. We need to deal with the problem of getting a card value from an input stream. The normal process of using a streaming operator will not work here. We need some help. One of the benefits of the fgw::read<> function template from my library is that it supports initialization. You will need to add #include "fgw text.h" to the implementation file and tell the compiler where it can find that header by using the Project Settings dialog box to add the fgw headers subdirectory to the includes path. Here is an implementation of card(std::istream &) that uses fgw::read<>:

card::card(std::istream & in):value_(fgw::read<card_value>(in)){ std::clog << "Card instance created from " << value_

<< " supplied from input.\n";

}

If you did not have access to fgw text.h, you could achieve a similar result by adding a helper function in the implementation file. For example, you could add:

card_value get_it(std::istream & in){ card_value temp;

in >> temp; return temp;

}

That implementation requires extra work to make it robust. The helper function allows us to define card(std::istream &) as:

card::card(std::istream & in):value_(get_it(in)){ std::clog << "Card instance created from " << value_

<< " supplied from input.\n";

}

192

CHAPTER 10

T R Y T H I S

Add the above code to the appropriate files, and then compile and execute a program with:

int main( ){

card c(card_value(12));

}

Make sure you are using instrumented versions of both card value and card. Study the resulting output; it will give you some idea as to how much can sometimes be hidden under the hood. Programmers from other languages are sometimes concerned that even a simple C++ program may generate a lot of code. In fact, the code is necessary to achieve our objectives, and all that C++ has done is allow us to reduce the amount of code we write at the application level.

EXERCISES

1.Write a complete test program for card.

2.Add an inline overload of operator<<( ) to output a card to an output stream.

3.Comment on why the current design does not support an operator>>( ).

4.Add a constructor to card that constructs a card directly from a card value::denomination and a card value::suit.

5.When you checked the output from your test code, you may have noticed that it reported calls of the copy constructor and destructor for card value when we constructed a card from a card value. Consider how you might remove that copy. Consider whether this removal would be helpful in the case of uninstrumented code. Hint: card value instances consist of a simple int value internally and so should be no more expensive to copy than copying an int would be.

Another Entity Type: Deck of Cards

My second example of an entity type is rather different, and we will see that it has some impact on our design for card value and card. One of the problems we will need to address is whether we should handle the individual cards in a deck of cards as values (card value instances) or entities (card instances). I am going to do my initial design using card value instances and then revisit the design from the more natural

perspective where they are card instances.

class deck{ public:

deck( ); ~deck( );

void shuffle( ); card_value next( ); void top( );

void copy_from(deck const &);

USER-DEFINED TYPES, PART 3: SIMPLE CLASSES (HOMOGENEOUS ENTITY TYPES)

193

private:

deck(deck const &);

deck & operator=(deck const &); static int const cards = 52; int position;

card_value pack[cards];

};

Later we will find that there are several flaws in this definition.

Let us look at the members to see how we could implement them. I will start with the data members. The declaration of cards introduces a special syntax that C++ allows for a member that is static (i.e. belonging to the class as a whole), of an integer type (int in this case), and const (fixed in value). This special syntax allows provision of the value as part of the declaration. Note that using that provision does not make the declaration into a definition. The definition still has to be provided exactly once in the program. In this case, we need to add the statement

int const deck::cards;

to the implementation file for deck (usually we would name that file deck.cpp). Three things to note: there is no static in the definition; we have to write deck::cards; and we must not provide the value again (if you use the facility for providing the value in the class definition, it must not be provided in the member definition). The purpose of the declaration is to name 52 as the number of cards in a pack.

We are going to use the instance variable position to keep track of where we are in the deck for such purposes as dealing cards. The last piece of data tells the compiler that a deck consists of an array of card value called pack. One of the requirements for a dimension of an array is that it must be an integer value known to the compiler. One of the principle motives for the special syntax for providing a value in the declaration of a static const integer member was to support declarations of array members with a named value as the dimension.

As we are looking at the private interface of deck, I might as well explain those two function declarations. You may recognize them from the previous chapter as being the copy constructor and copy-assignment operator for deck. If we did not declare them, the compiler would declare them implicitly as members of the public interface. I want to remove copy semantics from deck, because it would almost certainly be an error to pass or return a deck by value. The idiomatic way to do that is to declare the two special functions dealing with copying as members of the private interface. That means that if a programmer accidentally tries to copy an instance of deck the compiler will report an error (an access violation for attempting to use a private member of a class). As we do not want to use copy semantics, we do not provide a definition for these two functions in the implementation file. That adds an extra safeguard in case we forget and try to copy a deck in a member function. In that case, the linker will give us a missing-definition error message.

Having just gone to a lot of trouble to remove accidental copying, you may be surprised by the copy from( ) public member function. While we do not want a deck passed or returned by value, nor do we want one changed by a copy assignment: we might still want to produce a duplicate explicitly. That is the purpose of copy from( ). Here is a suitable implementation of copy from( ):

void deck::copy_from(deck const & source){ for(int i(0); i != cards; ++i){

pack[i] = source.pack[i]; position = source.position;

}

}

Now let us look at the constructor. Here we have to deal with the rule that we must use a default constructor to create the elements of an array – we have no option. That means that we will have to establish the correct cards inside the body of the constructor for deck. I am going to opt for a simple implementation

194

CHAPTER 10

even though expert programmers would (legitimately) criticize it as coupling the card value and deck implementations. However, it is worth noting that the coupling is real because a deck is a collection of cards.

deck::deck( ):position(0){

for(int i(0); i != cards; ++i) pack[i] = card_value(i);

}

This definition uses the card value(int) constructor of card value and the copy-assignment operator for card value to replace the default created card value so that our pack has one copy of each possible card.

The destructor for deck has nothing to do (unless you want to instrument it). The array is destroyed automatically after the body of the destructor has run. The process of destruction will destroy each of the elements of the array by calling the card value destructor for it. The elements of an array are destroyed in the reverse order of their construction. Note that you can (and should) check that assertion by using the instrumented implementation of card value. When you do so, you will see that quite a lot of construction and destruction happens in the background.

I will come back to shuffle( ) in a moment, but first I will deal with the two other member functions. The purpose of next( ) is to return the value of the card value currently identified by position, and increment position by one. Here is a simple implementation:

card_value deck::next( ){ card_value value(pack[position]); ++position;

if(position == cards) position = 0; return value;

}

We deal with going off the end of the pack with the simple solution of returning to the top.

The intention of the top( ) function is to reset position to the top. That makes it simple to define it as:

void deck::top( ){position = 0;}

Finally, we need to implement a function that will shuffle the deck. Most of the work for such a function can be done by using the Standard C++ Library function random shuffle( ) (declared in the algorithm header). We can call that function for any sequence of entities if we supply an iterator to the start and an iterator to one past the end as the two arguments. Iterators are generalized location indicators; in the case of arrays they are simple pointers to the elements of the array. We get the start of an array by using the array name, and we get one past the end by adding on the number of elements in the array. Put that all together and we get:

void deck::shuffle( ){ std::random_shuffle(pack, pack + cards);

}

 

 

 

 

 

 

 

 

TASK 10.1

 

Create a new project (in the chapter 10 directory). Copy card.h and

 

 

 

 

 

 

 

 

 

 

card.cpp from the chapter 9 directory. (You will be changing some of the

 

 

 

 

 

 

 

 

 

source code, so we want new copies, and it might be wise to rename them

 

 

 

 

so that you do not confuse different versions.) Now create a deck.h file and

 

 

insert a suitable header guard and the definition of the deck class. In addition

 

 

to the code above, you will need to include card.h, so that the compiler can

 

 

see the definition of card value when we use it in the definition of deck.

 

 

 

 

 

 

 

 

 

USER-DEFINED TYPES, PART 3: SIMPLE CLASSES (HOMOGENEOUS ENTITY TYPES)

195

Create a file named deck.cpp, and copy the implementation of the deck member functions and the static data member into it. You will need to include some headers and header files (I am leaving you to determine which). It is usually good practice to have a way to determine the order in which you write the #includes. My rule is to deal with the header files first and the C++ headers second. Within each group, I include them in alphabetical order.

Finally create a file named testdeck.cpp and add necessary headers and header files (in future, I will just use the term ‘header’ to include both Standard headers and user-written/third-party library header files). Add the following minimal version of main( ):

int main( ){ try{

deck d;

}

catch(...){std::cerr

<< "An exception was caught in main\n";}

}

Your project will need to include card.cpp, deck.cpp, and testdeck.cpp. When all those files compile successfully, build and execute the program. You will be flooded with messages produced by the instrumentation of the card value class. Modify the implementation of card value by removing the instrumentation and try again. This is now so simple that you will get no output. Add some instrumentation to the constructor and destructor for deck and try again.

It makes sense to keep the instrumented and uninstrumented implementations alongside each other in similarly (but not identically) named files. That way you can select which you use by editing the project to include the version you require. Another option would be to have two subdirectories, one for instrumented code and one for release versions. This option depends on your IDE allowing you to use .cpp files from different directories.

Output for deck

Before we test the other public member functions of deck, we need a way to examine the deck by printing out the cards. The following function does the job but reveals a design flaw in deck. In a moment, we will look at the flaw in the context of this draft definition:

std::ostream & operator<<(std::ostream & out, deck const & d){ d.top( );

for(int i(0); i != 52; ++i) out << d.next( ) << '\n'; return out;

}

I have chosen to use operator<< for output because C++ programmers expect that operator to do output. We would also expect that output of an object’s state would not alter the state, which is why the second parameter is a const reference rather than a simple reference. At this point, we hit the first problem.

196

CHAPTER 10

If we want to output the entire deck we must make sure we start at the beginning. The only member function we have for getting card value values is next( ). That is not a const member function, because it changes the value stored in our position variable. That means that we cannot use next( ) on a const reference to a deck. In addition, we do not know what value is currently stored in position. The only way we can access that is by using top( ) to reset position to 0. The consequence is that our deck type does not have the functionality that we need, even though it looked OK to start with. Yes, we could remove that const qualification from the second parameter of the operator<< overload for deck. However, that only hides the problem: as currently designed, outputting a deck to a stream changes its internals.

There are several ways to fix this problem but let us stick with a simple one. What we need is a way to find which card value value is at a specified point in the deck. We can achieve this using a member function with the following declaration:

card_value get_card(int pos)const;

The definition seems simple:

card_value deck::get_card(int pos)const{ return deck[pos];

}

However, do you see the problem? Yes, it is not robust: a user could ask for a card at a position that is not valid for a standard deck (outside the range 0 to 51). Again, let us stick with a simple solution even though it is not up to production quality. Check and throw an exception if the value of pos is out of range. (You will need to include the stdexcept header so that the compiler can see a declaration of std::range error.)

card_value deck::get_card(int pos)const{ if(pos < 0 or pos > (cards - 1))

throw std::range_error("No such position in deck"); return pack[pos];

}

Using that in the definition of operator<< we get:

std::ostream & operator<<(std::ostream & out, deck const & d){ for(int i(0); i != 52; ++i) out << d.get_card(i) << '\n'; return out;

}

Even this code has a problem – the use of a magic 52. This highlights that the number of cards in a deck is not a private property but a public one. We could just add a static member function that returns the value. However, that seems somewhat excessive for such a case, and I would be happy to make the declaration of cards part of the public interface of deck. I want to keep cards within the scope of deck because it is specifically a property of deck rather than being some global value. We should always declare variables and constants in the smallest scope that supports their use. Once we have moved the declaration of cards to the public interface we can write:

std::ostream & operator<<(std::ostream & out, deck const & d){ for(int i(0); i != deck::cards; ++i) out << d.get_card(i) << '\n'; return out;

}

USER-DEFINED TYPES, PART 3: SIMPLE CLASSES (HOMOGENEOUS ENTITY TYPES)

197

All the changes I have been suggesting have stuck with the design rule that we should avoid invalidating earlier code. Moving something from the private interface to the public interface is fine: old code will continue to work. Similarly, adding a new member function is usually acceptable, though we sometimes need to think before adding a new overload to an existing function. By the way, adding a const qualification is usually acceptable, but removing one almost always has the potential for breaking existing code.

Modify deck.h and deck.cpp so that the last version of std::ostream &

TASK 10.2 operator<<(std::ostream & out, deck const & d) compiles satisfactorily. Now modify testdeck.cpp to define main( ) as:

int main( ){ try{

deck d; d.shuffle( ); std::cout << d;

}

catch(...){std::cerr

<< "An exception was caught in main.\n";}

}

Build the project and run it a couple of times. Do you notice a problem? Each run of the program produces an identical shuffle. That may be fine when we are testing code, but it is not much use if we want to shuffle the deck differently each time. The C++ Standard is silent on this issue of how we can randomize the pseudo-random number generator used by the default version of random shuffle (perhaps because there is an alternative form of random shuffle( ) that uses a user-written pseudo-random number generator). In the case of the version of the Library that ships with this book, using std::srand( ) (declared in the cstdlib header) to seed the generator used by std::rand( ) works fine. (Do not worry too much about these little details; I am only including them to prevent false expectations if you later use a different C++ implementation.) Therefore, modify main( ) to:

int main( ){ try{

std::srand(1); deck d; d.shuffle( ); std::cout << d;

}

catch(...){std::cerr

<< "An exception was caught in main.\n";}

}

However, that is no immediate improvement, because we have hardwired the seed used by std::srand( ). If we really want different results each time we run the program, we need to remove that hard-coded argument