Hello, and welcome to C++ Weekly. I'm your host, Jason Turner, available for
code reviews and on-site training. This episode I need to bring up const_cast.
This is one of the things that I get sometimes pushback from my students on when I'm teaching,
and I just thought that this was an important topic to go ahead and bring up here. Now
may as well go ahead and point out that you can also buy a copy of my book, which is going
to be linked at the bottom of this episode which covers this same information as well.
Now I'm going to show you a very simple demonstration.
Now I am declaring "i" as const zero. I am then using const_cast to get rid of the const-ness of
"i", and then I am assigning it the value 42, and I am returning i from main. Now if we look over
here and we look at compiler explorer's output we can see that 0 is being returned from main,
not 42. Now lest you think that I have something up my sleeve and I am not doing a proper
const_cast here for some reason, I am just going to go ahead and get rid of the const and we can
see that the compiler is now in fact generating code that returns 42 from main. So let's go ahead
and put that back on here. Now the next thing that you're going to say is, "But you have -O3
turned on and you're right, I do have -O3 turned, on so let's just go and take this back to -O0,
and now what we can observe is that the compiler sets up stack space. It copies the value 0 into
this location and then takes that same location and stores it in rax, I don't know why it's doing
this level of indirection, I guess because we're going through a reference here - and then it takes
that memory location and it stores the value 42 in it, and then it returns zero from main
because const values cannot be mutated. It is undefined behavior if you do that.
So as we can see here in the C++ standard any attempt to modify a const object during
its lifetime results in undefined behavior, and there are some examples here in the standard
but they're terrible examples. They don't illustrate the point that I'm trying to make,
that is, when you actually run them through compiler explorer.
So here you have it, this is very clear-cut case that if we try to modify this object that was
declared const, that we are getting the zero returned and we are getting undefined behavior
from our compiler here; but you say, "I must use const_cast because I work with non-const
C APIs." Well, to that I say, you can't trust any API
that does not say that the parameters are const to not modify your code; and we can actually,
you know, take a look at some of these things. So I'm going to start by just poking around in
the POSIX API a little bit, and we can see here for functions like "opendir" and "scandir," POSIX
is const correct. If you need to pass it a const character string that it's not going to modify,
then it expects a const character string. There's no real confusion here.
So similarly, if we dig into the POSIX API to this function "basename," which is meant to
return the last component of a path name, we see that the char *path is not const.
Well, why is it not const? It turns out that if we actually dig in here we can see
that the "basename" function may modify the string pointed to by path. It doesn't say why,
it just says that it may, so it's a function where you might think, "Oh, I'll just cast
away const-ness here because there's no reason for it to modify the basename," but as we've already
seen the POSIX API is const correct, and if we look at our C libraries such as "strncmp," we can
see that this is in fact also const correct. It is expecting const character pointers to things.
So then the next thing that you might say is, "Uh, but I actually use the win32 API." Now the naming
conventions in the win32 API are a little bit annoying but we can see here that "LoadLibraryA"
for example takes an LPCSTR, so it is also const correct. If you are using library functions from
win32 that are not taking a const pointer to a thing, then it might modify it. The first excuse
that you might give me for using const_cast is that you're working with libraries that are not
const correct. Now that should not be true on any operating system API. We've just covered POSIX
and the C standard and win32 examples. If this is in fact false for your operating system, whatever
you're using, that it does not have const correct APIs, then you are going to have trouble because
you don't really know if any particular function may or may not actually modify the parameters
passed to it. Now the other possibility is not the system libraries, but your own internal libraries,
in which case I suggest that you strongly consider start making these things const correct. We have
had const for, um, I don't know, 40 years now or something. It's probably time for your libraries
that you use internally to be const correct. Now the next thing that you might say to me is,
"But I need it for the Meyers pattern of implementing both const and non-const versions
of functions in my types." Now I think that that particular use case is actually quite outmoded. It
should not exist anymore. We have better options as of C++14. So let's just say I've got (you
know honestly I often have a very difficult time finding a use case that really makes sense here,
so I'm just going to go with the simplest possible one) I've got some private variable
and I have to implement both versions like this. Now in the world where you're going to say,
"I can use const_cast safely here," what you might do is from the non-const version that is from
this version right here call the const version, and you think that you can safely do this because,
well, you know that you're being called via non-const pointer to this, and you end
up with something that looks kind of like this, although I'm sure there's a better way to do it.
What you're basically saying is, I want to take myself
and, well a const_cast here is unnecessary because I'm adding const,
but let's just say I'm doing a static cast. So I'm adding const to "this",
calling the const version of "get_i" and then removing the const-ness from the returned value,
which I can safely do in this particular case but I would offer to you instead this version:
I'm going to go ahead and implement this in C++14 instead of C++20 which I would like to.
Now I don't know if I've ever covered a decltype(auto) in this show before but the
basic premise is kind of like the inverse of perfect forwarding. You're saying "Return back
to me the exact type returned by the function that I'm calling." Now I'm wrapping this one
in parens because I want to make sure that this value "i" actually becomes an expression and not
just an identifier. So the expression this thing is going to be a reference to whatever this is,
and the decltype(auto) is going to automatically deduce this and we have "self" passed in. Now this
capital "Self" here is the type that was passed as my templated type, and this is going to be
a const, non-const, volatile, const volatile, whatever the case may be, or Rvalue reference or
whatever - you could go as deep as you wanted to with this, and I could have made it not a pointer,
but a reference and anyhow; so I'm passing in "this," deducing the type here, deducing
the return type here, and perfectly deducing the return type here. So this is in some ways
slightly more complicated but in other ways it avoids the const_cast gives us a nice super clean
place to put our common shared implementation and our presumably much more complicated than
this example. Then let's just verify that this does in fact work like we wanted to.
Yeah, okay, this does compile just wanted it to make sense. We're still at
-O0 from our examples earlier, but yeah we don't really expect anything to be happening
here. This is all just going to go away, so that is one way to avoid const_cast. If you
for some reason need to call in to a function that you don't know or trust and it's like,
it's some function that takes a character pointer, and they say they promise not to
modify your values. I really truly think the only safe way to actually call a function like this,
I think this is necessary for safety and correctness
if from your perspective this data is all logically const but you don't have any way
of trusting the function that you're calling, you might have to do something like:
So .data was added at some point for both string and vector, so we can say, "Give me the underlying
data here." So added for both string and vector here, and we might have to make a throwaway copy,
and you might end up in the situation where you say, "I have to make a throwaway copy
because I can't trust the other function that I'm calling to not modify my data."
We have no idea where this is coming from,
I mean, you know, just consider, and this is the kind of thing that sometimes people will do,
you know, you want to try to call the function like this. You can actually disable warnings and
stuff that allow this, but ISO C++ forbids converting a string to a character pointer
that says write-strings. Every compiler has some way of disabling this because of old, old code
like, really old code like, 35, 40 year old code.
I would strongly recommend not doing this because if it does try to modify this data then you can
end up in a really bad place. So yeah, basically there's never a reason to use const_cast, or
very little reason, and I will show the one place where I think maybe it sometimes makes sense
to use const_cast, but I'm going to caveat this with a lot of caveats.
Okay let's take a quick look at this code. This is going to be like a correct by construction kind of
idea. Now if I've got a pointer to some integer here and I want to create this "int_ptr_holder"
and I'm creating it via a pointer to this integer, now I'm going to call this function
this constructor here that has a non-const pointer to int, and I'm going to properly set my
local value saying that this thing was not const. Then if someone tries to call "get_mutable_data,"
I expect the compiler to actually be able to see through all of this. Yeah, so I'm returning 15
from main here because I called the version that has a non-const pointer in the constructor. I
properly keep track of this. Now if I were to make this thing const and construct it with a const
pointer, then yeah, compiler goes kablooey. If in some very rare cases you can know whether or not
the data was constructed as const, or know if it was constructed as mutable, and you need this
like super efficient storage kind of thing, then you have to make sure you get it right.
The easier thing to do, to get right, is to actually have two different storages here.
This doesn't require any kind of runtime check necessarily, although the caller
of the code would have to check this, and it makes it a little bit easier to make sure that
you keep everything you know, actually correct in the tracking of the code.
So even here in this case where I would say, "Okay you can use const_cast when you know for a fact
that the data was not constructed as const," then it might be okay.
So that's what I have to say. If you use const_cast in cases where you know
and can prove that the object was not created as const, then you are okay.
This should also be strongly avoided because it is very hard to get right. So stop using const_cast.
Do the correct thing in your code, and if it's because of internal libraries, fix those
internal libraries. Thank you for watching this episode of C++ Weekly and be sure to subscribe!