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
x( ).
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 9. 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 nonconfusing 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 so it’s hidden from the client.
Returning
by const value
A
similar truth holds for the return value. If you return by value from a
function, as a
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 is automatically not modified.
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:Constret.cpp
// Constant return by value
// Result cannot be used as an lvalue
class X {
int i;
public:
X(int ii = 0) { i = ii; }
void 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
9). 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.
The
way the constness of class objects is preserved is shown later in the chapter.
Passing
and returning addresses
If
you pass or return a pointer (or a reference), it’s possible for the user
to take the pointer and modify the original value. If you make the pointer a
const,
you prevent this from happening, which may be an important factor. 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 a pointer to a
const. The
choice of whether to return a pointer to a
const
depends on what you want to allow your user to do with it. Here’s an
example that demonstrates the use of
const
pointers as function arguments and return values:
//: C08:Constp.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 be a
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. 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 the problem. For the creator of the class,
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.
Because
of the syntax of references (it looks like pass-by-value) it’s possible
to pass a temporary
object to a function that takes a reference, whereas you can never pass a
temporary object to a function that takes 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:Consttmp.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.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru