B. Stroustrup - The C++ Programming Language (794319), страница 73
Текст из файла (страница 73)
Consider writing an error function that takes one integer argument indicating the severity of the error followed by an arbitrary number of strings. The idea is to compose the error message by passing each word as a separate C-style string argument. The list of string argumentsshould be terminated by the null pointer:extern void error(int ...);extern char∗ itoa(int, char[]); // int to alphaint main(int argc, char∗ argv[]){switch (argc) {case 1:error(0,argv[0],nullptr);break;case 2:error(0,argv[0],argv[1],nullptr);break;default:char buffer[8];error(1,argv[0],"with",itoa(argc−1,buffer),"arguments",nullptr);}// ...}The function itoa() returns a C-style string representing its int argument.
It is popular in C, but notpart of the C standard.I always pass argv[0] because that, conventionally, is the name of the program.Note that using the integer 0 as the terminator would not have been portable: on some implementations, the integer 0 and the null pointer do not have the same representation (§6.2.8). Thisillustrates the subtleties and extra work that face the programmer once type checking has been suppressed using the ellipsis.Section 12.2.4Unspecified Number of Arguments323The error() function could be defined like this:#include <cstdarg>void error(int severity ...) // ‘‘severity’’ followed by a zero-terminated list of char*s{va_list ap;va_start(ap,severity);// arg startupfor (;;) {char∗ p = va_arg(ap,char∗);if (p == nullptr) break;cerr << p << ' ';}va_end(ap);// arg cleanupcerr << '\n';if (severity) exit(severity);}First, a va_list is defined and initialized by a call of va_start(). The macro va_start takes the name ofthe va_list and the name of the last formal argument as arguments.
The macro va_arg() is used topick the unnamed arguments in order. In each call, the programmer must supply a type; va_arg()assumes that an actual argument of that type has been passed, but it typically has no way of ensuring that. Before returning from a function in which va_start() has been used, va_end() must becalled. The reason is that va_start() may modify the stack in such a way that a return cannot successfully be done; va_end() undoes any such modifications.Alternatively, error() could have been defined using a standard-library initializer_list:void error(int severity, initializer_list<string> err){for (auto& s : err)cerr << s << ' ';cerr << '\n';if (severity) exit(severity);}It would then have to be called using the list notation. For example:switch (argc) {case 1:error(0,{argv[0]});break;case 2:error(0,{argv[0],argv[1]});break;default:error(1,{argv[0],"with",to_string(argc−1),"arguments"});}324FunctionsChapter 12The int-to-string conversion function to_string() is provided by the standard library (§36.3.5).If I didn’t have to mimic C style, I would further simplify the code by passing a container as asingle argument:void error(int severity, const vector<string>& err) // almost as before{for (auto& s : err)cerr << s << ' ';cerr << '\n';if (severity) exit(severity);}vector<string> arguments(int argc, char∗ argv[]) // package arguments{vector<string> res;for (int i = 0; i!=argc; ++i)res.push_back(argv[i]);return res}int main(int argc, char∗ argv[]){auto args = arguments(argc,argv);error((args.size()<2)?0:1,args);// ...}The helper function, arguments(), is trivial, and main() and error() are simple.
The interface betweenmain() and error() is more general in that it now passes all arguments. That would allow laterimprovements of error(). The use of the vector<string> is far less error-prone than any use of anunspecified number of arguments.12.2.5 Default ArgumentsA general function often needs more arguments than are necessary to handle simple cases.
In particular, functions that construct objects (§16.2.5) often provide several options for flexibility. Consider class complex from §3.2.1.1:class complex {double re, im;public:complex(double r, double i) :re{r}, im{i} {}complex(double r) :re{r}, im{0} {}complex() :re{0}, im{0} {}// default complex: {0,0}// ...};// construct complex from two scalars// construct complex from one scalarThe actions of complex’s constructors are quite trivial, but logically there is something odd abouthaving three functions (here, constructors) doing essentially the same task.
Also, for many classes,Section 12.2.5Default Arguments325constructors do more work and the repetitiveness is common. We could deal with the repetitivenessby considering one of the constructors ‘‘the real one’’ and forward to that (§17.4.3):complex(double r, double i) :re{r}, im{i} {}complex(double r) :complex{2,0} {}complex() :complex{0,0} {}// construct complex from two scalars// construct complex from one scalar// default complex: {0,0}Say we wanted to add some debugging, tracing, or statistics-gathering code tohave a single place to do so. However, this can be abbreviated further:complex(double r ={}, double i ={}) :re{r}, im{i} {}complex;we now// construct complex from two scalarsThis makes it clear that if a user supplies fewer than the two arguments needed, the default is used.The intent of having a single constructor plus some shorthand notation is now explicit.A default argument is type checked at the time of the function declaration and evaluated at thetime of the call.
For example:class X {public:static int def_arg;void f(int =def_arg);// ...};int X::def_arg = 7;void g(X& a){a.f();// maybe f(7)a.def_arg = 9;a.f();// f(9)}Default arguments that can change value are most often best avoided because they introduce subtlecontext dependencies.Default arguments may be provided for trailing arguments only.
For example:int f(int, int =0, char∗ =nullptr);// OKint g(int =0, int =0, char∗);// errorint h(int =0, int, char∗ =nullptr);// errorNote that the space between the ∗ and the = is significant (∗= is an assignment operator; §10.3):int nasty(char∗=nullptr);// syntax errorA default argument cannot be repeated or changed in a subsequent declaration in the same scope.For example:void f(int x = 7);void f(int = 7);void f(int = 8);// error: cannot repeat default argument// error: different default arguments326FunctionsChapter 12void g(){void f(int x = 9);// ...}// OK: this declaration hides the outer oneDeclaring a name in a nested scope so that the name hides a declaration of the same name in anouter scope is error-prone.12.3 Overloaded FunctionsMost often, it is a good idea to give different functions different names, but when different functions conceptually perform the same task on objects of different types, it can be more convenient togive them the same name. Using the same name for operations on different types is called overloading.
The technique is already used for the basic operations in C++. That is, there is only onename for addition, +, yet it can be used to add values of integer and floating-point types and combinations of such types. This idea is easily extended to functions defined by the programmer. Forexample:void print(int);// print an intvoid print(const char∗); // print a C-style stringAs far as the compiler is concerned, the only thing functions of the same name have in common isthat name. Presumably, the functions are in some sense similar, but the language does not constrainor aid the programmer.
Thus, overloaded function names are primarily a notational convenience.This convenience is significant for functions with conventional names such as sqrt, print, and open.When a name is semantically significant, this convenience becomes essential. This happens, forexample, with operators such as +, ∗, and <<, in the case of constructors (§16.2.5, §17.1), and ingeneric programming (§4.5, Chapter 32).Templates provide a systematic way of defining sets of overloaded functions (§23.5).12.3.1 Automatic Overload ResolutionWhen a function fct is called, the compiler must determine which of the functions named fct toinvoke.
This is done by comparing the types of the actual arguments with the types of the parameters of all functions in scope called fct. The idea is to invoke the function that is the best match tothe arguments and give a compile-time error if no function is the best match. For example:void print(double);void print(long);void f(){print(1L);print(1.0);print(1);}// print(long)// print(double)// error, ambiguous: print(long(1)) or print(double(1))?Section 12.3.1Automatic Overload Resolution327To approximate our notions of what is reasonable, a series of criteria are tried in order:[1] Exact match; that is, match using no or only trivial conversions (for example, array nameto pointer, function name to pointer to function, and T to const T)[2] Match using promotions; that is, integral promotions (bool to int, char to int, short to int,and their unsigned counterparts; §10.5.1) and float to double[3] Match using standard conversions (e.g., int to double, double to int, double to long double,Derived∗ to Base∗ (§20.2), T∗ to void∗ (§7.2.1), int to unsigned int (§10.5))[4] Match using user-defined conversions (e.g., double to complex<double>; §18.4)[5] Match using the ellipsis ...
in a function declaration (§12.2.4)If two matches are found at the highest level where a match is found, the call is rejected as ambiguous. The resolution rules are this elaborate primarily to take into account the elaborate C and C++rules for built-in numeric types (§10.5). For example:void print(int);void print(const char∗);void print(double);void print(long);void print(char);void h(char c, int i, short s, float f){print(c);// exact match: invoke print(char)print(i);// exact match: invoke print(int)print(s);// integral promotion: invoke print(int)print(f);// float to double promotion: print(double)print('a');print(49);print(0);print("a");print(nullptr);// exact match: invoke print(char)// exact match: invoke print(int)// exact match: invoke print(int)// exact match: invoke print(const char*)// nullptr_t to const char* promotion: invoke print(cost char*)}The call print(0) invokes print(int) because 0 is an int.
The call print('a') invokes print(char) because 'a'is a char (§6.2.3.2). The reason to distinguish between conversions and promotions is that we wantto prefer safe promotions, such as char to int, over unsafe conversions, such as int to char. See also§12.3.5.Overload resolution is independent of the order of declaration of the functions considered.Function templates are handled by applying the overload resolution rules to the result of specialization based on a set of arguments (§23.5.3).