C++ function declaration tips

Creating and calling functions is one of the most fundamental tasks in C++ programming. The function declaration serves as the primary description of a function’s interface – or contract – with callers, thereby making it the most important piece of code documentation. A good function declaration should convey as much information as possible. I’m sure an entire book could be written on the topic of function interface design, but I’ll just touch briefly on a few factors to consider when designing a function’s interface.

Error handling

Simple one-purpose functions should leave all or most error handling to the caller. This is more efficient because it eliminates error checking overhead on “known good” input values. A common pattern is a low level function that performs no input validation along with a high level function that does (and then calls the low level function to do the work). Such a design pattern gives the caller maximum flexibility.

If the function must return error (or other status) values, it should do so via the return value whenever possible, and it should define an enum or type alias (typedef) so that the return value type name makes it clear that the value is a status. Note that using the return value as a status may necessitate extra arguments passed by reference so the function has a way to return a result to the caller without using the return value.

Signed vs. unsigned

Consider this function:
int CountWords(char* pszInputString);

The int type is a signed value, but a count is always zero or greater. The return value should at least be unsigned int, or better yet size_t:
size_t CountWords(char* pszInputString);

Likewise, arguments that must never be negative should always be declared as an unsigned type.

By value or by reference

If the caller needs to pass arguments that the function will modify, they must be passed by reference. If the argument will not be modified, then it should be passed by value if it is a trivial type, else by const reference (except in unusual cases). For example:
bool CountWords(const CountOptions& options, char* pszInputString, size_t& ctWords);

Const or not

Incoming arguments that will not be modified should be declared const. This conveys a clear guarantee that the function will not try to modify the incoming value. Proper const delarations also give the optimizing compiler more ways to optimize the code.

Since counting words does not require the input string to be modifed, the CountWords() function’s string argument should really be const char* const (constant pointer to constant char) instead of char*:
size_t CountWords(const char* const pszInputString);

Now the caller can pass a string literal or any other string (even an MFC CString), and be assured that the CountWords function won’t change it.

Source code annotation language

Source code annotation language (SAL) is a fairly recent Microsoft specific development. It consists of predefined macros that help more fully express the constraints and purposes of function arguments without changing the compiled output. SAL helps convey additional information about the function’s contract with the caller, and it also helps the compiler detect potential problems at compile time by performing additional static code analysis.

As you can see below, source code annotation can get a bit messy. I rarely use source code annotation in my own code for this reason and also because it’s not portable to older versions of Visual Studio without extra work. Source code annotation is most appropriate in an API that will be consumed by others, and even then it should be considered carefully in light of its readability and portability trade-offs.

Exception specification

In C++, the exception specification conveys information about what kinds of exceptions a function may throw, either directly or indirectly via functions it calls. If a function cannot throw an exception, you should include the empty throw() suffix on the function declaration.

Putting it all together

Suppose we create a higher level function that validates the input, then calls a lower level function to count words. We might end up with something like this:

size_t CountWords(_In_z_ const char* const pszInputString) throw();

enum Status { eOK, eNull, eError, };

_Check_return_ Status ValidateAndCountWords(_In_z_ const char* const pszInputString, _Out_ size_t& ctWords) throw();