5.1 Function Declarations

A function declaration tells the compiler about a function name and how to call the function. The actual body of the function can be defined separately (described later in this chapter). A function declaration has the following parts:

type name ( parameters ) cv-qualifiers except-spec ;

The parameters, cv-qualifiers, and except-spec are optional. The type is required, except for constructors, destructors, and type conversion operators. The name is the function name. (Each of these parts is described later in this chapter.) Example 5-1 shows a variety of function declarations.

Example 5-1. Declaring functions
// Function named "add", which returns type int, and takes two parameters, each

// of type int. The names a and b are optional.

int add(int a, int b); 



// Function named "print", which takes a const string reference, and does not 

// return anything. The function is expanded inline.

inline void print(const std::string& str) 

{

  std::cout << str;

}



// Function named "test", which takes two floating-point arguments and returns an

// enumeration. This function does not throw any exceptions.

enum fptest { less=-1, equal, greater };

fptest test(double, double) throw(  ); 



class demo {

public:

  // Member function named "value", which returns type int, takes no arguments,

  // and is const, which means it can be called for a const object.

  int value(  ) const; 

  // Function named "~demo", that is, a destructor, that is virtual. Constructors

  // and destructors do not have return types. Destructors do not take arguments.

  virtual ~demo(  ); 

  // Inline, overloaded, const type conversion operator

  operator bool(  ) const { return value(  ) != 0; } 

};

5.1.1 Return Type

The type in a function declaration is the function's return type. It is a series of type specifiers (see Chapter 2) with pointer and reference operators. You can mix any of the following function specifiers freely with the type specifiers, but the convention is to list function specifiers before other type specifiers:

explicit

Applies only to constructors. An explicit constructor cannot be used in an implicit type conversion. See Chapter 6 for details.

inline

Tells the compiler to expand the function body at the point of the function call. An inline function must be defined in every file in which it is used, and the definition must be identical in every file. If the function body contains a local static object, including string literals, every expansion of the function in every file refers to a common object.

A function definition in a class definition is an inline function definition, even without the use of the inline specifier.

figs/acorn.gif

The inline specifier is a hint to the compiler, and the compiler is free to ignore the hint. Most compilers impose a variety of restrictions on which functions can be expanded inline. The restrictions vary from one compiler to another. For example, most compilers cannot expand a recursive function.

Inline functions are most often used for extremely simple functions. For example, all standard containers have a member function empty, which returns true if the container is empty. Some containers might implement the function as the following inline function:

inline bool empty(  ) const { return size(  ) != 0; }
virtual

Applies only to nonstatic member functions. A virtual function's definition is bound to the function call at runtime instead of at compile time. See Chapter 6 for details.

If a function's return type is void, no value is returned to the caller. The function does not need a return statement. The form return; is permitted, or the return expression must have type void.

If the return type is anything other than void, every return statement must have an expression, the type of which can be implicitly converted to the function's return type. See Chapter 4 for more information about the return statement.

A function's return type cannot be an array or function type, but it can be a pointer or reference to an array or function.

In a function declaration (but not a definition), you can use the extern storage class specifier. Declarations and definitions can have linkage specifications, e.g., extern "C". See Chapter 2 for more information about storage class and linkage.

A friend specifier can be used to declare a friend function. See Chapter 6 for more information.

Note that the return type is not considered in overload resolution. See Section 5.3 later in this chapter for details.

5.1.2 Parameters

Function parameters are optional. If a function takes no parameters, you can leave the parentheses empty or use the keyword void. If a function requires parameters, the parameter list is comma-separated, in which each parameter is a simple declaration of the following form:

type-specifiers declarator = expr

expr is optional; if it is omitted, the = symbol is also omitted. The type-specifiers allow for the optional register and auto storage class specifiers and pointer and reference operators. See Chapter 2 for more information about declarators and specifiers.

In C, a function that takes no arguments requires the void keyword, but in C++, void is optional. Thus, void appears most often in headers that must be used in both C and C++ programs. For example:

