Team LiB
Previous Section Next Section

6.5. Features for Specialized Uses

 

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.

 

6.5.1. Default Arguments

 

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.

 
Calling Functions with 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.

 
Default Argument Declarations
 

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

 

Image Best Practices

Default arguments ordinarily should be specified with the function declaration in an appropriate header.

 

 
Default Argument Initializers
 

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_plural6.3.2, p. 224) a default argument of 's'. Test your program by printing singular and plural versions of the words success and failure.

 

 

6.5.2. Inline and constexpr Functions

 

In § 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 Overhead
 

A 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;
}

 

Image Note

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 Functions
 
Image

A 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.

 

Image Note

A constexpr function is not required to return a constant expression.

 

 
Put inline and constexpr Functions in Header Files
 

Unlike 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 be inline.

 

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 be inline.

Exercise 6.46: Would it be possible to define isShorter as a constexpr? If so, do so. If not, explain why not.


 

6.5.3. Aids for Debugging

 

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.

 
The assert Preprocessor Macro
 

assert 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);

 
The NDEBUG Preprocessor Variable
 

The 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

 
Team LiB
Previous Section Next Section