B. Stroustrup - The C++ Programming Language (794319), страница 101
Текст из файла (страница 101)
These functions are marked constto indicate that they don’t modify the state of the object/variable for which they arecalled.[3] A set of functions allowing the user to modify Dates without actually having to know thedetails of the representation or fiddle with the intricacies of the semantics.[4] Implicitly defined operations that allow Dates to be freely copied (§16.2.2).[5] A class, Bad_date, to be used for reporting errors as exceptions.[6] A set of useful helper functions.
The helper functions are not members and have nodirect access to the representation of a Date, but they are identified as related by the use ofthe namespace Chrono.I defined a Month type to cope with the problem of remembering the month/day order, for example,to avoid confusion about whether the 7th of June is written {6,7} (American style) or {7,6} (European style).I considered introducing separate types Day and Year to cope with possible confusion ofDate{1995,Month::jul,27} and Date{27,Month::jul,1995}.
However, these types would not be as usefulas the Month type. Almost all such errors are caught at run time anyway – the 26th of July year 27472ClassesChapter 16is not a common date in my work. Dealing with historical dates before year 1800 or so is a trickyissue best left to expert historians. Furthermore, the day of the month can’t be properly checked inisolation from its month and year.To save the user from having to explicitly mention year and month even when they are impliedby context, I added a mechanism for providing a default.
Note that for Month the {} gives the(default) value 0 just as for integers even though it is not a valid Month (§8.4). However, in thiscase, that’s exactly what we want: an otherwise illegal value to represent ‘‘pick the default.’’ Providing a default (e.g., a default value for Date objects) is a tricky design problem. For some types,there is a conventional default (e.g., 0 for integers); for others, no default makes sense; and finally,there are some types (such as Date) where the question of whether to provide a default is nontrivial.In such cases, it is best – at least initially – not to provide a default value. I provide one for Dateprimarily to be able to discuss how to do so.I omitted the cache technique from §16.2.9 as unnecessary for a type this simple. If needed, itcan be added as an implementation detail without affecting the user interface.Here is a small – and contrived – example of how Dates can be used:void f(Date& d){Date lvb_day {16,Month::dec,d.year()};if (d.day()==29 && d.month()==Month::feb) {// ...}if (midnight()) d.add_day(1);cout << "day after:" << d+1 << '\n';Date dd; // initialized to the default datecin>>dd;if (dd==d) cout << "Hurray!\n";}This assumes that the addition operator, +, has been declared for Dates.
I do that in §16.3.3.Note the use of explicit qualification of dec and feb by Month. I used an enum class (§8.4.1)specifically to be able to use short names for the months, yet also ensure that their use would not beobscure or ambiguous.Why is it worthwhile to define a specific type for something as simple as a date? After all, wecould just define a simple data structure:struct Date {int day, month, year;};Each programmer could then decide what to do with it. If we did that, though, every user wouldeither have to manipulate the components of Dates directly or provide separate functions for doingso.
In effect, the notion of a date would be scattered throughout the system, which would make ithard to understand, document, or change. Inevitably, providing a concept as only a simple structureSection 16.3Concrete Classes473causes extra work for every user of the structure.Also, even though the Date type seems simple, it takes some thought to get right. For example,incrementing a Date must deal with leap years, with the fact that months are of different lengths,and so on. Also, the day-month-and-year representation is rather poor for many applications.
If wedecided to change it, we would need to modify only a designated set of functions. For example, torepresent a Date as the number of days before or after January 1, 1970, we would need to changeonly Date’s member functions.To simplify, I decided to eliminate the notion of changing the default date. Doing so eliminatessome opportunities for confusion and the likelihood of race conditions in a multi-threaded program(§5.3.1). I seriously considered eliminating the notion of a default date altogether. That wouldhave forced users to consistently explicitly initialize their Dates. However, that can be inconvenientand surprising, and more importantly common interfaces used for generic code require default construction (§17.3.3). That means that I, as the designer of Date, have to pick the default date.
Ichose January 1, 1970, because that is the starting point for the C and C++ standard-library timeroutines (§35.2, §43.6). Obviously, eliminating set_default_date() caused some loss of generality ofDate. However, design – including class design – is about making decisions, rather than just deciding to postpone them or to leave all options open for users.To preserve an opportunity for future refinement, I declared default_date() as a helper function:const Date& Chrono::default_date();That doesn’t say anything about how the default date is actually set.16.3.1 Member FunctionsNaturally, an implementation for each member function must be provided somewhere. For example:Date::Date(int dd, Month mm, int yy):d{dd}, m{mm}, y{yy}{if (y == 0) y = default_date().year();if (m == Month{}) m = default_date().month();if (d == 0) d = default_date().day();if (!is_valid()) throw Bad_date();}The constructor checks that the data supplied denotes a valid Date.
If not, say, for{30,Month::feb,1994}, it throws an exception (§2.4.3.1, Chapter 13), which indicates that somethingwent wrong. If the data supplied is acceptable, the obvious initialization is done. Initialization is arelatively complicated operation because it involves data validation. This is fairly typical. On theother hand, once a Date has been created, it can be used and copied without further checking.
Inother words, the constructor establishes the invariant for the class (in this case, that it denotes avalid date). Other member functions can rely on that invariant and must maintain it. This designtechnique can simplify code immensely (see §2.4.3.2, §13.4).I’m using the value Month{} – which doesn’t represent a month and has the integer value 0 – torepresent ‘‘pick the default month.’’ I could have defined an enumerator in Month specifically to474ClassesChapter 16represent that.
But I decided that it was better to use an obviously anomalous value to represent‘‘pick the default month’’ rather than give the appearance that there were 13 months in a year. Notethat Month{}, meaning 0, can be used because it is within the range guaranteed for the enumerationMonth (§8.4).I use the member initializer syntax (§17.4) to initialize the members. After that, I check for 0and modify the values as needed. This clearly does not provide optimal performance in the (hopefully rare) case of an error, but the use of member initializers leaves the structure of the code obvious. This makes the style less error-prone and easier to maintain than alternatives.
Had I aimed atoptimal performance, I would have used three separate constructors rather than a single constructorwith default arguments.I considered making the validation function is_valid() public. However, I found the resultinguser code more complicated and less robust than code relying on catching the exception:void fill(vector<Date>& aa){while (cin) {Date d;try {cin >> d;}catch (Date::Bad_date) {// ... my error handling ...continue;}aa.push_back(d); // see §4.4.2}}However, checking that a {d,m,y} set of values is a valid date is not a computation that depends onthe representation of a Date, so I implemented is_valid() in terms of a helper function:bool Date::is_valid(){return is_date(d,m,y);}Why have both is_valid() and is_date()? In this simple example, we could manage with just one, butI can imagine systems where is_date() (as here) checks that a (d,m,y)-tuple represents a valid dateand where is_valid() does an additional check on whether that date can be reasonably represented.For example, is_valid() might reject dates from before the modern calendar became commonly used.As is common for such simple concrete types, the definitions of Date’s member functions varybetween the trivial and the not-too-complicated.
For example:inline int Date::day() const{return d;}Section 16.3.1Member Functions475Date& Date::add_month(int n){if (n==0) return ∗this;if (n>0) {int delta_y = n/12;int mm = static_cast<int>(m)+n%12;if (12 < mm) {++delta_y;mm −= 12;}// number of whole years// number of months ahead// note: dec is represented by 12// ... handle the cases where the month mm doesn’t have day d ...y += delta_y;m = static_cast<Month>(mm);return ∗this;}// ... handle negative n ...return ∗this;}I wouldn’t call the code for add_month() pretty. In fact, if I added all the details, it might evenapproach the complexity of relatively simple real-world code.