In this section we’ll cover three function-related features that are useful in many, but not all, programs: default arguments, inline and constexpr
functions, and some facilities that are often used during debugging.
Some functions have parameters that are given a particular value in most, but not all, calls. In such cases, we can declare that common value as a default argument for the function. Functions with default arguments can be called with or without that argument.
For example, we might use a string
to represent the contents of a window. By default, we might want the window to have a particular height, width, and background character. However, we might also want to allow users to pass values other than the defaults. To accommodate both default and specified values we would declare our function to define the window as follows:
typedef string::size_type sz; // typedef see § 2.5.1 (p. 67)
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
Here we’ve provided a default for each parameter. A default argument is specified as an initializer for a parameter in the parameter list. We may define defaults for one or more parameters. However, if a parameter has a default argument, all the parameters that follow it must also have default arguments.
If we want to use the default argument, we omit that argument when we call the function. Because screen
provides defaults for all of its parameters, we can call screen
with zero, one, two, or three arguments:
string window;
window = screen(); // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ')
window = screen(66, 256); // screen(66,256,' ')
window = screen(66, 256, '#'); // screen(66,256,'#')
Arguments in the call are resolved by position. The default arguments are used for the trailing (right-most) arguments of a call. For example, to override the default for background
, we must also supply arguments for height
and width
:
window = screen(, , '?'); // error: can omit only trailing arguments
window = screen('?'); // calls screen('?',80,' ')
Note that the second call, which passes a single character value, is legal. Although legal, it is unlikely to be what was intended. The call is legal because '?'
is a char
, and a char
can be converted (§ 4.11.1, p. 160) to the type of the left-most parameter. That parameter is string::size_type
, which is an unsigned
integral type. In this call, the char
argument is implicitly converted to string::size_type
, and is passed as the argument to height
. On our machine, '?'
has the hexadecimal value 0x3F
, which is decimal 63
. Thus, this call passes 63
to the height
parameter.
Part of the work of designing a function with default arguments is ordering the parameters so that those least likely to use a default value appear first and those most likely to use a default appear last.
Although it is normal practice to declare a function once inside a header, it is legal to redeclare a function multiple times. However, each parameter can have its default specified only once in a given scope. Thus, any subsequent declaration can add a default only for a parameter that has not previously had a default specified. As usual, defaults can be specified only if all parameters to the right already have defaults. For example, given
// no default for the height or width parameters
string screen(sz, sz, char = ' ');
we cannot change an already declared default value:
string screen(sz, sz, char = '*'); // error: redeclaration
but we can add a default argument as follows:
string screen(sz = 24, sz = 80, char); // ok: adds default arguments
Default arguments ordinarily should be specified with the function declaration in an appropriate header.
Local variables may not be used as a default argument. Excepting that restriction, a default argument can be any expression that has a type that is convertible to the type of the parameter:
// the declarations of wd, def, and ht must appear outside a function
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // calls screen(ht(), 80, ' ')
Names used as default arguments are resolved in the scope of the function declaration. The value that those names represent is evaluated at the time of the call:
void f2()
{
def = '*'; // changes the value of a default argument
sz wd = 100; // hides the outer definition of wd but does not change the default
window = screen(); // calls screen(ht(), 80, '*')
}
Inside f2
, we changed the value of def
. The call to screen
passes this updated value. Our function also declared a local variable that hides the outer wd
. However, the local named wd
is unrelated to the default argument passed to screen
.
Exercises Section 6.5.1
Exercise 6.40: Which, if either, of the following declarations are errors? Why?
(a)
int ff(int a, int b = 0, int c = 0);
(b)
char *init(int ht = 24, int wd, char bckgrnd);
Exercise 6.41: Which, if any, of the following calls are illegal? Why? Which, if any, are legal but unlikely to match the programmer’s intent? Why?
char *init(int ht, int wd = 80, char bckgrnd = ' ');
(a)
init();
(b)
init(24,10);
(c)
init(14, '*');
Exercise 6.42: Give the second parameter of
make_plural
(§ 6.3.2, p. 224) a default argument of's'
. Test your program by printing singular and plural versions of the wordssuccess
andfailure
.
constexpr
FunctionsIn § 6.3.2 (p. 224) we wrote a small function that returned a reference to the shorter of its two string
parameters. The benefits of defining a function for such a small operation include the following:
• It is easier to read and understand a call to
shorterString
than it would be to read and understand the equivalent conditional expression.
• Using a function ensures uniform behavior. Each test is guaranteed to be done the same way.
• If we need to change the computation, it is easier to change the function than to find and change every occurrence of the equivalent expression.
• The function can be reused rather than rewritten for other applications.
There is, however, one potential drawback to making shorterString
a function: Calling a function is apt to be slower than evaluating the equivalent expression. On most machines, a function call does a lot of work: Registers are saved before the call and restored after the return; arguments may be copied; and the program branches to a new location.
inline
Functions Avoid Function Call OverheadA function specified as inline
(usually) is expanded “in line” at each call. If shorterString
were defined as inline
, then this call
cout << shorterString(s1, s2) << endl;
(probably) would be expanded during compilation into something like
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
The run-time overhead of making shorterString
a function is thus removed.
We can define shorterString
as an inline function by putting the keyword inline
before the function’s return type:
// inline version: find the shorter of two strings
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
The
inline
specification is only a request to the compiler. The compiler may choose to ignore this request.
In general, the inline
mechanism is meant to optimize small, straight-line functions that are called frequently. Many compilers will not inline a recursive function. A 75-line function will almost surely not be expanded inline.
constexpr
FunctionsA constexpr
function is a function that can be used in a constant expression (§ 2.4.4, p. 65). A constexpr
function is defined like any other function but must meet certain restrictions: The return
type and the type of each parameter in a must be a literal type (§ 2.4.4, p. 66), and the function body must contain exactly one return
statement:
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // ok: foo is a constant expression
Here we defined new_sz
as a constexpr
that takes no arguments. The compiler can verify—at compile time—that a call to new_sz
returns a constant expression, so we can use new_sz
to initialize our constexpr
variable, foo
.
When it can do so, the compiler will replace a call to a constexpr
function with its resulting value. In order to be able to expand the function immediately, constexpr
functions are implicitly inline
.
A constexpr
function body may contain other statements so long as those statements generate no actions at run time. For example, a constexpr
function may contain null statements, type aliases (§ 2.5.1, p. 67), and using
declarations.
A constexpr
function is permitted to return a value that is not a constant:
// scale(arg) is a constant expression if arg is a constant expression
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
The scale
function will return a constant expression if its argument is a constant expression but not otherwise:
int arr[scale(2)]; // ok: scale(2) is a constant expression
int i = 2; // i is not a constant expression
int a2[scale(i)]; // error: scale(i) is not a constant expression
When we pass a constant expression—such as the literal 2
—then the return is a constant expression. In this case, the compiler will replace the call to scale
with the resulting value.
If we call scale
with an expression that is not a constant expression—such as on the int
object i
—then the return is not a constant expression. If we use scale
in a context that requires a constant expression, the compiler checks that the result is a constant expression. If it is not, the compiler will produce an error message.
inline
and constexpr
Functions in Header FilesUnlike other functions, inline
and constexpr
functions may be defined multiple times in the program. After all, the compiler needs the definition, not just the declaration, in order to expand the code. However, all of the definitions of a given inline
or constexpr
must match exactly. As a result, inline
and constexpr
functions normally are defined in headers.
Exercises Section 6.5.2
Exercise 6.43: Which one of the following declarations and definitions would you put in a header? In a source file? Explain why.
(a)
inline bool eq(const BigInt&, const BigInt&) {...}
(b)
void putValues(int *arr, int size);
Exercise 6.44: Rewrite the
isShorter
function from § 6.2.2 (p. 211) to beinline
.Exercise 6.45: Review the programs you’ve written for the earlier exercises and decide whether they should be defined as
inline
. If so, do so. If not, explain why they should not beinline
.Exercise 6.46: Would it be possible to define
isShorter
as aconstexpr
? If so, do so. If not, explain why not.
C++ programmers sometimes use a technique similar to header guards (§ 2.6.3, p. 77) to conditionally execute debugging code. The idea is that the program will contain debugging code that is executed only while the program is being developed. When the application is completed and ready to ship, the debugging code is turned off. This approach uses two preprocessor facilities: assert
and NDEBUG
.
assert
Preprocessor Macroassert
is a preprocessor macro. A preprocessor macro is a preprocessor variable that acts somewhat like an inline function. The assert
macro takes a single expression, which it uses as a condition:
assert(expr);
evaluates expr and if the expression is false (i.e., zero), then assert
writes a message and terminates the program. If the expression is true (i.e., is nonzero), then assert
does nothing.
The assert
macro is defined in the cassert
header. As we’ve seen, preprocessor names are managed by the preprocessor not the compiler (§ 2.3.2, p. 54). As a result, we use preprocessor names directly and do not provide a using
declaration for them. That is, we refer to assert
, not std::assert
, and provide no using
declaration for assert
.
As with preprocessor variables, macro names must be unique within the program. Programs that include the cassert
header may not define a variable, function, or other entity named assert
. In practice, it is a good idea to avoid using the name assert
for our own purposes even if we don’t include cassert
. Many headers include the cassert
header, which means that even if you don’t directly include that file, your programs are likely to have it included anyway.
The assert
macro is often used to check for conditions that “cannot happen.” For example, a program that does some manipulation of input text might know that all words it is given are always longer than a threshold. That program might contain a statement such as
assert(word.size() > threshold);
NDEBUG
Preprocessor VariableThe behavior of assert
depends on the status of a preprocessor variable named NDEBUG
. If NDEBUG
is defined, assert
does nothing. By default, NDEBUG
is not defined, so, by default, assert
performs a run-time check.
We can “turn off” debugging by providing a #define
to define NDEBUG
. Alternatively, most compilers provide a command-line option that lets us define preprocessor variables:
$ CC -D NDEBUG main.C # use /D with the Microsoft compiler
has the same effect as writing #define NDEBUG
at the beginning of main.C
.
If NDEBUG
is defined, we avoid the potential run-time overhead involved in checking various conditions. Of course, there is also no run-time check. Therefore, assert
should be used only to verify things that truly should not be possible. It can be useful as an aid in getting a program debugged but should not be used to substitute for run-time logic checks or error checking that the program should do.
In addition to using assert
, we can write our own conditional debugging code using NDEBUG
. If NDEBUG
is not defined, the code between the #ifndef
and the #endif
is executed. If NDEBUG
is defined, that code is ignored:
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
// _ _func_ _ is a local static defined by the compiler that holds the function's name
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
// ...
Here we use a variable named _ _func_ _
to print the name of the function we are debugging. The compiler defines _ _func_ _
in every function. It is a local static
array of const char
that holds the name of the function.
In addition to _ _func_ _
, which the C++ compiler defines, the preprocessor defines four other names that can be useful in debugging:
_ _FILE_ _
string literal containing the name of the file
_ _LINE_ _
integer literal containing the current line number
_ _TIME_ _
string literal containing the time the file was compiled
_ _DATE_ _
string literal containing the date the file was compiled
We might use these constants to report additional information in error messages:
if (word.size() < threshold)
cerr << "Error: " << _ _FILE_ _
<< " : in function " << _ _func_ _
<< " at line " << _ _LINE_ _ << endl
<< " Compiled on " << _ _DATE_ _
<< " at " << _ _TIME_ _ << endl
<< " Word read was \"" << word
<< "\": Length too short" << endl;
If we give this program a string
that is shorter than the threshold
, then the following error message will be generated:
Error: wdebug.cc : in function main at line 27
Compiled on Jul 11 2012 at 20:50:03
Word read was "foo": Length too short