Hello and welcome to C++ Weekly. I'm your host, Jason Turner. In this episode I'm going to cover
something that is actually quite foundational to using and understanding C++, and that is
'const' versus 'non-const' member functions. Now there's also volatile overloads, which I
might get into, but probably not, so if you just want to, you know, learn more about C++,
if you are perhaps a beginner to the language, this will be a great episode for you.
In the next episode I'll go into a little bit more detail about how this actually works though.
This episode of C++ Weekly is sponsored by me. Be sure to check out the video description for
important links about upcoming events, sponsoring the channel with Patreon, buying copies of my Best
Practices and Puzzler books and how to contact me if you are interested in on-site training or
remote code reviews. Okay, so like I said, this is a very fundamental intro to C++ kind of concept.
C++ has member functions for our structs and our classes. Now it is worth noting
that the only difference between a struct and a class is the default access level.
I'll probably dig into this a little bit for the sake of this episode, which I don't normally
tend to talk about in this YouTube channel, but I default to struct for all of my examples because
it just makes the slideware, if you will, a little bit shorter and easier to look at.
Now the very first thing that I want to comment on is that this is a terribly named structure.
If you've got a single letter name for your struct then you should probably give it something better,
so let's just pretend like this is going to be called 'string'. So I'm going to make something
that kind of looks a little bit like a string but not at all because we're not going to implement
all of the gory details here. So if you're new to C++ you might do something like this:
We can debate all day about whether or not
it makes sense to call this thing 'm_size' or whatever, I don't care. That's really not the
point of this, but you can't have two different return types on your function now can you? Okay.
Now this is a function that should be returning a value and it is not,
and we've got our warnings turned up pretty high here so we're going
to get this "no return statement in a function returning non-void." Very good.
So I'm just going to go ahead and make this thing return 'size'. Let's go ahead
and create an object of my type 'string' here, and I've just created the string on the stack,
and I want to access its values. Let's go ahead and return its current size
and we're going to get a conversion warning here.
You know what? It's kind of actually worth pointing out because if the size of the string
is more than the size of a signed 32-bit 'int', which is, you know, like 2 billion characters
then we wouldn't get the return value that we expect from this; and so that's, you know,
it's an argument worth pointing out. So let's not return it and deal with this warning.
Instead let's go ahead and print it, and I'm using the excellent lib 'format' of course here.
Okay. String size is zero, and it is printing what I wanted it to. If you are a fan of any of
my Best Practices books or any of the talks that I have given on C++ Code Smells and the way that
I admonish you and encourage you to use 'const' wherever possible, you might be tempted to say,
"Well this is a 'const' string," and it is. I haven't modified it in this code,
but now we're going to see a compile error occur, and it says I am "passing 'const string' as this
argument discards the qualifiers." If you're new to C++ this error isn't going to make
any sense to you. What it is telling you is that you are calling a 'non-const' member function
with a 'const' object, and 'size' should never modify the thing that it's querying. It would be
extremely strange if we wrote code that did this - if it said, "I am going to decrement the size
every time you call the 'size' function." So let's go ahead and leave that here,
and let's go ahead and add 'const'. So now when I add 'const' I'm going to fix one error and create
another error. So you'll notice that the error down here went away, and this is what we wanted
because I am calling a 'const' member function on a 'const' object, and that's good.
Now this 'const' saves us from ourselves because the compiler is warning us that I am
trying to "decrement a read-only object." Oh no! You're right, compiler. Why in the world would I
try to modify something when the user is simply trying to query the size of the string?
So I'm going to go ahead and remove that, and now everything works as expected;
and in case you are curious, we can remove the 'const' here
and the code still compiles because we can call a 'const' member function on a 'non-const' object.
This has always been allowed, and I will demonstrate that. It's doing this effectively:
That's allowed with no problem.
We're going to get errors from the compiler here saying that we've created
objects, variables, that is, that we're not using. It's this unused
value, "unused variable" error here, from the compiler. Let's just go ahead and say,
"That's intentional, compiler in this case." I am not planning to use that so that we can illustrate
the point. So I have no problem creating a 'const' reference to a 'non-const' object
or a 'non-const' reference to a 'non-const' object. Now if I go back to making this 'const'
I will get an error on line 16 because I cannot have a 'non-const' reference to a 'const' object.
(Comment that out for now.) So I think at this point you can see how 'const' is useful - it
signals to you and to the compiler and to the future user of your code that this member function
is not going to modify the object that it is being called on, and that's good. We want to know this.
It has other implications - very, very rarely performance implications. That's really not the
point at all here. This is the point about communicating the intent of the code. If,
by the way, before we move on, I convert this to a class, I am now going to get a different compile
time error because this is now private. So let's just go ahead and make this a little bit more like
what a real string object might look like. There. I am clearly specifying the things that I want to
have public access and the things that I want to have private access. Let's look at iterator.
For the sake of illustrating this, I am going to turn off code execution
so that we don't have to actually implement every detail.
This is not actually too far off from how a standard string is actually going to work.
It's going to effectively return a pointer to the beginning of the internal data storage
when you call 'begin', and similarly we're going to have an 'end' function.
By simply having these two things it's actually possible to do a ranged 'for' loop,
but this code is not going to compile for a couple of reasons. The main problem is we have not given
'const' access here, we've only given' non-const' access. So I can change these to 'const'
so this compiles, but now we can only access it in a 'const' way.
So if I'm going to go ahead and do this, and let's say I want to get a reference to it,
this code's still going to compile because this is going to be deduced as a 'const' reference;
but as soon as I want to modify it, let's just say, I want to set everything in the string to
the character 'a' now, I'm going to get... I did not get a compile time error. The reason I did not
get a compile time error, the astute of you may have observed, is that I did not correctly specify
the return type of my 'const' iterator 'begin' and 'end'. They need to return a 'const' pointer back.
Okay. So now we have "assignment of a read-only reference 'character'."
We need some way of being able to do both of these things. If I take 'const' away here
I'm still going to get a compile error. So the solution is the fact that 'const' and 'non-const'
member functions can act as overloads. So if I call a member function on a 'non-const' object,
I'm going to get these versions. If I call it on the 'const' object,
I'm going to get these versions. You'll see this throughout the standard library in 'string' and
'vector,' any of our containers. They have 'const' and 'non-const' overloads for all the common
accessors that we would need where we need to be able to get either a 'const' reference or
a 'const pointer' back to a thing, OR a mutable pointer or immutable reference back to the thing.
Now as I'm sure will be pointed out to me if I do not point this out right now,
there is the "deducing 'this'" from C++ 23 that simplifies some of the implementation
details here, but I wanted to be able to show you the difference between a 'const' and a
'non-const' member function. Let's just do one quick illustration of this actually executing.
Okay. So I am calling 'print_string' on a 'non-const' object, and I am getting the string
"non-const" printed. If I mark this 'const', then I should get the string "const" printed here,
and I do. "const" is printed. So there you go. I believe that I have covered in a nutshell
the difference between 'const' and 'non-const' member functions and their utility,
why you might need to implement both of them;
and I covered a little bit just about access levels between public and private in struct and
class definitions in C++. Everything that I have covered in this example goes all the way back to
C++ 98, so there's no fundamentally changed concepts here in any recent versions of C++.
In the next episode that I'm getting ready to record, I will cover how the compiler actually
implements these things, like what does it actually mean that a member function is 'const'
or 'non-const', and we saw just a little hint of that in one of the error messages that we got.