(light music)
(keyboard clickking)
Frank: In this video, we'll look at what it means
for an enumeration to be unscoped.
In the last video,
we looked at the structure and syntax of an enumeration
and said that an enumeration's key defines its scope.
So far we've only seen the singular enum key used
and so you won't be surprised to hear that this key
implements an unscoped enumeration.
But what does it mean for an enumeration to be unscoped?
When an enumeration is unscoped,
its enumerators are unqualified
and they're visible throughout the entire scope
in which the enumeration was declared.
This means that if we want
to use an enumeration's enumerators,
it's not necessary to specify
which enumeration the enumerators belong to.
Seems great, right, less work for us.
But as we'll see later,
this can sometimes cause issues
and this is why scoped enumerations are so useful.
In the meantime, let's look
at some common uses of unscoped enumerations.
One of the most common uses of enumerations
is as conditionals and control structures
such as if and switch statements.
As we saw with the rocket launch example,
by using an enumeration to represent the different states
and sequences of the launch,
we can create a readable control structure
using if else statements.
Now suppose the ordering of control is reversed as seen here
so that the first condition evaluated
is whether the state is Nominal
and the last condition evaluated
is whether the state is Engine_Failure.
Can you think of any reason
why this series of if else statements
might not be the best implementation
for our control structure?
To understand why we might want to implement
a different control structure,
let's imagine that the state returned
from the function get_state is Engine_Failure.
Obviously we want to initiate the abort sequence
as quickly as possible.
But with the current implementation,
we must first evaluate whether the state is Nominal
and then evaluate whether the state is Inclement_Weather.
Then only after each of those two states have been evaluated
can we determine that the state is Engine_Failure
and finally, initiate the abort sequence.
I know what you're probably thinking.
The extra time taken has to be negligible
and for small control structures
like this, it's certainly true.
But what if instead we were evaluating 100 different states
and again, the Engine_Failure state is evaluated last?
That extra time taken in this case
might not be so negligible.
You may think to simply reorder the condition evaluations
of the control structure,
but we often want equal access time
for each of our control evaluations
meaning we want the Nominal state to be evaluated
as quickly as possible,
as well as the Engine_Failure state and so forth.
So is there a way we can do this using if else statements?
Unfortunately not, but we can do it using switch statements.
Switch statements are by far
the most popular control structure used with enumerations.
This is not because they provide better readability
than if else statements,
but also because for five or more cases generally,
they're implemented as jump tables
which provide equal access time for all cases.
This means that regardless of where we position the Nominal
and Engine_Failure states in the control structure,
they'll be evaluated in equal time.
It should be noted though
that we can only use switch statements with enumerations
so long as the enumerators we use
as switch cases have unique values.
Otherwise the switch will not be able
to discern between its cases
and the code won't compile.
As we saw with our accidental launch scenario,
we might want the user
to initialize enumeration type variable.
Unfortunately we can't use the standard cin command
with the extraction operator
because the extraction operator has no knowledge
of what a State type variable is
or how to deal with it.
Thankfully we have two methods to deal with this.
The first is to extract the user input value into a type
that the extraction operator does know about
and then cast it to the State type variable.
The second is to overload the extraction operator
to deal with State type variables.
Let's take a look at both of these.
Here we can see the first method
which reads the user input into a temporary variable
which is then cast to a State type variable.
When we declare the type of the temporary variable,
in some cases the underlying type of an enumeration
might not be obvious,
so we can use the class template,
underlying_type_t, to deduce it.
In this case, the underlying type
of the state enumeration is integer,
so the type of the user input variable will also be integer.
It's important to note
that when we cast variables to enumeration types,
the value of the variable does not change.
This means that there's no guarantee
that the cast state enumeration type variable
will have a value corresponding
to the enumeration's enumerators.
For example, if the user were to enter the value three,
it would be cast
to the State type variable state with no issues.
Obviously this is not what we want
since three does not correspond to any state enumerator.
Our only recourse in this situation
is to implement our own checks
to ensure that the variable we're casting
has a value corresponding
to one of the enumerations enumerators.
This is usually done by using a switch statement
with the condition being the variable to be casted
and the cases being each of the enumeration's enumerators.
There are many ways to perform these kinds of checks,
but the important thing to remember
is that you're responsible for ensuring
that a variable cast to an enumeration type
will have a valid enumerator value.
In an example like this,
you may want to set the output stream to a fail state
in the case of an invalid state.
The second method of reading
user input values into enumeration types
is by overloading the extraction operator.
Using this method, we overload the extraction operator
to take as parameters a reference to the input stream
and a reference to the state variable
that the user input will be read into.
It's important that we declare
an enumeration in the same scope
as any function referencing
the enumeration or its enumerators.
This is to ensure that the enumeration type
and its enumerators can be accessed
from within the function.
In this case, we've declared the state enumeration
in the same global namespace
that contains the overloaded extraction operator function.
Within the function, again, we have declared
a temporary user input variable with the same type
as the underlying type of the enumeration.
Next, the user's input is extracted from the input stream
and assigned to the temporary variable
which is then checked to ensure that its value
corresponds to a valid state enumerator.
If it does, the temporary variable is cast
to the reference state variable.
If it doesn't, the user is displayed a message
informing them that they have not entered a valid state.
Finally, the input stream is returned
to the calling function.
As you may have noticed,
this method is identical to the first
but this time we've implemented it
as an overloaded extraction operator.
This is so we can write clean readable code like this.
Now that we've seen how to use cin in with enumerations,
let's switch our focus to see
how we can use cout with enumerations.
To display enumeration type variables,
we can use the standard cout
and the insertion operator.
When we do so,
the enumeration type variable is implicitly converted
to its underlying type
and its value is displayed.
This means that when we display enumeration type variables,
it's always their value that's displayed
and never their enumerator name.
This makes sense since an enumerator's name isn't a string,
but simply a name used
to identify its value within the code.
Sometimes it may be useful to display
the name of the enumeration type variable
and so to accomplish this,
we can implement a simple switch statement
using the variable as the condition
and its cases being the enumerations enumerators.
As you may have guessed,
we could even implement this functionality
by overloading the insertion operator.
In this case, we overload the insertion operator
to take as parameters,
a reference to the output stream,
and a reference to the state variable
whose name will be displayed.
Again, it's important to declare the enumeration
in the same namespace as the overloaded function
so that the enumeration type
and its enumerators can be accessed
from within the function.
Within the overloaded function,
the reference state is compared
to the enumeration's enumerators
and the appropriate enumerator name
is inserted into the output stream.
Finally, the output stream is returned
to the calling function.
By overloading the insertion operator
to display the names of State type variables,
we can write clean, readable code like this.
Now now that we've covered some of the most common uses
and pitfalls of unscoped enumerations,
let's head over to the IDE
and see some more examples.
Okay, so I'm in the IDE.
I am in the Enumerations workspace
and I'm in the UnscopedEnums project.
What I've done in this project is written
three tests that exercise different kinds
of functionalities for unscoped enumerations.
Right now I've got test two and test three commented out,
so we're just running test one.
Now I've already built it
and the output is over here on the right,
but let's look at the code that I wrote
so you get an idea of what's going on here.
So the first thing I did was I created an enumeration
and it's called Direction,
and you can see it right there.
Notice that it's unscoped
and the enumerators are North, South, East, and West
and as we know, this gets a zero, this gets a one,
this gets a two, and West gets the three,
and that's automatically done by the compiler.
Now the first thing we wanna know
is if we try to create another enumeration,
let's say you call it Street
that models different kinds of street names, for example,
and we've got Main, North Street, Elm Street, and so forth.
We can't do this because we've got North twice
and the compiler's gonna say
I don't know what you're trying to do.
You're trying to redeclare that.
So this is one of the limitations
as I mentioned in the slides with unscoped enumerations
and we can handle that with scoped enumerations
in the next example.
That's where we're at here
and let me just show you what's going on in this example.
Here I wrote a function.
The function is called direction_to_string.
You can see the name of the function right there.
It expects a direction parameter
and depending on the enumerator of that direction,
we're going to return a string.
It's either going to be North, South, East, or West
depending on what the case
of that enumerator is right there, okay?
And as you can see, it's returning a string.
So this is just a real handy utility function
where you pass in a direction
and it's going to return North, South, East, West,
or unknown direction as a string.
So we've got that
and here's the test.
The test is really straightforward
and you can see the output over here on the right.
So let's walk through this.
You can see exactly what I'm talking about.
So the first thing we're doing is we're outputting tests
and that's the output you see right there,
and we're creating a variable here called direction
and that variable is of a Direction type.
Now remember when we create an enumeration,
we're creating a new type in the system.
So this is very similar to doing something like int i
and initializing it to zero let's say in this case.
So we're using a type,
we're creating a variable,
and we're initializing it.
Same thing, we're using the type,
a new type that we've just created
when we created that enumeration.
There is the variable name
and there's the initializer, okay?
So that's it, I've initialized it to North.
Remember what we had originally here,
we had North was zero, South was one,
East was two, and West was three.
So right now that enumerator value is zero, right?
That's the underlying value.
So we're gonna display Direction
and then we're just gonna pass in
direction to that overloaded operator right here.
As far as that's concerned,
this is an integer.
Now scoped enumerators work very differently,
but let's just talk about unscoped here.
So this is going to display zero
and that's what's happening here.
Then I'm calling direction_to_string,
I'm passing in direction.
That's going to convert it to a std string
as we saw in the function a minute ago
and it's gonna display North.
If we set the direction to West,
it's going to do exactly the same thing.
It's gonna display three
and then West in this case.
If we try to set the direction to five, for example,
we're gonna get a compiler error
because five is not valid.
Be very careful when you're casting enumerators.
You could cast things that aren't there
and that's a problem.
So here's an example here.
I'm casting, remember direction is my variable.
I'm casting 100 to a direction type.
The compiler assumes you know what you're doing.
Remember right now all we've got
is zero, one, two, and three in there, right?
North, South, East, and West.
But if you're doing a cast like this,
the compiler says okay.
You know what you're doing,
so I'm gonna let you do it.
And notice what displays displays, 100,
and then when we try to display the to_string,
we get unknown direction.
And this is very, very similar to doing a static cast,
so this is what's happening here.
I just wrote this here
so that you can understand what's happening.
We come up here
and you guys could try this out on your own.
If we come up here to the top
and we do something like North is one, South is 10,
and then East and West would get 11,
then when you display these values here,
you'll get different displays.
Okay, so that takes care of test one.
You can play around with it and modify things,
so let's switch over to test two.
Okay, so I've gone ahead
and commented out test one
and uncommented out test two here,
and I've rerun the program
and this is the output you can see over here.
So now let's talk about what's going on in test two.
Let me scroll back up to test two.
So here's the enumeration
that I'm using for the test two example.
In this case, I'm modeling a Grocery_Item.
That's the enumeration I've created
and my enumerators are Milk, Bread, Apple, and Orange
and again, behind the scenes
we're getting a zero, a one, a two, and a three.
And if we've got specific codes that we could use
and I believe I'm doing that
a little bit later in the scoped enumeration.
So let's say that Milk is 350
and Bread might be 150 or something like that.
Those might be internal codes that we're using
that represent the code for Milk,
the code for Bread and so forth.
So if we need something like that,
that's perfectly valid to do.
But in this case,
I'm just gonna use the implicit initial
is zero, one, two, and three.
Now rather than having a function called to_string
like the one we created a little bit ago in test one,
I wanna overload the insertion operator
'cause I want to be able to take these enumeration variables
and just put them right on an output stream.
And much easier than having to call a function
and it blends right into the way C++ feels.
So in this case, I've covered this in the class.
If you're not familiar with overloading operators,
there's a whole section in the course about it.
So in this case, I'm overloading the insertion operator
and I'm inserting a grocery_item
or the string representation of a grocery_item
into an output stream.
So really simple, right?
There's the grocery_item, I'm switching off of it.
We could have used an if else ladder if we want,
but in this case, I'm using a switch.
I'm switching off of it.
If it's Milk, I'm putting Milk on the output stream.
Bread, Apple, Orange, if it's something else,
I'm writing invalid object
and I'm returning the output stream.
And now we also have a little test function
here that I've written
which can come in really handy.
It's called is_valid_grocery_item
and it returns a Boolean, right?
A true-false value, so given a grocery_item,
is it a valid numerator?
Well, it is a valid enumerator
if it's one of the ones that I've defined, right?
Milk, Bread, Apple, or Orange.
So in the case of any of these, I'm returning true.
Otherwise I'm returning false.
If I return false
and I've got something else in there, right?
I've probably cast it to something.
And now I want to display a grocery list
and that's what test two is all about,
and test two's down here.
I'll show you what that looks like in a moment.
But the idea with this function
is the function's called display_grocery_list
and it expects a std vector of Grocery_Items.
So basically a collection of Grocery_Items
and those guys are enumerations.
So all I'm doing is I'm looping,
you can see right here I'm using a range-based for loop.
I'm looping over that grocery_list right here
and for each grocery_item, I'm displaying it.
How am I displaying it?
I'm using that overloaded operator
that I just overloaded a moment ago
and I'm also keeping track of how many invalid items
and how many valid items I've had
using my is_valid_grocery_item function that I wrote.
So that's all we're doing.
We're looping through each element in that list,
we're displaying it, and then we're checking
was it a valid item, yes or no,
and keep counters for those
and then at the end, we're just displaying some information.
And now let's look at the actual example
which is right here
and in this case,
this one's actually pretty interesting
'cause you can see I'm creating my shopping list here
which is a std vector of Grocery_Items
and I'm pushing back Apple, Apple, Milk, Orange.
Those are all valid enumerators, right?
The std vector can contain those
because it contains Grocery_Items.
What we're gonna do is we're gonna add them to that vector.
That's what that's doing.
That's what the pushback does.
In this case, if I try to say something
like Grocery_Item item is 100,
I'm gonna get a compiler error.
The compiler won't let me do that.
However if I create an integer called Helicopter
or some other silly name
and initialize it to 100
and I push that,
it's gonna let me push it
but it's gonna be an invalid item when I display it.
In this case, if I create a Grocery_Item
and initialize it to zero
and this is a cast,
that's gonna be Milk, right?
Because Milk was the zero value enumerator
in that enumeration, so that's what we're doing.
We're just pushing some items on there
and then I'm displaying that list
with the function I just showed you.
So when this runs, this is what we get.
We get Apple, Apple, Milk, Orange
and you can see them right here.
Apple, Apple, Milk, Orange.
Then we're gonna get an invalid item
which is this guy right here, the Helicopter,
and then this guy is zero
so we're gonna get Milk again.
And since we're keeping track of valid and invalid items,
you could see here that we've got six total items,
five are valid and one is invalid,
and that would be this guy right here, the Helicopter.
Okay, so that covers test two
and again, play around with this, change some things,
try some things out,
and you'll really get a feel for it.
The important thing to understand here
is just how much more readable this code becomes.
Okay, so finally we're in test three.
Okay, so let me show you what's going on in test three.
In test three, we've got an enumeration state,
that is the state
which is either Engine_Failure,
Inclement_Weather, Nominal, or Unknown.
Now this is a common thing
that you'll see a lot of times in enumerations,
some sort of unknown or unspecified state.
A lot of times it comes in real handy
because sometimes we get strange values
and it's nice to assign them to a known state,
in this case an unknown state.
So that's that, then we've got a sequence
which is the Abort sequence, the Hold sequence,
or the Launch sequence.
Depending on what the state is,
we execute a specific sequence.
So what we've done here
is we've got our stream extraction operator
now that we've overloaded
and this is pretty neat
because it allows the user to enter something,
and we're gonna create a state variable from it.
So in this case, we're gonna use the underlying type
and you can see that right there.
That's the underlying type.
We know that the underlying type
for this enumeration is an int.
We could have used int user_input just like this,
but it's much better to use the underlying type
because it could change in the future if someone
assigns some initial enumerator value
to one of those things that's not an int
and what we're doing is we're reading that user_input
from the input stream.
We're switching on it
and depending on what it is,
in this case, if it's one of those things
that we know about, right?
Engine failure, Inclement_Weather, Nominal or Unknown,
we're casting that user input to a State type
and assigning it to state which is that guy right there.
Otherwise we're gonna assign it to the state Unknown
because they've obviously entered something else
and the idea being if it's in an Unknown state,
we're not gonna launch this rocket, we're gonna abort.
So we've got those three sequences you can see up here,
Abort, Hold, and Launch.
Here we're overloading the string insertion operator
just like we did before,
but for sequence in this case.
So given a sequence,
if it's abort, we're just going to display Abort, Hold,
or Launch depending on what the enumerator value is.
And then in the case of initiate
when we're calling initiate with a sequence,
it's just gonna display the sequence that's being initiated
just so we can actually see it.
And this is going to use
the overloaded stream insertion operator right there.
Okay, so here's test three.
So take a look at this, it's pretty straightforward.
I'm creating my state variable right here
and I'm reading that from the user.
Now this is using our extraction operator
that we just overloaded.
You can see how neat this is
because this feels just like anything else in C++, right?
This is how we read integers.
This is how we read strings.
This is how we read a lot of things.
However this is a user-defined type that we created, state,
so we have to overload the extraction operator
so that we can write code like this.
Depending on whatever stake the user entered,
if they entered Engine_Failure
or if it's Unknown, right?
They entered something else,
we're going to initiate the abort sequence.
If it's Inclement_Weather, we're gonna hold
and if it's Nominal, we're going to launch.
So let's run this
and I'll put in a zero which is Engine_Failure
and we're initiating the Abort sequence, right?
That's what we want.
So let's try it with another one.
Let's run it again.
In this case, we'll put in a one
and we're holding because one is Inclement_Weather.
That's pretty cool, let's try it again.
Let's put in a two this time
and let me move this over here.
We're gonna put in a two, we're launching, right?
Because two is Nominal.
And now let's put in something like 100 or something, right?
Something that's definitely not
a valid enumerator for the estate.
So let's run this
and let's put in 100,
and what we want is we want to abort.
And that's exactly what we get.
The user input is not a valid launch state,
so what we're doing is we're setting the state to Unknown
which it initiates that Abort sequence right here.
This is all well and good,
but what's really important that you understand
is look at that code.
Look how readable this is.
We're switching on the state.
If it's Engine_Failure, Unknown, we initiate Abort.
We're using these enumerators which is so important.
Instead of just having magic numbers
and all kinds of weird things in our code
that make things very, very difficult to understand.
Okay, so I encourage you to play around with this code,
change things around, understand it.
The best way to learn this is just to get in that code
and fiddle with it, and change it around a little bit,
and add some new states,
and try different things.
In the next video,
what we'll do is we'll talk about scoped enumerations
and we'll see how much better they actually are.