Revisiting function basics
A function identifier must begin with a letter or underscore and may also contain digits. The function’s return type, argument list, and return value are optional. The basic form of a C++ function is as follows:
<return type> FunctionName (<argumentType argument1, …>) { expression 1…N; <return value/expression;> }
Let’s review a simple function:
#include <iostream> using namespace std; // we'll limit the namespace shortly int Minimum(int a, int b) { if (a < b) return a; else return b; } int main() { int x = 0, y = 0; cout << "Enter two integers: "; cin >> x >> y; cout << "The minimum is: " << Minimum(x, y) << endl; return 0; }
In the preceding simple example, first, a function Minimum()
is defined. It has a return type of int
and it takes two integer arguments: formal parameters a
and b
. In the main()
function, Minimum()
is called with actual parameters x
and y
. The call to Minimum()
is permitted within the cout
statement because Minimum()
returns an integer value; this value is passed along to the extraction operator (<<
) in conjunction with printing. In fact, the string "The minimum is: "
is first placed into the buffer associated with cout
, followed by the return value from calling function Minimum()
. The output buffer is then flushed by endl
(which first places a newline character in the buffer before flushing).
Notice that the function is first defined in the file and then called later in the file in the main()
function. Strong type checking is performed on the call to the function by comparing the parameter types and their usage in the call to the function’s definition. What happens, however, when the function call precedes its definition? Or if the call to the function is in a separate file from its definition?
In these cases, the default action is for the compiler to assume a certain signature to the function, such as an integer return type, and that the formal parameters will match the types of arguments in the function call. Often, the default assumptions are incorrect; when the compiler then encounters the function definition later in the file (or when another file is linked in), an error will be raised indicating that the function call and definition do not match.
These issues have historically been solved with a forward declaration of a function included at the top of a file where the function will be called. Forward declarations consist of the function return type, function name and types, and the number of parameters. In C++, a forward declaration has been improved upon and is instead known as a function prototype. Since there are many interesting details surrounding function prototyping, this topic will be covered in reasonable detail in the next chapter.
Important note
The specifier [[nodiscard]]
can optionally be added to precede the return type of a function. This specifier is used to indicate that the return value from a function must not be ignored – that is, it must be captured in a variable or utilized in an expression. Should the function’s return value consequently be ignored, a compiler warning will be issued. Note that the nodiscard
qualifier can be added to the function prototype and optionally to the definition (or required in a definition if there is no prototype). Ideally, nodiscard
should appear in both locations.
As we move to the object-oriented sections in this book (Chapter 5, Exploring Classes in Detail, and beyond), we will learn that there are many more details and quite interesting features relating to functions. Nonetheless, we have sufficiently recalled the basics needed to move forward. Next, let’s continue our C++ language review with user defined types.