EXPLORATION 65

image

Enumerations

The final mechanism for defining types in C++ is the enum keyword, which is short for enumeration. Enumerations in C++ 11 come in two flavors. One flavor originated in C and has some strange quirks. The other flavor addresses those quirks and will probably make more sense to you. This Exploration starts with the new flavor.

Scoped Enumerations

An enumerated type is a user-defined type that defines a set of identifiers as the values of the type. Define an enumerated type with the enum class keywords, followed by the name of your new type, followed by an optional colon and integer type, followed by the enumerated literals in curly braces. Feel free to substitute struct for class; they are interchangeable in an enum declaration. The following code shows some examples of enumerated types:

enum class color { black, red, green, yellow, blue, magenta, cyan, white };
enum class sign : char { negative, positive };
enum class flags : unsigned { boolalpha, showbase, showpoint, showpos, skipws };
enum class review : int { scathing = -2, negative, neutral, positive, rave };

A scoped enum definition defines a brand-new type that is distinct from all other types. The type name is also a scope name, and the names of all the enumerators are declared in that scope. Because enumerators are scoped, you can use the same enumerator name in multiple scoped enumerations.

The type after the colon must be an integral type. If you omit the colon and type, the compiler implicitly uses int. This type is called the underlying type. The enumerated value is stored as though it were a value of the underlying type.

Each enumerator names a compile-time constant. The type of each enumerator is the enumeration type. You can obtain the integral value by casting the enumerated type to its underlying type, and you can cast an integral type to the enumerated type. The compiler will not perform these conversions automatically. Listing 65-1 shows one way to implement the std::ios_base::openmode type (refresh your memory in Exploration 14). The type must support the bitwise operators to combine out, trunc, app, etc., which it can provide by converting enumerated value to unsigned, performing the operation, and casting back to openmode.

Listing 65-1.  One Way to Implement openmode Type

enum class openmode : unsigned { in, out, binary, trunc, app, ate };
 
openmode operator|(openmode lhs, openmode rhs)
{
   return static_cast<openmode>(
     static_cast<unsigned>(lhs) | static_cast<unsigned>(rhs) );
}
 
openmode operator&(openmode lhs, openmode rhs)
{
   return static_cast<openmode>(
     static_cast<unsigned>(lhs) & static_cast<unsigned>(rhs) );
}
 
openmode operator~(openmode arg)
{
   return static_cast<openmode>( ~static_cast<unsigned>(arg) );
}

When you declare an enumerator, you can provide an integral value by following the enumerator name with an equal sign and a constant expression. The expression can use enumerators declared earlier in the same type as integral constants. If you omit a value, the compiler adds one to the previous value. If you omit a value for the first enumerator, the compiler uses zero. For example:

enum class color : unsigned { black, red=0xff0000, green=0x00ff00, blue=0x0000ff,
     cyan = blue|green, yellow=red|green, magenta=red|blue, white=red|blue|green };

You can forward declare an enumerated type, similar to the way you can forward declare a class type. If you omit the curly-braced list of enumerators, it tells the compiler the type name and its underlying type, so you can use the type to declare function parameters, data members, and so on. You can provide the enumerators in a separate declaration:

enum class deferred : short;
enum class deferred : short { example, of, forward, declared, enumeration };

A forward-declared enumeration is also called an opaque declaration. One way to use an opaque declaration is to declare the type in a header file, but provide the enumerators in a separate source file. If the opaque type is declared in a class or namespace, be sure to qualify the type’s name when you provide the full type definition. For example, if the header contains

class demo {
  enum hidden : unsigned;
  ...
};

The source file might declare enumerators that are for use solely by the implementation, and not the user of the class as follows:

#include "demo.hpp"
enum demo::hidden : unsigned { a, b, c };

The compiler implicitly defines the comparison operators for enumerators. As you probably expect, they work by comparing the underlying integral values. The compiler provides no other operators, such as I/O, increment, decrement, etc.

Scoped enumerations pretty much work the way one would expect an enumerated type to work. Given the name “scoped” enumerations, you must surely be asking yourself, “What is an unscoped enumeration?” You are in for a surprise.

Unscoped Enumerations

