Const Method That Modifies *This Without Const_Cast

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.

const_cast 'this' in const method to assign 'this' to outer variable?

No, this is potentially dangerous.

func() is marked const which means that it can be called by a const object:

const Foo foo;
foo.func();

Because this is const Foo*.

If you const_cast away the const you end up with a Foo* to a const object. This means that any modification to that object through the non-const pointer (or through any copy of that pointer, bar in this case) will get you undefined behavior since you are not allowed to modify a const object (duh).

Plus the obvious problem is that you're lying to the consumer of class Foo by saying func() won't modify anything while you're doing the opposite.

const_cast is almost never correct and this seems like an XY-problem to me.

Is const_cast(this) with a write operation undefined behaviour, if the actual object is non-const?

That's not undefined. That's exactly what const_cast is for. As long as the object itself is non-const then you can cast it away with const_cast and do the same things with it as a non-const pointer.

Do note that const_cast is usually considered a code smell and might indicate bad design.


As the standard says:

In the body of a non-static ([class.mfct]) member function, the
keyword this is a prvalue whose value is a pointer to the object for
which the function is called. The type of this in a member function of
a class X is X*. If the member function is declared const, the type of
this is const X*, if the member function is declared volatile, the
type of this is volatile X*, and if the member function is declared
const volatile, the type of this is const volatile X*.

The type of this is const X* in your case even though the object itself is non-const.


The standard says this about const_cast:

For two similar types T1 and T2, a prvalue of type T1 may be
explicitly converted to the type T2 using a const_­cast. The
result of a const_­cast refers to the original entity.

So, casting from const X* to X* is also legal.


Lastly, it says (albeit in a note):

[ 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-qualifier may produce undefined
behavior ([dcl.type.cv]). — end note  ]

And [dcl.type.cv] tells us:

Any attempt to modify ([expr.ass], [expr.post.incr], [expr.pre.incr])
a const object ([basic.type.qualifier]) during its lifetime
([basic.life]) results in undefined behavior.

Luckily, our this is pointing to a non-const object, so casting it and then modifying this object through the new non-const pointer doesn't trigger undefined behaviour.


Sorry Angew.

How do I remove code duplication between similar const and non-const member functions?

Yes, it is possible to avoid the code duplication. You need to use the const member function to have the logic and have the non-const member function call the const member function and re-cast the return value to a non-const reference (or pointer if the functions returns a pointer):

class X
{
std::vector<Z> vecZ;

public:
const Z& z(size_t index) const
{
// same really-really-really long access
// and checking code as in OP
// ...
return vecZ[index];
}

Z& z(size_t index)
{
// One line. One ugly, ugly line - but just one line!
return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
}

#if 0 // A slightly less-ugly version
Z& Z(size_t index)
{
// Two lines -- one cast. This is slightly less ugly but takes an extra line.
const X& constMe = *this;
return const_cast<Z&>( constMe.z(index) );
}
#endif
};

NOTE: It is important that you do NOT put the logic in the non-const function and have the const-function call the non-const function -- it may result in undefined behavior. The reason is that a constant class instance gets cast as a non-constant instance. The non-const member function may accidentally modify the class, which the C++ standard states will result in undefined behavior.

Why can I call a method that changes a member from a const method?

The const applies to the pointer itself, f, not to what this pointer is pointing to. The type of f inside your const-qualified member function, Test::test(), is Foo* const (i.e., const pointer to Foo), not const Foo* (i.e., pointer to const Foo). That's why you can modify what the pointer is pointing to in spite of the const qualification of the member function.


Note that the following sample member function, Test::test2(), does fail to compile since it is const-qualified and tries to modify the pointer data member, f:

void Test::test2() const {
f = nullptr; // <-- error
}

Const function calling non const or vice versa (to avoid duplication)?

If you have to make a function that is const-agnostic, and avoids duplication, one neat way to do it is delegating implementation to a template, for example

class Foo {
private:

int my_int;
template <typename ThisPtr>
static auto& get(ThisPtr this_ptr) {
return this_ptr->my_int;
}

public:
int& get() {
return get(this);
}

const int& get() const {
return get(this);
}
};

This way you are free from the fear associated with using const_cast, mutable and other stuff that goes into trying to reduce code duplication in cases like this. If you get something wrong, the compiler will let you know.

Return 'this' as non-const from const member function

The reason why this fails is that inside a const member function, this is really a const Point*, not a Point*. Thus you are trying to initialize a non-const reference from a const pointer. It's not that the compiler isn't believing you, you're just asking for two incompatible things at one time.

This is one of the very few valid uses of const_cast, in my opinion. Normally, using const_cast is almost always a sign of a design error, or worse a programming error.

Here, the function is really const and should be const, but there is no reason why you shouldn't be able to chain something non-const afterwards, so it's arguably legitimate to do such a thing.

Do note, however, although the function is strictly const (in respect to the object, not so much in its use of IO functions!), one thing you should consider is that in some (rare) cases, it may result in code that doesn't do what you want. The compiler is allowed to cache the result of a const function and omit another call to the same const function (since you promised that it won't change anything). Therefore, it is allowable to optimize some_point.Print().Print(); into some_point.Print(). This is probably not a problem for you (why would you want to print the same values twice), just something to be generally aware of.



Related Topics



Leave a reply



Submit