Is It Allowed to Cast Away Const on a Const-Defined Object as Long as It Is Not Actually Modified

Is it allowed to cast away const on a const-defined object as long as it is not actually modified?

Yes. This is entirely legal. (It is dangerous, but it is legal.) If you (attempt to) modify a an object declared const, then the behaviour is undefined.

From n4659 (which is the last draft of C++17), section 10.1.7.1 [dcl.type.cv] para 4:

Except that any class member declared mutable (10.1.1) can be modified, any attempt to modify a const object during its lifetime (6.8) results in undefined behavior

My emphasis. That is from C++17, but this has been true of all versions of C++.

If you look at the section on const_cast there is a note that

[ Note: Depending on the type of the object, a write operation through the pointer, lvalue or pointer
to data member resulting from a const_cast that casts away a const-qualifier76 may produce undefined
behavior (10.1.7.1). — end note ]

Notes are not normative, but this strongly implies that obtaining a non-const reference or pointer to a const object is legal. It is the write that is not allowed.

Is const_cast safe?

const_cast is safe only if you're casting a variable that was originally non-const. For example, if you have a function that takes a parameter of a const char *, and you pass in a modifiable char *, it's safe to const_cast that parameter back to a char * and modify it. However, if the original variable was in fact const, then using const_cast will result in undefined behavior.

void func(const char *param, size_t sz, bool modify)
{
if(modify)
strncpy(const_cast<char *>(param), sz, "new string");
printf("param: %s\n", param);
}

...

char buffer[16];
const char *unmodifiable = "string constant";
func(buffer, sizeof(buffer), true); // OK
func(unmodifiable, strlen(unmodifiable), false); // OK
func(unmodifiable, strlen(unmodifiable), true); // UNDEFINED BEHAVIOR

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.

Is const-casting away const-ness of references to actual const objects permitted if they are never modified through them?

Per https://en.cppreference.com/w/cpp/language/const_cast:

const_cast makes it possible to form a reference or pointer to non-const type that is actually referring to a const object or a reference or pointer to non-volatile type that is actually referring to a volatile object. Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

So, the const_cast itself is allowed (and well-defined), even though it would be undefined behavior to actually modify the object via the resulting non-const reference.

Is it safe to remove const via const_cast and invoke a non-const function that does not modify the resulting object?

Undefined behavior with respect to const_cast is defined by the C++11 standard's §3.8/9 (§3.8 is “Object lifetime”):

Creating a new object at the storage location that a const object with static, thread, or automatic storage duration occupies or, at the storage location that such a const object used to occupy before its lifetime ended results in undefined behavior.

and §7.1.6.1/4 (§7.1.6.1 is “The cv-qualifiers”)

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.

In other words, you get UB if you modify an originally const object, and otherwise 1not.

The const_cast itself doesn't introduce UB.


There is additionally a non-normative note at §5.2.11/7 that “depending on the type” a write through the pointer or reference obtained from a const_cast, may have undefined behavior.

This non-normative note is so wooly that it has its own non-normative footnote, that explains that “const_cast is not limited to conversions that cast away a const-qualifier.”.

However, still with that clarification I fail to think of any case where the write could be well-defined or not depending on the type, i.e., I fail to make sense of this note. Two other answers here focus on the word “write” in this note, and that's necessary to get into UB-land via §3.8/9, yes. The to me rather fishy aspect is the “depending on the type”, which appears to be the significant part of that note.


1) Except insofar as UB-rules about other non-const_cast-related things get into play, e.g. nulling a pointer that's later dereferenced in a context other than as typeid-expression.

How to use const_cast?

You are not allowed to const_cast variables that are actually const. This results in undefined behavior. const_cast is used to remove the const-ness from references and pointers that ultimately refer to something that is not const.

So, this is allowed:

int i = 0;
const int& ref = i;
const int* ptr = &i;

const_cast<int&>(ref) = 3;
*const_cast<int*>(ptr) = 3;

It's allowed because i, the object being assigned to, is not const. The below is not allowed:

const int i = 0;
const int& ref = i;
const int* ptr = &i;

const_cast<int&>(ref) = 3;
*const_cast<int*>(ptr) = 3;

because here i is const and you are modifying it by assigning it a new value. The code will compile, but its behavior is undefined (which can mean anything from "it works just fine" to "the program will crash".)

You should initialize constant data members in the constructor's initializers instead of assigning them in the body of constructors:

Student(const Student & s) 
: Person(p.getName(), p.getEmailAddress(), p.getBirthDate()),
school(0),
studentNumber(s.studentNumber)
{
// ...
}

Am I right in saying that const_cast followed by modification on a ref-to-const bound to a temporary is okay?

No.

First, as far as I can tell, whether it is a literal or not is
irrelevant. Rvalues of non-class types always have non-cv qualified
types (§3.10/9), however, in §8.5.3 (initialization of a reference), we
have:

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

[...]

--

Otherwise, a temporary of type “cv1 T1” is created and initialized from the initializer expression using the rules for a non-reference copy initialization (8.5). The reference is then bound to the temporary. If T1 is reference-related to T2, cv1 must be the same cv-qualification as, or greater cvqualification
than, cv2; otherwise, the program is ill-formed.

(All of the preceding points concern either lvalues or class types.)

In our case, we have:

int const& x = ...;

So cv1 T1 is int const, and the temporary object we create has type
int const. This is a top level const (on the object), so any attempt
to modify it is undefined behavior.

At least, that's my interpretation. I wish the standard were a bit clearer about this.

Is it legal to modify an object created with new through a const pointer?

const is part of the type. It doesn't matter whether you allocate your object with dynamic, static or automatic storage duration. It's still const. Casting away that constness and mutating the object would still be an undefined operation.

constness is an abstraction that the type system gives us to implement safety around non-mutable objects; it does so in large part to aid us in interaction with read-only memory, but that does not mean that its semantics are restricted to such memory. Indeed, C++ doesn't even know what is and isn't read-only memory.

As well as this being derivable from all the usual rules, with no exception [lol] made for dynamically-allocated objects, the standards mention this explicitly (albeit in a note):

[C++03: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). [Note: because references are not objects, references cannot be created by new-expressions. ] [Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. ] [..]

[C++11: 5.3.4/1]: The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). It is implementation-defined whether over-aligned types are supported (3.11). [ Note: because references are not objects, references cannot be created by new-expressions. —end note ] [ Note: the type-id may be a cv-qualified type, in which case the object created by the new-expression has a cv-qualified type. —end note ] [..]

There's also a usage example given in [C++11: 7.1.6.1/4].

Not sure what else you expected. I can't say I've ever done this myself, but I don't see any particular reason not to. There's probably some tech sociologist who can tell you statistics on how rarely we dynamically allocate something only to treat it as non-mutable.



Related Topics



Leave a reply



Submit