Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Function arguments

& return values

The use of const to specify function arguments and return values is another place where the concept of constants can be confusing. If you are passing objects by value, specifying const has no meaning to the client (it means that the passed argument cannot be modified inside the function). If you are returning an object of a user-defined type by value as a const, it means the returned value cannot be modified. If you are passing and returning addresses, const is a promise that the destination of the address will not be changed.

Passing by const value

You can specify that function arguments are const when passing them by value, such as

void f1(const int i) {
  i++; // Illegal -- compile-time error
}

but what does this mean? You’re making a promise that the original value of the variable will not be changed by the function f1( ). However, because the argument is passed by value, you immediately make a copy of the original variable, so the promise to the client is implicitly kept.

Inside the function, the const takes on meaning: the argument cannot be changed. So it’s really a tool for the creator of the function, and not the caller.

To avoid confusion to the caller, you can make the argument a const inside the function, rather than in the argument list. You could do this with a pointer, but a nicer syntax is achieved with the reference, a subject that will be fully developed in Chapter XX. Briefly, a reference is like a constant pointer that is automatically dereferenced, so it has the effect of being an alias to an object. To create a reference, you use the & in the definition. So the non-confusing function definition looks like this:

void f2(int ic) {
  const int& i = ic;
  i++;  // Illegal -- compile-time error
}

Again, you’ll get an error message, but this time the constness of the local object is not part of the function signature; it only has meaning to the implementation of the function and therefore it’s hidden from the client.

Returning by const value

A similar truth holds for the return value. If you say that a function’s return value is const:

const int g();

you are promising that the original variable (inside the function frame) will not be modified. And again, because you’re returning it by value, it’s copied so the original value could never be modified via the return value.

At first, this can make the specification of const seem meaningless. You can see the apparent lack of effect of returning consts by value in this example:

//: C08:Constval.cpp
// Returning consts by value
// has no meaning for built-in types

int f3() { return 1; }
const int f4() { return 1; }

int main() {
  const int j = f3(); // Works fine
  int k = f4(); // But this works fine too!
} ///:~

For built-in types, it doesn’t matter whether you return by value as a const, so you should avoid confusing the client programmer by leaving off the const when returning a built-in type by value.

Returning by value as a const becomes important when you’re dealing with user-defined types. If a function returns a class object by value as a const, the return value of that function cannot be an lvalue (that is, it cannot be assigned to or otherwise modified). For example:

//: C08:ConstReturnValues.cpp
// Constant return by value
// Result cannot be used as an lvalue

class X {
  int i;
public:
  X(int ii = 0);
  void modify();
};

X::X(int ii) { i = ii; }

void X::modify() { i++; }

X f5() {
  return X();
}

const X f6() {
  return X();
}

void f7(X& x) { // Pass by non-const reference
  x.modify();
}

int main() {
  f5() = X(1); // OK -- non-const return value
  f5().modify(); // OK
// Causes compile-time errors:
//!  f7(f5());
//!  f6() = X(1);
//!  f6().modify();
//!  f7(f6());
} ///:~

f5( ) returns a non- const X object, while f6( ) returns a const X object. Only the non- const return value can be used as an lvalue. Thus, it’s important to use const when returning an object by value if you want to prevent its use as an lvalue.

The reason const has no meaning when you’re returning a built-in type by value is that the compiler already prevents it from being an lvalue (because it’s always a value, and not a variable). Only when you’re returning objects of user-defined types by value does it become an issue.

The function f7( ) takes its argument as a non- const reference (an additional way of handling addresses in C++ which is the subject of Chapter XX). This is effectively the same as taking a non- const pointer; it’s just that the syntax is different. The reason this won’t compile in C++ is because of the creation of a temporary.

Temporaries

Sometimes, during the evaluation of an expression, the compiler must create temporary objects. These are objects like any other: they require storage and they must be constructed and destroyed. The difference is that you never see them – the compiler is responsible for deciding that they’re needed and the details of their existence. But there is one thing about temporaries: they’re automatically const. Because you usually won’t be able to get your hands on a temporary object, telling it to do something that will change that temporary is almost certainly a mistake because you won’t be able to use that information. By making all temporaries automatically const, the compiler informs you when you make that mistake.

In the above example, f5( ) returns a non- const X object. But in the expression:

f7(f5());

the compiler must manufacture a temporary object to hold the return value of f5( ) so it can be passed to f7( ). This would be fine if f7( ) took it’s argument by value; then the temporary would be copied into f7( ) and it wouldn’t matter what happened to the temporary X. However, f7( ) takes its argument by reference , which means in this example takes the address of the temporary X. Since f7( ) doesn’t take it’s argument by const reference, it has permission to modify the temporary object. But the compiler knows that the temporary will vanish as soon as the expression evaluation is complete, and thus any modifications you make to the temporary X will be lost. By making all temporary objects automatically const, this situation causes a compile-time error so you don’t get caught by what would be a very difficult bug to find.

However, notice the expressions that are legal:

  f5() = X(1);
f5().modify();

Although these pass muster for the compiler, they are actually problematic. f5( ) returns an X object, and for the compiler to satisfy the above expressions it must create a temporary to hold that return value. So in both expressions the temporary object is being modified, and as soon as the expression is over the temporary is cleaned up. As a result, the modifications are lost so this code is probably a bug – but the compiler doesn’t tell you anything about it. Expressions like these are simple enough for you to detect the problem, but when things get more complex it’s possible for a bug to slip through these cracks.

