This post isn’t about our favorite old school robot Johnny 5. So let’s start with the second part: the non defined order of evaluation of function parameters (yes, it’s non defined)
if(A(C()), B())
The compiler is free to evaluate A(), then B(), C(), or B(), then C(), then A(), etc. This could be troublesome if you expect a variable to be initialized in A() to be used in B(). The one guarantee is that C() is evaluated before A().
Another item which has some fun with the order of evaluations are the logical operators. Probably a bit of an “yeah, makes sense”, but worth the refresher.
operator || and operator && won’t necessarily evaluate all their arguments. This is known as “Short circuit evaluation”. For example:
if( true || A())
function A() would never be called.
For operator &&:
if( false && B() )
function B() would similarly never be called.
To be complete, the ternary operator also works in that fashion. There can be some advantage to this, such as when you only want the second argument to be evaluated if the first is “acceptable”.
Bool UsePtrIfValid(char* ptr) { return ptr != nullptr AND functUsePtr(ptr); }
It’s a construct I’ve seen in code before many times, but the ternary operator may make it clearer for whoever has to maintain your code in the future.
Bool UsePtrIfValid(char* ptr) { return ptr != nullptr ? functUsePtr(ptr), false; }
To rewind a bit, as we saw, “The order of evaluation of function arguments is unspecified”. This has some implications for default arguments. If you read the standard, presumably at night to help yourself fall asleep, you may recall the following example from the standard [dcl.fct.default / 5.7 paragraph 9]:
int a; int f(int a, int b = a); // Error:parameter 'a' used // as a default argument.
Oops, meant to use the variable ‘a’, not the first parameter ‘a’. The point however isn’t about the misnaming of variables. If variable ‘a’ was renamed ‘c’ as below, you would still have issues as you can’t reference another parameter since the order is undefined and it may not be defined yet.
int newName; int f(int a, int b = a); // Error:parameter 'a' used // as a default argument.
There are 2 more interesting facts with default parameters. First, “A default argument is not
part of the type of a function.”
int f(int = 0); void h() { int j = f(1); int k = f(); // OK, means f(0) } int (*p1)(int) = &f; int (*p2)() = &f; // error: type mismatch
In the example above, the function f(int) has a default parameter which means the method may be called without it’s parameter. However since the default parameter is not part of the type of the function, if we want to take a reference to the function through a function pointer, we need to include the parameter type. The function pointer p1 above does just that. However failing to specify the parameter type in function pointer p2 gives an error.
The second interesting fact is that for virtual function calls, the default arguments are defined based on the static type of the pointer or reference to the object. Basically, this means what matters is the static type of the pointer used. Not the underlying object type. An overriding function in a derived class doesn’t acquire the default arguments from the function it overrides. This has 2 implications, outlined in the code:
struct A { virtual void f(int a = 7) { cout << a << endl; } }; struct B : public A { void f(int a = 8) { cout << a << endl; } }; struct C : public A { void f(int a ) { cout << a << endl; } }; A* pA = new B(); // static type is A, outputs: 7. B* pB = new B(); // static type is B, outputs: 8. A* pB = new C(); // same as before, // static type is A, outputs: 7. C* pB = new C(); // Error: static type is C which // has no default argument.
Final Thoughts: Overall only a few surprises at the end with virtual functions and default parameters. For the first part, worth a refresher since you’re likely to see most of this in production code.