In this video, we'll look at what it means for an enumeration to be UN scoped.
In the last video we looked at the structure and syntax of an enumeration and said that an enumerations
key defines its scope.
So far we've only seen the singular enum key use, and so you won't be surprised to hear that this key
implements an scoped enumeration.
But what does it mean for an enumeration to be on scoped?
When an enumeration is scoped, it's 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 enumerations enumerators, it's not necessary to specify which
enumeration the enumerators belong to.
Seems great, right?
Let's 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 scoped 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 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 L 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 state returned from the function gets 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 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 only because they provide better readability than EPFL statements, but also because for
five or more cases generally they are 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 the enumeration 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 C in 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 enumerations 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 tested
and the case is being each of the enumerations 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 failed 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 it 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 see in with enumerations, let's switch our focus to see how we can use
see out with the durations.
To display enumeration type variables, we can use the standard, see out 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 there value that's displayed
and never there enumerator name.
This makes sense since an enumerators 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 enumerations 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 that we've covered some of the most common uses and pitfalls of UN scoped enumerations, let's head
over to the IDE and see some more examples.
Okay.
So I'm in the ID, I'm in the enumerations workspace and I'm in the UN scoped E Nums project.
What I've done in this project is written three tests that exercise different kinds of functionalities
for UN scoped 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 in.
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 a enumeration and it's called direction and you can see it right
there.
Notice that it's un scoped and the enumerators are north, south, east and west.
And as we know, this gets a zero, this gets a one, this gets two and West gets the three and that's
automatically done by the compiler.
Now the first thing we want to know is if we try to create another enumeration, let's say, called
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 is going to say, I don't know what you're trying to do.
You're trying to declare that.
So this is one of the limitations as I mentioned in the slides with UN scoped 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.
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 test 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 initialize here.
So that's it.
I've initialize the north.
Remember what we had originally here?
We had north was zero, south was one, west, east was two and west was three.
So right now that's enumerator value is zero, right?
That's the underlying value.
So we're going to display direction and then we're just going to pass in direction to that overloaded
operator right here.
As far as that's concerned, this is an integer now scoped numerators work very differently.
But let's just talk about scoped here.
So this is going to display zero and that's what's happening here that I'm calling direction to string.
I'm passing in direction that's going to convert it to a string as we saw in the function a minute ago.
And it's going to display north.
If we set the direction to West, it's going to do exactly the same thing.
It's going to display three and then West.
In this case, if we try to set the direction to five, for example, we're going to 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 am casting a direction as 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, I know you know what you're doing,
so I'm going to let you do it and notice what this place displays 100.
And then when we try to display the two 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, you know, South is ten and then
east and west would get 11.
Then when you display these values here, you'll get different displaced.
OC 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 uncommon two doubt 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 numerators are milk, bread, apple and orange.
And again, behind the scenes, we're getting a01, two, 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 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 going to use the implicit initial 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 want to overload the insertion operator because I want to be able to take these enumeration variables.
They just put the 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 is the grocery item.
I'm switching off of it.
We could have used it if else matter 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 enumerator?
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, 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 choose down here and I'll show you what that looks like in a moment.
But the idea with this function is the function is called display grocery list and it expects a stood
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 have 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 was actually pretty interesting because you can see I'm creating my shopping
list here, which is a state vector of grocery items and I'm pushing back apple, apple, milk, orange.
Those are all valid enumerators, right?
The steward vector can contain those because it contains grocery items.
What we're going to do is we're going to 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 going to 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 pushed that it's going to let me push it, but it's going to 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 going
to 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.
You can see them right here.
Apple, apple, milk, orange.
Then we're going to get an invalid item, which is this guy right here, the helicopter, and then this
guy is zero.
So we're going to get milk again.
And since we're keeping track of valid and invalid items, you can 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.
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 and 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 enumeration is 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 whole 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 that we've overloaded and this is
pretty neat because it allows the user to enter something and we're going to create a state variable
from it.
So in this case, we're going to 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 going to 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 going to launch this rocket, we're going
to abort.
So we've got those three sequences you can see up here, abort, hold and launch.
Here we are overloading the string insertion operator just like we did before.
So given a C 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 numerator value is.
And then in the case of initiate, when we're calling initiate with a sequence, it's just going to
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 opera 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
state 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 going to 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 will 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 chew this time.
And we move this over here.
We're going to 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 it'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 date.
So what we're doing is we're setting the state to unknown, which 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 the 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.