The way the constness of class objects is preserved is shown later in the chapter.

Passing and returning addresses

If you pass or return an address (either a pointer or a reference), it’s possible for the client programmer to take it and modify the original value. If you make the pointer or reference a const, you prevent this from happening, which may save you some grief. In fact, whenever you’re passing an address into a function, you should make it a const if at all possible. If you don’t, you’re excluding the possibility of using that function with anything that is a const.

The choice of whether to return a pointer or reference to a const depends on what you want to allow your client programmer to do with it. Here’s an example that demonstrates the use of const pointers as function arguments and return values:

//: C08:ConstPointer.cpp
// Constant pointer arg/return

void t(int*) {}

void u(const int* cip) {
//!  *cip = 2; // Illegal -- modifies value
  int i = *cip; // OK -- copies value
//!  int* ip2 = cip; // Illegal: non-const
}

const char* v() {
  // Returns address of static character array:
  return "result of function v()";
}

const int* const w() {
  static int i;
  return &i;
}

int main() {
  int x = 0;
  int* ip = &x;
  const int* cip = &x;
  t(ip);  // OK
//!  t(cip); // Not OK
  u(ip);  // OK
  u(cip); // Also OK
//!  char* cp = v(); // Not OK
  const char* ccp = v(); // OK
//!  int* ip2 = w(); // Not OK
  const int* const ccip = w(); // OK
  const int* cip2 = w(); // OK
//!  *w() = 1; // Not OK
} ///:~

The function t( ) takes an ordinary non- const pointer as an argument, and u( ) takes a const pointer. Inside u( ) you can see that attempting to modify the destination of the const pointer is illegal, but you can of course copy the information out into a non- const variable. The compiler also prevents you from creating a non- const pointer using the address stored inside a const pointer.

The functions v( ) and w( ) test return value semantics. v( ) returns a const char* that is created from a character array literal. This statement actually produces the address of the character array literal, after the compiler creates it and stores it in the static storage area. As mentioned earlier, this character array is technically a constant, which is properly expressed by the return value of v( ).

The return value of w( ) requires that both the pointer and what it points to must be const. As with v( ), the value returned by w( ) is valid after the function returns only because it is static. You never want to return pointers to local stack variables because they will be invalid after the function returns and the stack is cleaned up. (Another common pointer you might return is the address of storage allocated on the heap, which is still valid after the function returns.)

In main( ), the functions are tested with various arguments. You can see that t( ) will accept a non- const pointer argument, but if you try to pass it a pointer to a const, there’s no promise that t( ) will leave the pointer’s destination alone, so the compiler gives you an error message. u( ) takes a const pointer, so it will accept both types of arguments. Thus, a function that takes a const pointer is more general than one that does not.

As expected, the return value of v( ) can be assigned only to a const pointer. You would also expect that the compiler refuses to assign the return value of w( ) to a non- const pointer, and accepts a const int* const , but it might be a bit surprising to see that it also accepts a const int* , which is not an exact match to the return type. Once again, because the value (which is the address contained in the pointer) is being copied, the promise that the original variable is untouched is automatically kept. Thus, the second const in const int* const is only meaningful when you try to use it as an lvalue, in which case the compiler prevents you.

Standard argument passing

In C it’s very common to pass by value, and when you want to pass an address your only choice is to use a pointer [31]. However, neither of these approaches is preferred in C++. Instead, your first choice when passing an argument is to pass by reference, and by const reference at that. To the client programmer, the syntax is identical to that of passing by value, so there’s no confusion about pointers – they don’t even have to think about pointers. For the creator of the function, passing an address is virtually always more efficient than passing an entire class object, and if you pass by const reference it means your function will not change the destination of that address, so the effect from the client programmer’s point of view is exactly the same as pass-by-value (only more efficient).

Because of the syntax of references (it looks like pass-by-value to the caller) it’s possible to pass a temporary object to a function that takes a const reference, whereas you can never pass a temporary object to a function that takes a pointer – with a pointer, the address must be explicitly taken. So passing by reference produces a new situation that never occurs in C: a temporary, which is always const, can have its address passed to a function. This is why, to allow temporaries to be passed to functions by reference, the argument must be a const reference. The following example demonstrates this:

//: C08:ConstTemporary.cpp
// Temporaries are const

class X {};

X f() { return X(); } // Return by value

void g1(X&) {} // Pass by non-const reference
void g2(const X&) {} // Pass by const reference

int main() {
  // Error: const temporary created by f():
//!  g1(f());
  // OK: g2 takes a const reference:
  g2(f());
} ///:~

f( ) returns an object of class X by value . That means when you immediately take the return value of f( ) and pass it to another function as in the calls to g1( ) and g2( ), a temporary is created and that temporary is const. Thus, the call in g1( ) is an error because g1( ) doesn’t take a const reference, but the call to g2( ) is OK.


[31] Some folks go as far as saying that everything in C is pass by value, since when you pass a pointer a copy is made (so you’re passing the pointer by value). However precise this might be, I think it actually confuses things.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]