course of C++ programming language

lecture 3: functions


Functions declarations and definitions

It's important to understand the difference between declarations and definitions. Essentially all C and C++ programs require declarations. Before you can write your first program, you need to understand the proper way to write a declaration.

A declaration introduces a name - an identifier - to the compiler. It tells the compiler: "this function or this variable exists somewhere, and here is what it should look like".

A definition, on the other hand, says: "make this variable here" or "make this function here". It allocates storage for the name. This meaning works whether you're talking about a variable or a function; in either case, at the point of definition the compiler allocates storage. For a variable, the compiler determines how big that variable is and causes space to be generated in memory to hold the data for that variable. For a function, the compiler generates code, which ends up occupying storage in memory.

You can declare a variable or a function in many different places, but there must be only one definition in program.

A definition can also be a declaration. If the compiler hasn't seen the name x before and you define int x;, the compiler sees the name as a declaration and allocates storage for it all at once.

A function declaration gives the name of the function, the type of the value returned (if any) by the function, and the number and types of the arguments that must be supplied in a call of the function.

Elem * next_elem (); char * strcpy (char *to, const char *from); void exit (int);

The semantics of argument passing are identical to the semantics of initialization. Argument types are checked and implicit argument type conversion takes place when necessary.

double sqrt (double); double sr2 = sqrt(2); // call sqrt() with the argument double(2) double sq3 = sqrt ("three"); // error: sqrt() requires an argument of type double

A function declaration may contain argument names. This can be a help to the reader of a program, but the compiler simply ignores such names. void as a return type means that the function does not return a value.

Every function that is called in a program must be defined somewhere (once only). A function definition is a function declaration in which the body of the function is presented.

extern void swap (int &, int &); // a declaration void swap (int &p, int &q) // a definition { int t = p ; p = q; q = t; }

The type of the definition and all declarations for a function must specify the same type. The argument names, however, are not part of the type and need not be identical.

Inline functions

A function can be defined to be inline.

inline int sum1n (int n) { return (n*(n+1))/2; }

The inline specifier is a hint to the compiler that it should attempt to generate code for a call of fac() inline rather than laying down the code for the function once and then calling through the usual function call mechanism.

C++ implements the macro as inline function, which is a true function in every sense. Any behavior you expect from an ordinary function, you get from an inline function. The only difference is that an inline function is expanded in place, like a preprocessor macro, so the overhead of the function call is eliminated. Thus, you should (almost) never use macros, only inline functions.

You'll almost always want to put inline definitions in a header file. An inline function in a header file has a special status, since you must include the header file containing the function and its definition in every file where the function is used, but you don't end up with multiple definition errors (however, the definition must be identical in all places where the inline function is included).

Static variables

A local variable is initialized when the thread of execution reaches its definition. By default, this happens in every call of the function and each invocation of the function has its own copy of the variable. If a local variable is declared static, a single, statically allocated object will be used to represent that variable in all calls of the function. It will be initialized only the first time the thread of execution reaches its definition.

int call_counter () { static int cnt = 0; return ++cnt; }

A static variable provides a function with "a memory" without introducing a global variable that might be accessed and corrupted by other functions.

Argument passing

When a function is called, store is set aside for its formal arguments and each formal argument is initialized by its corresponding actual argument. The semantics of argument passing are identical to the semantics of initialization. In particular, the type of an actual argument is checked against the type of the corresponding formal argument, and all standard and userdefined type conversions are performed.

void f (int val, int &ref) { val++; ref++; }

When f() is called, val++ increments a local copy of the first actual argument, whereas ref++ increments the second actual argument. The first argument is passed by value, the second argument is passed by reference. Functions that modify call-by-reference arguments can make programs hard to read and should most often be avoided. It can, however, be noticeably more efficient to pass a large object by reference than to pass it by value. In that case, the argument might be declared const to indicate that the reference is used for efficiency reasons only and not to enable the called function to change the value of the object.

void f (const Large &arg); // the value of arg cannot be changed without explicit use of type conversion

The absence of const in the declaration of a reference argument is taken as a statement of intent to modify the variable.

void g (Large &arg); // assume that g() modifies arg

Similarly, declaring a pointer argument const tells readers that the value of an object pointed to by that argument is not changed by the function.

int strlen (const char*); int strcmp (const char*, const char*);
Value return

A value must be returned from a function that is not declared void. Conversely, a value cannot be returned from a void function. A return value is specified by a return statement.

int fac (int n) { return n>1 ? n*fac(n-1) : 1; }

A function that calls itself is said to be recursive.

Like the semantics of argument passing, the semantics of function value return are identical to the semantics of initialization. A return statement is considered to initialize an unnamed variable of the returned type. The type of a return expression is checked against the type of the returned type, and all standard and userdefined type conversions are performed.

Overloaded function names

Most often, it is a good idea to give different functions different names, but when some functions conceptually perform the same task on objects of different types, it can be more convenient to give 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 one name for addition, +, yet it can be used to add values of integer, floatingpoint, and pointer types. This idea is easily extended to functions defined by the programmer.

void print (int); // print an int void print (const char*); // print a C-style character string

When a function f() is called, the compiler must figure out which of the functions with the name f is to be invoked. This is done by comparing the types of the actual arguments with the types of the formal arguments of all functions called f. The idea is to invoke the function that is the best match on the arguments and give a compiletime error if no function is the best match.

void print (double); void print (long); void f () { print(1L); // print(long) print(1.0) ; // print(double) print(1); // error, ambiguous: print(long(1)) or print(double(1))? }

Finding the right version to call from a set of overloaded functions is done by looking for a best match between the type of the argument expression and the parameters (formal arguments) of the functions. To 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 name to 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), float to double, and double to long double.
  3. Match using standard conversions (for example, int to double, double to int, Derived* to Base*, T* to void*, int to unsigned int).
  4. Match using user-defined conversions.
  5. Match using the ellipsis ... in a function declaration.

If two matches are found at the highest level where a match is found, the call is rejected as ambiguous.

Return types are not considered in overload resolution.

Default arguments

A general function often needs more arguments than are necessary to handle simple cases. C++ provides a remedy with default arguments. A default argument is a value given in the declaration that the compiler automatically inserts if you don't provide a value in the function call.

Default arguments are a convenience, as function overloading is a convenience. Both features allow you to use a single function name in different situations. The difference is that with default arguments the compiler is substituting arguments when you don't want to put them in yourself. If the functions have very different behaviors, it doesn't usually make sense to use default arguments.

There are two rules you must be aware of when using default arguments. First, only trailing arguments may be defaulted. That is, you can't have a default argument followed by a non-default argument. Second, once you start using default arguments in a particular function call, all the subsequent arguments in that function's argument list must be defaulted (this follows from the first rule).

Default arguments are only placed in the declaration of a function (typically placed in a header file). The compiler must see the default value before it can use it.

void fn (int x=0);

Sometimes people will place the commented values of the default arguments in the function definition, for documentation purposes.

void fn (int x/*=0*/) { /*...*/ }

References
  1. B.Stroustrup: The C++ programming language. Third edition.
    Section 7: functions; pp. 143-164.
  2. Bruce Eckel: Thinking in C++. Second edition.
    Section 7: function overloading and default arguments; pp. 309-332.
    Section 9: inline functions; pp. 371-404.