#ifdef _  _cplusplus

  #define EXTERN extern "C"

#else

  #define EXTERN extern

#endif

EXTERN int get_version(void);

In other situations, you can use whichever style you prefer.

You can omit the parameter name from the declarator. In a function declaration, the name is important only to the human reader. In a function definition, a nameless parameter cannot be used in the function body. For example, suppose a graphics package defines a variety of shape classes, all deriving from a common base class, shape. Among the operations permitted on a shape is scale, which takes two arguments: the scaling amounts in the x and y directions. Also, suppose that the square shape (unlike rectangle) heeds only the x scale factor. The square::scale function might be written as:

void square::scale(double xscale, double)

{

  this->size *= xscale;

}

A parameter can have cv-qualifiers (const and volatile, as discussed in Chapter 2). The qualifiers have their usual meaning in the function body. The qualifiers and storage class specifier are not part of the function type and do not participate in overload resolution.

5.1.3 Default Arguments

A parameter can have a default argument (separated from the declarator by an equal sign). Only the right-most parameters can have default arguments. If any given parameter does not have a default argument, all parameters to its left cannot have default arguments. The default argument can be any expression. (If you want to use a comma operator, enclose the expression in parentheses.)

In a function call, arguments that have default values can be omitted, starting from the right. For each omitted argument, the default argument is substituted in the function call. Each default argument is implicitly converted to the parameter type, applying the same rules that govern initializers in declarations. The default argument expressions are evaluated every time the argument is needed in a function call. Names used in the default arguments are looked up at the point of declaration, not the point of use, as shown in Example 5-2.

Example 5-2. Declaring and using default arguments
#include <iostream>

#include <ostream>



namespace demo {

  int f(  )

  {

    return 20;

  }

}



int f(  )

{

  return 10;

}



// The default argument for y is always the global f(  ), even if a different f(  )

// is visible where func(  ) is called.

int func(int x, int y = f(  ))

{

  return x + y;

}



int main(  )

{

  using demo::f;

  std::cout << f(  ) << '\n';       // Prints 20

  std::cout << func(32) << '\n';    // Prints 42

}

Default arguments are cumulative in multiple declarations of the same function in the same scope. Later declarations can provide default arguments for additional parameters, in which case the declaration must omit the default arguments for parameters that already have default arguments, as shown in Example 5-3.

Example 5-3. Accumulating default arguments
void func(int x, int y);

void func(int x, int y = 10);

void func(int x = 20, int y);

void other(  )

{

  func(  ); // Same as func(20, 10)

}

Different scopes can have different default arguments. For example, the source file in which a function is defined might have different default arguments from those used in function declarations where the function is used. However, most of the time, different default arguments suggests programmer errors.

In a derived class, an overridden virtual function can have different default arguments than its counterpart in the base class. The default argument is chosen at compile time, based on the object's static type. Thus, the default arguments are typically those of the base class, even if the function actually called is from the derived class. To avoid confusion, it is best to avoid default arguments with virtual functions, or make sure they are the same for all overridden functions. (See Chapter 6 for more information about virtual functions.)

In a member function declaration, you cannot use a nonstatic data member as a default argument unless it is the member of a specific object. If you want to use the value of a data member as the default value for a parameter, use an overloaded function, as shown in Example 5-4. (See Section 5.3 for more on overloaded functions.)

Example 5-4. Default arguments in member functions
class example {

public:

  void func(int x, int y = data_); // Error



  // Achieve the desired effect with overloaded functions.

  void func(int x, int y);

  void func(int x)         { func(x, data_); }

private:

  int data_;

};

5.1.4 Variable Number of Arguments

The last parameter in a function declaration can be an ellipsis (...), which permits a variable number of arguments to be passed to the function. The comma that separates the next-to-last parameter from the ellipsis is optional. However, if portability with C is important, be sure to include the comma. (See <cstdarg> in Chapter 13 to learn how to access the additional arguments.) You can use an ellipsis as the sole parameter in a function, but there is no mechanism in standard C++ to access the arguments from the function body. Such a declaration might be used for an external function, however.

