Consider the following:
int i = 3;
i
is an object, and it has the type int
. It is not cv-qualified (is not const
or volatile
, or both.)
Now we add:
const int& j = i;
const int* k = &i;
j
is a reference which refers to i
, and k
is a pointer which points to i
. (From now on, we simply combine "refer to" and "points to" to just "points to".)
At this point, we have two cv-qualified variables, j
and k
, that point to a non-cv-qualified object. This is mentioned in §7.1.?5.1/3:
A pointer or reference to a cv-qualified type need not actually point or refer to a cv-qualified object, but it is treated as if it does; a const-qualified access path cannot be used to modify an object even if the object referenced is a non-const object and can be modified through some other access path. [Note: cv-qualifiers are supported by the type system so that they cannot be subverted without casting (5.2.11). ]
What this means is that a compiler must respect that j
and k
are cv-qualified, even though they point to a non-cv-qualified object. (So j = 5
and *k = 5
are illegal, even though i = 5
is legal.)
We now consider removing the const
from those:
const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;
This is legal (§refer to 5.2.11), but is it undefined behavior? No. See §7.1.?5.1/4:
Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.
Emphasis mine.
Remember that i
is not const
and that j
and k
both point to i
. All we've done is tell the type system to remove the const-qualifier from the type so we can modify the pointed to object, and then modified i
through those variables.
This is exactly the same as doing:
int& j = i; // removed const with const_cast...
int* k = &i; // ..trivially legal code
j = 5;
*k = 5;
And this is trivially legal. We now consider that i
was this instead:
const int i = 3;
What of our code now?
const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;
It now leads to undefined behavior, because i
is a const-qualified object. We told the type system to remove const
so we can modify the pointed to object, and then modified a const-qualified object. This is undefined, as quoted above.
Again, more apparent as:
int& j = i; // removed const with const_cast...
int* k = &i; // ...but this is not legal!
j = 5;
*k = 5;
Note that simply doing this:
const_cast<int&>(j);
*const_cast<int*>(k);
Is perfectly legal and defined, as no const-qualified objects are being modified; we're just messing with the type-system.
Now consider:
struct foo
{
foo() :
me(this), self(*this), i(3)
{}
void bar() const
{
me->i = 5;
self.i = 5;
}
foo* me;
foo& self;
int i;
};
What does const
on bar
do to the members? It makes access to them go through something called a cv-qualified access path. (It does this by changing the type of this
from T* const
to cv T const*
, where cv
is the cv-qualifiers on the function.)
So what are the members types during the execution of bar
? They are:
// const-pointer-to-non-const, where the pointer points cannot be changed
foo* const me;
// foo& const is ill-formed, cv-qualifiers do nothing to reference types
foo& self;
// same as const int
int const i;
Of course, the types are irrelevant, as the important thing is the const-qualification of the pointed to objects, not the pointers. (Had k
above been const int* const
, the latter const
is irrelevant.) We now consider:
int main()
{
foo f;
f.bar(); // UB?
}
Within bar
, both me
and self
point to a non-const foo
, so just like with int i
above we have well-defined behavior. Had we had:
const foo f;
f.bar(); // UB!
We would have had UB, just like with const int
, because we would be modifying a const-qualified object.
In your question, you have no const-qualified objects, so you have no undefined behavior.
And just to add an appeal to authority, consider the const_cast
trick by Scott Meyers, used to recycle a const-qualified function in a non-const function:
struct foo
{
const int& bar() const
{
int* result = /* complicated process to get the resulting int */
return *result;
}
int& bar()
{
// we wouldn't like to copy-paste a complicated process, what can we do?
}
};
He suggests:
int& bar(void)
{
const foo& self = *this; // add const
const int& result = self.bar(); // call const version
return const_cast<int&>(result); // take off const
}
Or how it's usually written:
int& bar(void)
{
return const_cast<int&>( // (3) remove const from result
static_cast<const foo&>(*this) // (1) add const to this
.bar() // (2) call const version
);
}
Note this is, again, perfectly legal and well-defined. Specifically, because this function must be called on a non-const-qualified foo
, we are perfectly safe in stripping the const-qualification from the return type of int& boo() const
.
(Unless someone shoots themselves with a const_cast
+ call in the first place.)
To summarize:
struct foo
{
foo(void) :
i(),
self(*this), me(this),
self_2(*this), me_2(this)
{}
const int& bar() const
{
return i; // always well-formed, always defined
}
int& bar() const
{
// always well-formed, always well-defined
return const_cast<int&>(
static_cast<const foo&>(*this).
bar()
);
}
void baz() const
{
// always ill-formed, i is a const int in baz
i = 5;
// always ill-formed, me is a foo* const in baz
me = 0;
// always ill-formed, me_2 is a const foo* const in baz
me_2 = 0;
// always well-formed, defined if the foo pointed to is non-const
self.i = 5;
me->i = 5;
// always ill-formed, type points to a const (though the object it
// points to may or may not necessarily be const-qualified)
self_2.i = 5;
me_2->i = 5;
// always well-formed, always defined, nothing being modified
// (note: if the result/member was not an int and was a user-defined
// type, if it had its copy-constructor and/or operator= parameter
// as T& instead of const T&, like auto_ptr for example, this would
// be defined if the foo self_2/me_2 points to was non-const
int r = const_cast<foo&>(self_2).i;
r = const_cast<foo* const>(me_2)->i;
// always well-formed, always defined, nothing being modified.
// (same idea behind the non-const bar, only const qualifications
// are being changed, not any objects.)
const_cast<foo&>(self_2);
const_cast<foo* const>(me_2);
// always well-formed, defined if the foo pointed to is non-const
// (note, equivalent to using self and me)
const_cast<foo&>(self_2).i = 5;
const_cast<foo* const>(me_2)->i = 5;
// always well-formed, defined if the foo pointed to is non-const
const_cast<foo&>(*this).i = 5;
const_cast<foo* const>(this)->i = 5;
}
int i;
foo& self;
foo* me;
const foo& self_2;
const foo* me_2;
};
int main()
{
int i = 0;
{
// always well-formed, always defined
int& x = i;
int* y = &i;
const int& z = i;
const int* w = &i;
// always well-formed, always defined
// (note, same as using x and y)
const_cast<int&>(z) = 5;
const_cast<int*>(w) = 5;
}
const int j = 0;
{
// never well-formed, strips cv-qualifications without a cast
int& x = j;
int* y = &j;
// always well-formed, always defined
const int& z = i;
const int* w = &i;
// always well-formed, never defined
// (note, same as using x and y, but those were ill-formed)
const_cast<int&>(z) = 5;
const_cast<int*>(w) = 5;
}
foo x;
x.bar(); // calls non-const, well-formed, always defined
x.bar() = 5; // calls non-const, which calls const, removes const from
// result, and modifies which is defined because the object
// pointed to by the returned reference is non-const,
// because x is non-const.
x.baz(); // well-formed, always defined
const foo y;
y.bar(); // calls const, well-formed, always defined
const_cast<foo&>(y).bar(); // calls non-const, well-formed,
// always defined (nothing being modified)
const_cast<foo&>(y).bar() = 5; // calls non-const, which calls const,
// removes const from result, and
// modifies which is undefined because
// the object pointed to by the returned
// reference is const, because y is const.
y.baz(); // well-formed, always undefined
}
I refer to the ISO C++03 standard.