An unscoped enumerated type defines a new type, distinct from other types. The new type is an integer bitmask type, with a set of predefined mask values. If you do not specify an underlying type, the compiler chooses one of the built-in integral types; the exact type is implementation-defined. Define an unscoped enumeration in the same manner as a scoped enumeration, except you must omit the class (or struct) keyword.

The compiler does not define the arithmetic operators for enumerated types, leaving you free to define these operators. The compiler implicitly converts an unscoped enumerated value to its underlying integer value, but to convert back, you must use an explicit type cast. Use the enumerated type name in the manner of a constructor with an integer argument or use static_cast.

enum color { black, blue, green, cyan, red, magenta, yellow, white };
int c1{yellow};
color c2{ color(c1 + 1) };
color c3{ static_cast<color>(c2 + 1) };

Calling an enum a “bitmask” may strike you as odd, but that is how the standard defines the implementation of an unscoped enumeration. Suppose you define the following enumeration:

enum sample { low=4, high=256 };

The permissible values for an object of type sample are all values in the range [sample(0), sample(511)]. The permissible values are all the bitmask values that fit into a bitfield that can hold the largest and smallest bitmask values among the enumerators. Thus, in order to store the value 256, the enumerated type must be able to store up to 9 bits. As a side effect, any 9-bit value is valid for the enumerated type, or integers up to 511.

You can use an enumerated type for a bitfield (Exploration 64). It is your responsibility to ensure that the bitfield is large enough to store all the possible enumeration values, as demonstrated in the following:

class demo {
  color okay  : 3; // big enough
  color small : 2; // oops, not enough bits, but valid C++
};

The compiler will let you declare a bitfield that is too small; if you are fortunate, your compiler will warn you about the problem.

C++ inherits this definition of enum from C. The earliest implementations lacked a formal model, so when the standards committee tried to nail down the formal semantics, the best they could do was to capture the behavior of extant compilers. In C++ 11, they invented scoped enumerators, to try to provide a rational way to define enumerations.

Strings and Enumerations

One deficiency of scoped and unscoped enumerations is I/O support. C++ does not implicitly create any I/O operators for enumerations. You are on your own. Unscoped enumerators can be implicitly promoted to the underlying type and printed as an integer, but that’s all. If you want to use the names of the enumerators, you must implement the I/O operators yourself. One difficulty is that C++ gives you no way to discover the literal names with any reflection or introspection mechanism. Instead, you have to duplicate information and hope you don’t make any mistakes.

In order to implement I/O operators that can read and write strings, you must be able to map strings to enumerated values and vice versa. Assume that conversions to and from strings will occur frequently. What data structure should you use to convert strings to enumerated values? ________________ What data structure should you use for the reverse conversion? ________________If you know that the enumerators are not arbitrary values but follow the implicit values that the compiler assigns, you can use std::array to map values to strings; otherwise, use std::unordered_map. To reduce the amount of redundant information in the program, write a helper function to populate both data structures simultaneously.

Suppose you define a language type that enumerates your favorite computer languages. Implement the to_string(language) function to return a language as a string and a function, from_string(), to convert a string to a language. Throw std::out_of_range if the input is not valid. Write a function, void initialize_languages(), that initializes the internal data structures.

See Listing 65-2 for the string conversion code for the language class.

Listing 65-2.  Mapping a language Value to and from Strings

#include <array>
#include <istream>
#include <stdexcept>
#include <string>
#include <vector>
#include <unordered_map>
 
enum class language { apl, low=apl, c, cpp, forth, haskell, jovial, lisp, scala, high=scala };
 
std::unordered_map<std::string, language> from_string_map_;
std::array<std::string, 8> to_string_map_;
 
void store(std::string const& name, language lang)
{
   from_string_map_.emplace(name, lang);
   to_string_map_.at(static_cast<int>(lang)) = name;
}
 
void initialize_language()
{
   store("apl", language::apl);
   store("c", language::c);
   store("cpp", language::cpp);
   store("forth", language::forth);
   store("haskell", language::haskell);
   store("jovial", language::jovial);
   store("lisp", language::lisp);
   store("scala", language::scala);
}
 
