You Can Program In C++ (2006) [eng]
.pdf308 |
CHAPTER 17 |
However, we would want to be able to extend a container of primes by adding new ones to it. That suggests that we would like to be able to write:
std::vector<int> vec_of_primes(5) = {2, 3, 5, 7, 11};
The language does not support such syntax, or anything close to it. All is not lost, because we can use the facility for constructing a container to contain copies of the elements of a different container. It does require an extra line of code, but normally we can live with that. Here is how:
int const prime_values[ ] = {2, 3, 5, 7, 11}; // compute the number of elements:
int const provided_values(sizeof(prime_values) / sizeof(int)); std::vector<int>
vec_of_primes(prime_values, prime_values + provided_values};
The line that defines provided values is the idiomatic way of making code robust against changes in the number of elements used in initializing an array. I have const-qualified prime values to give a hint to the compiler that it may, if it can, optimize away the storage for prime values, because I am only going to use the values, not change them.
Future versions of C++ may make this mechanism unnecessary.
EXERCISES
1.The above definition of find anagram( ) ends its output with a comma. Improve the output so that this is not the case. (There are several ways to do this, but you might find using a std::stringstream useful as a way to prepare output.)
2.Change the definition of print anagram dictionary( ) so that it prints each letter selection followed by a colon and then a comma-separated list of the words formed from that selection.
3.While the sorted letters in the anagram dictionary are held in alphabetical order, the words that can be made from a specific selection are not ordered. Modify your solution to Exercise 2 so that the words are listed in alphabetical order.
4.Combine the solutions of Problems 1 and 3 of this chapter to be able to use an ordinary file of text as the source of words for the anagram dictionary. Warning: you will need to be careful that you do not add the same word more than once to the dictionary. It is worth considering a multi-stage solution where you first extract the words from the current anagram dictionary into a set, then add the new words to this set, and finally create a new version of the anagram dictionary. This is only one of several good ways to solve this problem.
Conclusion
I have only given you the briefest of introductions to the STL and similar components. Undoubtedly, template technology is one of the most powerful features of C++. Writing good templates is time-consuming and makes considerable demands on the programmer’s C++ skills. However, using well-designed templates such as those in the Standard Library will save a good deal of your programming time.
CONTAINERS, ITERATORS, AND ALGORITHMS |
309 |
Other sources of template components include the library extensions provided by the first Standard C++ Library Technical Report. While the Standard C++ Library is nowhere near as extensive as that provided by Java, those components that are in it have been carefully designed and expertly implemented.
REFERENCE SECTION
Containers
There are two major categories of container: sequence containers and associative containers. A sequence container is one where notionally the elements of the container can be arranged in some arbitrary order. An associative container is one in which there is some rule that predetermines the way that the elements will be organized. Rather than try to pin down the differences in words, it is probably easier to describe the principal STL containers.
Each of the Standard Library containers has one or more constructors for construction from another container or sequence (these constructors take two iterator arguments). Although it is not possible to construct a raw array from a Library container (because arrays do not have constructors), it is possible to construct a Library container from an array by using a pointer to the start and one beyond the end of the array.
Note that all the standard containers are value-based and require that the type of the contained elements supports standard copy semantics (i.e. has both a public copy constructor and a copyassignment operator, each of whose arguments is a const reference). The reason that you cannot have a container of std::auto ptr<> is that it does not have standard copy semantics – it changes the instance being copied, to transfer ownership of the object it points to.
Sequence containers
The raw (built-in) array is the simplest sequence container. Unlike the other C++ sequence containers, it has a fixed number of elements. The reason that you need to know that it meets the STL requirements for a sequence container is that we can apply any algorithm to an array as long as it does not attempt to change the number of elements.
The STL provides three other sequence containers: std::vector, std::deque, and std::list.
std::vector<> provides all the functionality of a dynamically sized array; the elements are held in a contiguous array. This container type is designed for efficient growth and access. One consequence of supporting growth in a data structure that holds its data in contiguous storage is that the data may be relocated when the amount of it changes, thereby invalidating any iterators or pointers into the prior version. That means that you should be wary of holding on to iterators into a std::vector<>, because they may be made invalid by actions that change the size of the container.
Because of the possibility of relocation, which will invalidate iterators, it is usually better to use subscripting to access the elements of a std::vector<>. As long as a std::vector<> named vec has at least n + 1 elements, vec[n] will be valid (and refer to the last element of vec).
std::deque<> (stands for double-ended queue) uses a different internal structure, which allows growth at both ends and guarantees that the elements are not moved during the lifetime of an instance of a std::deque<>. Meeting that design objective requires an extra level of indirection, so access to individual elements is marginally slower, but the benefit is that iterators into a std::deque<> are more stable and we can efficiently add new elements at the beginning as well as at the end.
std::basic string<> is also designed as a sequence container, but one for character-like elements. std::string, which we have used extensively, is a specialization of std::basic string<> for a char.
314 |
CHAPTER 18 |
The answer is that it does not matter, but it helps to be consistent and use the style of your colleagues. What does matter is that we initialize pointers in their definitions. Without further context, you do not know whether the above statements are just declarations (as they would be in the scope of a class definition) or definitions (as they would be at block scope).
When you write code for yourself, choose any layout conventions that you are happy with; when you write code in a team, use that which the team uses. However, in both cases be consistent. If you write things differently, it should be because you are trying to highlight a difference.
For example, we can use either struct or class in defining a class type. In the modern style for C++, the public interface is listed first (i.e. we write things in a need-to-know order), and it makes no difference to the rest of your code whether you write
struct example{ public:
//interface private:
//interface
};
or:
class example{ public:
//interface private:
//interface
};
However, we could leave out the use of the public access specifier in the first case. No competent C++ programmer that I know would justify using struct rather than class so that they could save typing public:. It is idiomatic in C++ that using the struct keyword highlights the fact that we are including public data members.
A slightly less strong idiom concerns the choice of class or typename for declaring template type parameters. Either pick one and stick with it, or join the growing band that uses typename when the parameter can be any type, and class when the parameter is essentially restricted to user-defined types.
Where to Put const
The const keyword was introduced to C (borrowed from C++ during the 1980s) well after the language was in extensive use. There was no great reason to choose where the keyword went as a qualifier. As a result, C programmers got into the habit of putting it first whenever that was possible. Generally, the only time it was not possible was when qualifying a pointer as const rather than qualifying what a pointer was pointing to. C++ is different in that there are more uses of const. One of those uses is in qualifying member functions as ones that do not mutate the object data. That meant that there were now two places where const had to be to the right of what it qualified.
Then, in the 1990s, Dan Saks noticed that the increasing use of typedef applied to pointers was causing another misunderstanding among many programmers (not the expert ones, but many others). Here is a short code snippet that illustrates the problem:
typedef int * int_ptr; int i;
const int_ptr i_ptr(&i);
SOMETHING OLD, SOMETHING NEW |
315 |
Many programmers think of typedef as some form of macro. They think of it as just an alternative to:
#define INT_PTR int *
So they mistakenly interpret the definition of i ptr above as:
const int * i_ptr(&i);
In other words, they read it as: i ptr is a pointer granting read-only access to i. That is not how typedef works. The const qualifies the type, and so the definition above is equivalent to:
int_ptr const i_ptr(&i);
That is equivalent to:
int * const i_ptr(&i);
In other words, i ptr contains the address of i throughout the lifetime of i ptr.
So we now had two places where const had to be on the right, and one place where putting it on the left caused confusion in the minds of some programmers. That led Dan to start a movement to make it idiomatic to place const to the right of the type being qualified. Changing habits takes a great deal of time, and many programmers are repeatedly exposed to the older ‘idiom’. In a way, it does not matter where you place const in the cases where you have a choice. However, I favor consistency, and so I always place it to the right of what I am qualifying. If you sometimes see it on the left, you will have to think carefully about exactly what is being qualified as const.
If you have a choice, I suggest that consistency should win the day, but it is not something worth fighting over.
Function-Style Versus Assignment-Style
Initialization
C programmers had no choice: initialization of variables in their definitions was always done by using an equality sign. For example:
struct ex{ int y; int z;
};
int i = 0;
ex x = {1, 0};
C++ inherited that mechanism and maintains it to this day, in order to maximize compatibility between C and C++ source code. However, C++ introduced this situation:
class mytype{ public:
mytype(double x, double y):x_(x), y_(y){ } private:
double x_; double y_;
};
316 |
CHAPTER 18 |
Now C++ needed syntax for calling the constructor. The chosen syntax was to ‘call’ the constructor with a function-like initializer:
mytype mt(3.0, 2.1);
So far, so good. However, it then seemed to make sense to allow initialization of fundamental types with a similar syntax. So int i(0); became a valid alternative to int i = 0;. Unfortunately, this was not pursued to the logical conclusion. For example, we cannot write ex x(1, 0); as an alternative to ex x = {1, 0};. It got worse. Add the default constructor
mytype( ):x_(0.0), y_(0.0){ }
and, surprisingly to most newcomers,
mytype mt( );
does not define mt to be a default instance of mytype. What it does is to declare mt to be a function with no parameter that returns an instance of mytype by value. You may soon get used to recognizing the empty brackets as the cause of a problem in your code, and quickly learn to write:
mytype mt;
However, there are other subtler cases where the compiler manages to parse what the programmer intends to be a definition of a variable as a declaration of a function.
Consider the development of this tiny program:
Version 1
#include <iostream> #include <ostream>
int main( ){ double d(3.3); int i(d); std::cout << i;
}
The program compiles and, when built, outputs 3, just as you expect.
Now your teacher comes along and points out that the compiler is giving you a warning for converting a double into an int (quite sensibly, because such a conversion can lose some data, and, more to the point, not all double values can be expressed as int values, even approximately.) You try to fix it with a simple function-style cast:
Version 2
#include <iostream> #include <ostream>
int main( ){ double d(3.3); int i(int(d)); std::cout << i;
}
SOMETHING OLD, SOMETHING NEW |
317 |
Now the compiler gives you this warning:
warning: the address of 'int i(int)' will always evaluate as 'true'
What is it on about? It is complaining about your output line, but that is not where the problem is. It has parsed the previous line as a declaration of a function i( ) with an int parameter and an int return. Your first instinct may be to think that the compiler is wrong, but you are mistaken. You have fallen foul of the redundant-parentheses rule, inherited from C. int i(int(d)) is the same as int i(int d) as far as the compiler is concerned. In turn, that is just the same as int i(int), because parameter names have no significance in the context of a function declaration.
Yes, we should fix code that compiles with warnings, because the warnings are sometimes more serious than we think. Note that the warning for Version 1 is not that serious in context, but the warning for Version 2 tells us that the compiler is not seeing what we intended to write.
There are many ways to correct Version 2. My preference is to use the new-style C++ casts – a static cast<int> in this case:
Version 3a
int main( ){ double d(3.3);
int i(static_cast<int>(d)); std::cout << i;
}
However, you can also get around the problem by adding another pair of parentheses around the whole argument:
Version 3b
int main( ){ double d(3);
int i((int(d))); std::cout << i;
}
That form may be more useful if this problem hits you when initializing an object by calling a constructor that has more than one parameter. Try this code to see that problem manifest:
#include <iostream> #include <ostream>
struct ex{
ex(int i, int j):i_(i), j_(j){ } int i_;
int j_;
};
int main( ){ double d(3.3);
ex i(int(d), int(d)); std::cout << i.i_;
}