Inapoi
Inainte
Cuprins
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 11. 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 and leave 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++ and the subject of Chapter 11). 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 its 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 its 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 pointer to a const. 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[43].
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.
 |
|