std::string const& to_string(language lang)
{
   return  to_string_map_.at(static_cast<int>(lang));
}
 
language from_string(std::string const& text)
{
   return from_string_map_.at(text);
}
 
std::ostream& operator<<(std::ostream& stream, language lang)
{
   stream << to_string(lang);
   return stream;
}
 
std::istream& operator>>(std::istream& stream, language& lang)
{
   std::string text;
   if (stream >> text)
      lang = from_string(text);
   return stream;
}

The next task is to ensure that initialize_language() is called. The common idiom is to define an initialization class. Call it initializer. Its constructor calls initialize_language(). Thus, constructing a single instance of initializer ensures that the language class is initialized, as shown in Listing 65-3.

Listing 65-3.  Automatically Initializing the Language Data Structures

class initializer {
public:
   initializer() { initialize_language(); }
};
initializer init;

Objects at namespace or global scope are constructed before main() begins or before the use of any function or object in the same file. Thus, the init object is constructed early, which calls initialize_language(), which in turn ensures the language data structures are properly initialized.

The one difficulty is when another global initializer has to use language. C++ offers no convenient way to ensure that objects in one file are initialized before objects in another file. Within a single file, objects are initialized in the order of declaration, starting at the top of the file. Dealing with this issue is beyond the scope of this book. None of the examples in this book depends on the order of initialization across files. In fact, most programs don’t face this problem, because they don’t use global or namespace scoped objects. So we can return to the immediate problem: reading and writing language values.

Revisiting Projects

Now that you know all about enumerations, consider how you could improve some previous projects. For example, in Exploration 35, we wrote a constructor for the point class that uses a bool to distinguish between Cartesian and polar coordinate systems. Because it is not obvious whether true means Cartesian or polar, a better solution is to use an enumerated type, such as the following:

enum class coordinate_system : bool { cartesian, polar };

Another example that can be improved with enumerations is the card class, from Listing 53-5. Instead of using int constants for the suits, use an enumeration. You can also use an enumeration for the rank. The enumeration has to specify enumerators: the number cards and ace, jack, queen, and king. Choose appropriate values so that you can cast an integer in the range [2, 10] to rank and get the desired value. You will have to implement operator++ for suit and rank. Write your new, improved card class and compare it with my solution in Listing 65-4.

Listing 65-4.  Improving the card Class with Enumerations

#ifndef CARD_HPP_
#define CARD_HPP_
 
#include <istream>
#include <ostream>
 
enum class suit { nosuit, diamonds, clubs, hearts, spades };
enum class rank { norank=0, r2=2, r3, r4, r5, r6, r7, r8, r9, r10, jack, queen, king, ace };
 
suit&operator++(suit&s)
{
   if (s == suit::spades)
      s = suit::diamonds;
  else
     s = static_cast<suit>(static_cast<int>(s) + 1);
  return s;
}
 
rank operator++(rank&r)
{
   if (r == rank::norank or r == rank::ace)
      r = rank::r2;
   else
      r = static_cast<rank>(static_cast<int>(r) + 1);
   return r;
}
 
/// Represent a standard western playing card.
class card
{
public:
  card() : rank_(rank::norank), suit_(suit::nosuit) {}
  card(rank r, suit s) : rank_(r), suit_(s) {}
 
  void assign(rank r, suit s);
  suit get_suit() const { return suit_; }
  rank get_rank() const { return rank_; }
private:
  rank rank_;
  suit suit_;
};
 
bool operator==(card a, card b);
std::ostream& operator<<(std::ostream& out, card c);
std::istream& operator>>(std::istream& in, card& c);
 
/// In some games, Aces are high. In other Aces are low. Use different
/// comparison functors depending on the game.
bool acehigh_compare(card a, card b);
bool acelow_compare(card a, card b);
 
/// Generate successive playing cards, in a well-defined order,
/// namely, 2-10, J, Q, K, A. Diamonds first, then Clubs, Hearts, and Spades.
/// Roll-over and start at the beginning again after generating 52 cards.
class card_generator
{
public:
  card_generator();
  card operator()();
private:
  card card_;
};
 
#endif

What other projects can you improve with enumerations?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset