Unless otherwise stated, the following rules also apply to member functions.
const
&
) instead of call-by-value, unless using a pre-defined data
type or a pointer.
The best known function which uses unspecified arguments is
printf()
. The use of such functions is not advised since
the strong type checking provided by C++ is thereby avoided. Some of the
possibilities provided by unspecified function arguments can be attained
by overloading functions and by using default arguments.
Functions having long lists of arguments look complicated, are difficult to read, and can indicate poor design. In addition, they are difficult to use and to maintain.
By using references instead of pointers as function arguments, code can be made more readable, especially within the function. A disadvantage is that it is not easy to see which functions change the values of their arguments. Member functions which store pointers which have been provided as arguments should document this clearly by declaring the argument as a pointer instead of as a reference. This simplifies the code, since it is normal to store a pointer member as a reference to an object.
One difference between references and pointers is that there is no null-reference in the language, whereas there is a null-pointer. This means that an object must have been allocated before passing it to a function. The advantage with this is that it is not necessary to test the existence of the object within the function.
C++ invokes functions according to call-by-value. This means that
the function arguments are copied to the stack via invocations of copy
constructors, which, for large objects, reduces performance. In addition,
destructors will be invoked when exiting the function. const
&
arguments mean that only a reference to the object in
question is placed on the stack (call-by-reference) and that the
object's state (its instance variables) cannot be modified. (At least
some const
member functions are necessary for such objects
to be at all useful).
// Unnecessarily complicated use of pointers void addOneComplicated( int* integerPointer ) { *integerPointer += 1; } addOneComplicated( &j ); // Write this way instead: void addOneEasy( int& integerReference ) { integerReference += 1; } addOneEasy( i );
// a. A copy of the argument is created on the stack. // The copy constructor is called on entry, // and the destructor is called at exit from the function. // This may lead to very inefficient code. void foo1( String s ); String a; foo1( a ); // call-by-value // b. The actual argument is used by the function // and it can be modified by the function. void foo2( String& s ); String b; foo2( b ); // call-by-reference // c. The actual argument is used by the function // but it cannot be modified by the function. void foo3( const String& s ); String c; foo3( c ); // call-by-constant-reference // d. A pointer to the actual argument is used by the function. // May lead to messy syntax when the function uses the argument. void foo4( const String* s ); String d; foo4( &d ); // call-by-constant-pointer
Overloading of functions can be a powerful tool for creating a family of related functions that only differ as to the type of data provided as arguments. If not used properly (such as using functions with the same name for different purposes), they can, however, cause considerable confusion.
class String { public: // Used like this: // ... // String x = "abc123"; int contains( const char c ); // int i = x.contains( 'b' ); int contains( const char* cs ); // int j = x.contains( "bc1" ); int contains( const String& s ); // int k = x.contains( x ); // ... };
The names of formal arguments may be specified in both the function declaration and the function definition in C++, even if these are ignored by the compiler in the declaration. Providing names for function arguments is a part of the function documentation. The name of an argument may clarify how the argument is used, reducing the need to include comments in, for example, a class definition. It is also easier to refer to an argument in the documentation of a class if it has a name.
int setPoint( int, int ); // No ! int setPoint( int x, int y ); // Good int setPoint( int x, int y ) { // ... }
Functions, for which no return type is explicitly declared, implicitly
receive int
as the return type. This can be confusing for
a beginner, since the compiler gives a warning for a missing return
type. Because of this, functions which return no value should specify
void
as the return type.
If a function returns a reference or a pointer to a local variable, the memory to which it refers will already have been deallocated, when this reference or pointer is used. The compiler may or may not give a warning for this.
void
.void strangeFunction( const char* before, const char* after ) { // ... }
#define
to obtain more efficient code; instead, use inline
functions.
inline
functions
when they are really needed.
See also 7.2.
Inline
functions have the advantage of often being faster
to execute than ordinary functions. The disadvantage in their use is
that the implementation becomes more exposed, since the definition of
an inline
function must be placed in an include file for
the class, while the definition of an ordinary function may be placed
in its own separate file.
A result of this is that a change in the implementation of an
inline
function can require comprehensive re-compiling when
the include file is changed. This is true for traditional file-based
programming environments which use such mechanisms as make
for compilation.
The compiler is not compelled to actually make a function inline. The decision criteria for this differ from one compiler to another. It is often possible to set a compiler flag so that the compiler gives a warning each time it does not make a function inline (contrary to the declaration). "Outlined inlines" can result in programs that are both unnecessarily large and slow.
It may be appropriate to separate inline definitions from class definitions and to place these in a separate file.
// Example of problems with #define "functions" #define SQUARE(x) ((x)*(x)) int a = 2; int b = SQUARE(a++); // b = (2 * 3) = 6 // Inline functions are safer and easier to use than macros if you // need an ordinary function that would have been unacceptable for // efficiency reasons. // They are also easier to convert to ordinary functions later on. inline int square( int x ) { return ( x * x ); }; int c = 2; int d = square( c++ ); // d = ( 2 * 2 ) = 4
Temporary objects are often created when objects are returned from functions or when objects are given as arguments to functions. In either case, a constructor for the object is first invoked; later, a destructor is invoked. Large temporary objects make for inefficient code. In some cases, errors are introduced when temporary objects are created. It is important to keep this in mind when writing code. It is especially inappropriate to have pointers to temporary objects, since the lifetime of a temporary object is undefined. (See 18.7).
class BigObject { double big[123456]; }; // Example of a very inefficient function with respect to temporary objects: BigObject slowTransform( BigObject myBO ) { // When entering slowTransform(), myBO is a copy of the function argument // provided by the user. -> A copy constructor for BigObject is executed. // ... Transform myBO in some way return myBO; // Transformed myBO returned to the user } // When exiting slowTransform(), a copy of myBO is returned to the // user -> copy-constructor for BigObject is executed, again. // Much more efficient solution: BigObject& fastTransform( BigObject& myBO ) { // When entering fastTransform(), myBO is the same object as the function // argument provided by the user. -> No copy-constructor is executed. // Transform myBO in some way return myBO; // Transformed myBO is returned to the user. } // When exiting fastTransform(), the very same myBO is returned // to the user. -> No copy constructor executed. void main() { BigObject BO; BO = slowTransform( BO ); BO = fastTransform( BO ); // Same syntax as slowTransform() !! }
Long functions have disadvantages:
Complex functions are difficult to test. If a function consists of 15 nested if statements, then there are 2**15 (or 32768) different branches to test in a single function.