(soft music)
(air wooshing)
(keyboard clicking)
Frank: Hello everyone.
In the last video we learned about unscoped enumerations.
That is enumerations whose enumerators are unqualified
and they are visible
throughout the scope in which the enumerator was declared.
In this video, we'll learn
about the other side of the coin, scoped enumerations.
That is enumerations whose enumerators are qualified
and therefore they're only visible
by using the scope resolution operator.
This means that
if we want to access an enumeration's enumerators,
we must specify which enumeration the enumerator belongs to.
In place of the singular enum keyword used to
declare unscoped enumerations,
we declare scoped enumerations using the enum class key.
We can also use enum struct
and they are both semantically equivalent.
So you're probably wondering why do we even
need scoped enumerations?
Well, you may remember me saying
that unscoped enumerations can sometimes
cause issues that can only be resolved
by using scope enumerations.
So let's take a look
at two motivating examples so you can understand why.
Here we can see two unscoped enumerations.
The first one contains species of whales
and the other species of sharks.
Now suppose we want to determine
whether a beluga whale is equivalent to a hammerhead shark.
Because both enumerations are unscoped,
this is a valid comparison.
And even worse, it'll evaluate to true
since both enumerators have the same underlying value
of one.
Obviously this is not what we want
since a beluga whale is certainly not equivalent
to a hammerhead shark.
Another issue that can arise
from the use of unscoped enumerations are name clashes.
Suppose that we want to include the blue shark species
in our shark enumeration.
You'll notice that it has the same enumerator name
as the blue whale species,
because both enumerations are unscoped,
the compiler will think we're trying
to redeclare the enumerator blue
and it will generate an error.
Of course, we could just rename the enumerator
to something more specific,
but in general we don't want to have to worry
about naming our enumerators.
So what can we do to address this issue?
As you've probably already guessed,
we can declare the enumerations as scoped enumerations.
The use of if and switch statements
with scoped enumerations is identical to that of unscoped
but with the additional requirement
of specifying enumerator scopes.
In fact, everything you can do
with an unscoped enumeration,
you can do with a scoped enumeration
so long as you specify the scope of its enumerators.
With that said,
there is one difference that's worth discussing.
In this example, we have a scoped enumeration type
named item that might be items
in a grocery or shopping list.
The value of each item might be an item code
or some other value that makes sense in our application.
You'll remember that with unscoped enumerations,
if we wanted to use their enumerator values,
we simply stated the name of the enumerator
and the compiler would implicitly
convert its value to its underlying type.
With scoped enumerations that's not possible.
In fact, if we try to do so,
even with specifying the enumerator scope,
the compiler will generate an error.
This is because scoped enumeration type variables
are not implicitly convertible.
Instead, we must explicitly
cast a variable to its underlying type.
Here you can see that we can
cast the enumerator to the integer underlying type
using a few different styles of casting.
Only then can we use the value
of a scoped enumeration type variable.
Now that we've seen what scoped enumerations are
and how to use them, let's head
on over to the IDE and see a few examples.
Okay, so I'm in the IDE, I'm in the enumerations workspace
and in the Scoped Enums project.
I'm going to close this workspace
for you just to give myself a little bit more room here.
Alright, so what we've got here is just two test examples
that show you different ways to use scoped enumerations.
So I've got them both set up
and I've already built and run the program,
you can see it over here on the right.
And what I want to do is go
over those examples so you can see exactly what's happening.
This first test is very, very similar
to the same first test that we did
in the unscoped enumerations project.
Here we've got our grocery item enumeration.
Now notice that this is now a scoped enumeration
because we're using enum class.
Here I've got milk, bread, apple,
and orange, just like before.
The difference is I've assigned some initializer values
to these enumerators.
And this could represent the item code
or really just about anything you can think of
that could identify the values for these enumerators.
So in this case, we are explicitly defining those values
and the compiler will not generate 0, 1, 2, and 3.
We're going do exactly what we did before
with maybe a little bit of a change.
Okay, so look at some of the examples we've written here.
Here we're overloading the insertion operator again,
the stream insertion operator, just like we did before.
The only real difference is that now,
in order to use those enumerators, we must scope them.
We have to use the enumeration class name, grocery item,
right before the enumerator value
otherwise it's not going to compile.
So that's one of the benefits of using scoped enumerators.
The other thing we're going to do is,
in this case, I'm grabbing the underlying type
of that grocery item value right here,
so all I'm doing is displaying that.
So when we display these values, we're going to display,
you can see up here, apple 132, milk 350 and so forth.
What we're going to do is I'm going to switch
off the grocery item passed into me.
I'm going to put whatever string is appropriate
on the output stream,
and then I'm just going to display the value right
after that colon.
And the value again is one of these guys.
Those are the ones I'm grabbing from the underlying type.
I've got the Boolean function again,
exactly like it was in the previous example with unscoped,
except since it's a scoped enumeration,
we need to qualify those enumerators.
So in this case,
whatever grocery item is passed into this function,
if it's one of these four, then we know it's valid
otherwise we'll return false because it's invalid.
And then the display grocery list function
that I wrote is exactly the same again,
the whole point of this was to create an unscoped version
and a scoped version so you could see the difference.
So in this case, we've got that std::vector
of grocery items just like we did before.
And we're going to loop through that using the range based
for loop right here on line 64 and just display them.
So that's pretty simple.
We'll keep track of what's valid and invalid.
I went over this in more detail in the unscoped video,
so you can check it out there.
And then what we'll do is, we will create some items
and add them to the shopping list, right?
So that's what test one is doing
and you could see what I'm doing right here
and let me grab my pen.
Right here, we're simply displaying test one
and that's done right up here,
there's the output.
I'm creating my shopping list and I'm adding, not apple,
I'm adding grocery item apple.
We've got to scope it.
So I'm adding apple, milk and orange,
and I'm creating a helicopter integer
with a thousand and I'm going to push that guy back in.
Now I can do that here because I'm casting.
So remember, be very careful with casting,
make sure your cast makes sense.
In this case, it doesn't make sense really
'cause I don't have an item with a 1000 code.
Notice that if I try to add helicopter here,
I'll get a compiler error because this is an int
and it's doing a little bit of checking for me.
The shopping list is expecting a grocery item,
not an int.
Here, I'm telling it, "Hey, I know what I'm doing.
So do what I tell you to do."
And that's not always a good thing.
I'm doing the 350 item, which is the milk.
So it's going to add that again.
And when I'm done, I display the list.
And here's the list that displays, I get apple,
I get milk, I get orange, I get that invalid item
which is this one right here on line 93.
I get milk again because I'm adding the item code 350.
And we've got five total items, four valid, one invalid.
So there you go.
That's very, very similar to the example
that I did in the unscoped video.
So now let's check this example, test two.
For test two,
I created a class just to show you
how we can use these scoped enumerators
or really unscoped enumerators inside a class.
So here's an example.
I've got a class called Player
and this would model a player in a video game
or something like that, where they've got a mode
which is an attack, a defense or an idol, right?
Their current mode in the game,
a direction that they're facing, North, South, East or West.
And they've got a name.
So you can see those attributes of the class right here down
at the bottom, right here with lines 129 to 131.
So again, you can see that the player has a name,
it has a mode of attack or defense,
whatever their current mode is,
and the direction that they're facing in the game.
So those are the attributes.
And if I scroll back up, we've got a constructor here,
just like we've done before, nothing new.
We've got our getters in our setters for the name,
the mode and the direction.
So let's write a couple of utility functions.
The first one gets me the player mode.
So if I pass in, not a player,
but if I pass in a player mode, it's going to return attack,
defense or idle as a string.
That'll be a really handy function
that I can call whenever I need to display that,
for example.
I also have a function that's going to
give me the player direction
and it works exactly the same way.
It expects a player's direction, not a player,
just the player's direction.
And it returns the std::string here
and it's going to return North, South, East
or West depending on that enumeration.
Since it's a scoped enumeration, I need to qualify it.
And then finally,
I've overloaded the stream insertion operator,
and this is where those functions
that I just wrote will be called.
So stream insertion operator,
if we're passing in a player object by reference
in this case, by construct reference as we're supposed to,
this is simply going to display player name
and then it's going to get the player's name,
then player mode,
and it's going to get the player mode that function we wrote
and it's going to pass in the player's mode
rather than the entire player.
We don't need to pass
in the entire player to display the mode.
And we're going to display player direction followed
by get player direction and p.direction.
We'll put an end line at the end
and we'll return os, and now we'll be able to use that.
So in test two, you can see the test.
It's really, really simple here.
I'm creating three player objects right here
on lines 190 to 192.
The first one is p1, it's cloud strife.
It's current player mode is attack
and its current direction is facing North.
And you can see
how we're using those enumerators right here.
Also, you can see the way the scope works, right?
it's not just mode attack, it's player mode attack
because that enumeration is defined in the player class.
So it's defined publicly
within the scope of the player class.
So we need to provide all the information
that the compiler needs to do this.
But look at the readability, again,
that's very readable, right?
Player one, player two is Tifa Lockhart
in defense mode, facing West.
This is so much easier
than having magic numbers again in there.
And the compiler's helping us out,
it's checking for errors.
So once I define those three player objects,
I can display them really simply
by just putting those player objects right
on the output stream here with the insertion operator.
And this is what we're going to get
as the output right there.
Player name, Cloud Strife, attack mode North,
just like we did here.
Tifa Lockhart, defense West, and Sephiroth, Idle, South.
And you can see that display right there.
Now, I didn't implement test three
which is the rocket launch example.
I'll leave that to you as an exercise.
That way you can translate it
from an unscoped to a scoped enumeration
and play around with that and have some fun.
So that covers enumerations.
As you can see, at their core, they're very, very simple.
Using them involves a lot because we can use a lot
of advanced features in C++.
We can use them with all kinds of control structures.
We can use them inside classes.
We can overload operators.
We can get underlying types.
We can do all kinds of really cool stuff.
The big benefits, readability.
Also, let the compiler help you find mistakes.