5.1.5 cv-qualifiers

Only nonstatic member functions (but not constructors or destructors) can have cv-qualifiers (const and volatile). They are optional, and if used in a member function declaration, apply to the implicit object parameter of the member function (this). You can use const, volatile, neither, or both in any order. Place cv-qualifiers after the closing parenthesis of the function parameters and before the exception specification. The qualifiers are part of the function type and participate in overload resolution, so you can have multiple functions with the same name and parameters, but with different qualifiers (but only if you do not also have a static member function of the same name and parameters; see Section 5.3 later in this chapter for details).

A pointer-to-member function and a function typedef can also have cv-qualifiers. Only a top-level typedef can have cv-qualifiers; you cannot declare a typedef that combines a function typedef and a qualifier.

cv-qualifiers are most often used to declare const member functions. These functions can be called for a const object. In general, member functions that do not change *this should be declared const. (See Chapter 6 for more information on how cv-qualifiers affect member functions.) Example 5-5 shows some simple uses of qualifiers.

Example 5-5. Using qualifiers with member functions
class point

{

public:

  point(int x, int y) : x_(x), y_(y) {}

  int x(  )      const   { return x_; }

  int y(  )      const   { return y_; }

  double abs(  ) const   { return sqrt(double(x(  ))*x(  ) + y(  )*y(  )); }

  void offset(const point& p) {

    // Cannot be const because offset(  ) modifies x_ and y_

    x_ += p.x(  );

    y_ += p.y(  );

  }

private:

  int x_, y_;

};

5.1.6 Exception Specifications

An exception specification tells the compiler which exceptions a function can throw. Exception specifications are optional in a function declaration and are rarely used. The syntax is:

throw ( type-list )

The type-list is optional. The exception specification follows the function header and cv-qualifiers. If present, it is a comma-separated list of type names. (See Chapter 2 for details about type names.) Each type name is an exception type that the function can throw. If the function throws an exception that is not listed in the exception specification, the unexpected function is called. If the function declaration does not have an exception specification, the function can throw any exception.

The default implementation of unexpected calls terminate to terminate the program. You can set your own unexpected handler, which must call terminate or throw an exception. If your handler throws an exception that is not listed in the function's exception specification, bad_exception is thrown. If bad_exception is not listed in the function's exception specification, terminate is called. In other words, if there is an exception specification, only exceptions of the listed types (or derived from one of the listed types) can be thrown from the function, or else the program terminates. See <exception> in Chapter 13 for details.

An overridden virtual function must have an exception specification that lists only types that are also listed in the base-class exception specifications. In particular, if the base-class function does not throw any exceptions, the derived class function must not throw any exceptions.

An exception specification most often marks functions that do not throw exceptions at all (throw( )). Example 5-6 shows various uses of exception specifications.

Example 5-6. Declaring exception specifications
class base {

public:

  virtual void f(  ) throw(  );

  virtual void g(  ); // Can throw anything

  virtual void h(  ) throw(std::string);

};



class derived : public base {

public:

  virtual void f(  ) throw(  );    // OK: same as base

  virtual void g(  ) throw(int);   // OK: subset of base

  virtual void h(  ) throw(int);   // Error: int not in base

};



class more : public derived {

public:

  virtual void f(  );              // Error: can throw anything

  virtual void g(  ) throw(  );    // OK

};



// Function does not throw any exceptions

int noproblem(int x, int y) throw(  )

try

{

  dostuff(x);

  dostuff(y);

  return 1;

}

catch(...)

{

  return 0;

}



derived* downcast(base* b) throw(std::bad_cast)

{

  return dynamic_cast<derived*>(b);

}

Java programmers should note two significant differences between C++ and Java with respect to exception specifications:

  • The exception specification is introduced by throw, not throws.

  • The correctness of the exception specification is checked at runtime, not at compile time.