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: 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:
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);
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.
Contact: webmaster@codeguru.com
CodeGuru - the website for developers.