Strange Behavior of Const_Cast

Strange behavior of const_cast

Modifying a constant object gives undefined behaviour, so your program could (in principle) do anything.

One reason for leaving this behaviour undefined is to allow the optimisation of replacing a constant variable with its value (since you've stated that the value can never change). That's what is happening here: a is replaced with the value 7 at compile time, and so will keep that value whatever you try to do to it at run time.

Weird Behaviour with const_cast

So, aside from the "it's undefined behaviour" (which it is), the compiler is perfectly fine to use the fact that M is a constant, thus won't change, in the evaluation of cout ... << M << ..., so can use an instruction that has the immediate value 10, instead of the actual value stored in the memory of M. (Of course, the standard will not say how this works, more than "it's undefined", and compilers are able to choose different solutions in different circumstances, etc, etc, so it's entirely possible that you'll get different results if you modify the code, use a different compiler, different version of compiler or the wind is blowing in a different direction).

Part of the tricky bit with "undefined behaviour" is that it includes things that are "perfectly what you may expect" as well as "nearly what you'd expect". The compiler could also decide to start tetris if it discovers this is what you are doing.

And yes, this is very much one of the reasons why you SHOULD NOT use const_cast. At the very least NOT on things that were originally const - it's OK if you have something along these lines:

int x;

void func(const int* p)
{
...
int *q = const_cast<int *>(p);

*q = 7;
}

...

func(&x);

In this case, x is not actually const, it just becomes const when we pass it to func. Of course, the compiler may still assume that x is not changed in func, and thus you could have problems....

behavior of const_cast in C++

As-written the way you're doing this is undefined behavior. If you wanted to see the effects of const_cast<> in a defined manner:

int a = 5;                  // note: not const. regular object.
const int& cref = a; // const-reference to same object.
cref = 7; // illegal. cref is a const reference.
const_cast<int&>(cref) = 7; // legal. the original object a is not const.

The only reason this is defined behavior is due to the non-const nature of the original variable, a. You cannot take an outright-const object and simply cast away the const-ness, which is what your posted code did. (at least as it has been explained to me on several occasions).

Is this undefined behavior with const_cast?

Quote from cppreference:

Even though const_cast may remove constness or volatility from any pointer or reference, using the resulting pointer or reference to write to an object that was declared const or to access an object that was declared volatile invokes undefined behavior.

So yes, modifying constant variables is undefined behavior. The output you see is caused by the fact that you tell the compiler that the value of a will never change, so it can just put a literal 0 instead of the variable a in the cout line.

Undefined behaviour with const_cast

I was hoping that someone could clarify exactly what is meant by undefined behaviour in C++.

Technically, "Undefined Behaviour" means that the language defines no semantics for doing such a thing.

In practice, this usually means "don't do it; it can break when your compiler performs optimisations, or for other reasons".

What is puzzling me is why this appears to work and will modify the original const object but doesn't even prompt me with a warning to notify me that this behaviour is undefined.

In this specific example, attempting to modify any non-mutable object may "appear to work", or it may overwrite memory that doesn't belong to the program or that belongs to [part of] some other object, because the non-mutable object might have been optimised away at compile-time, or it may exist in some read-only data segment in memory.

The factors that may lead to these things happening are simply too complex to list. Consider the case of dereferencing an uninitialised pointer (also UB): the "object" you're then working with will have some arbitrary memory address that depends on whatever value happened to be in memory at the pointer's location; that "value" is potentially dependent on previous program invocations, previous work in the same program, storage of user-provided input etc. It's simply not feasible to try to rationalise the possible outcomes of invoking Undefined Behaviour so, again, we usually don't bother and instead just say "don't do it".

What is puzzling me is why this appears to work and will modify the original const object but doesn't even prompt me with a warning to notify me that this behaviour is undefined.

As a further complication, compilers are not required to diagnose (emit warnings/errors) for Undefined Behaviour, because code that invokes Undefined Behaviour is not the same as code that is ill-formed (i.e. explicitly illegal). In many cases, it's not tractible for the compiler to even detect UB, so this is an area where it is the programmer's responsibility to write the code properly.

The type system — including the existence and semantics of the const keyword — presents basic protection against writing code that will break; a C++ programmer should always remain aware that subverting this system — e.g. by hacking away constness — is done at your own risk, and is generally A Bad Idea.™

I can imagine a case where lack of awareness that C-style cast can result in a const_cast being made could occur without being noticed.

Absolutely. With warning levels set high enough, a sane compiler may choose to warn you about this, but it doesn't have to and it may not. In general, this is a good reason why C-style casts are frowned upon, but they are still supported for backwards compatibility with C. It's just one of those unfortunate things.

strange behavior with pointers to constants

A nice example of a problem caused by optimization., and also why you should be careful using stuff like that.
The main objective of the keywords const is to tell the compiler that this value will not change, and the compiler may (but not must) optimize it out. It is more effecient to just put an assembly instruction that uses the value 20 rather than going to the memory and fetching the actual value, and this is exactly what the compiler did. Because you allowed it. Compiler is not obliged to follow pointers and actually check what you modify. So this is undefined behavior you have here and this is how const works.

Const method that modifies *this without const_cast

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.



Related Topics



Leave a reply



Submit