C++/CLI: Inheritance and Enumerators

C/C++ Users Journal June, 2005

Working with CLI-compliant enumerators

By Rex Jaeschke

Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the Standards for C++/CLI, CLI, and C#. Rex can be reached at rex@RexJaeschke.com.

This month, I examine C++/CLI issues relating to inheritance. The class hierarchy used involves three kinds of transactions at a bank: deposits, withdrawals, and transfers. The transaction type is implemented using a new form of enum.

Enumerators

Consider the type declaration in Listing 1, which lives in its own source file and is compiled to an assembly that contains only this type.

As you should expect, the enumerators Deposit, Withdrawal, and Transfer act like constants with the values 0, 1, and 2, respectively. (Enumerators really are literal fields.)

Three things set this enum type definition apart from a Standard C++ enum (that is, a "native enum"):

The reason to support this new syntax is that CLI enums are CLS-compliant, while native enums are not.

The primary difference between a CLI enum and a native enum is that in the former, the name of an enumerator is scoped by its parent enum type. In addition, the integral promotions defined by the C++ Standard do not apply to a CLI enum.

Like a native enum, a CLI enum can also be defined inside a class. In such cases, however, a visibility specifier is not permitted because the visibility of that nested type is taken from that of the type in which it is nested.

The Abstract Transaction Base Class

The transaction type hierarchy is rooted in the base class Transaction that, by default, is derived from System::Object (see Listing 2).

In case 1 of Listing 2, this class is marked with the abstract modifier, meaning that it cannot be instantiated directly. ("abstract" is not a keyword; it is merely reserved for this purpose in this context.) This modifier lets an abstract class be defined without having to explicitly declare one or more if its member functions to be pure virtual.

As you can see from the private data members, a Transaction contains a transaction type and a time/date stamp, both of which I access via the properties defined in cases 3a and 3b. The CLI library value class type System::DateTime used in case 2 allows an instant in time to be represented as a date and time of day. Note how both properties have public getters and private setters (which under the new CLI Standard, is now CLS-compliant).

Case 4 requires that every concrete transaction type have the public member function PostTransaction, with the signature shown. The abstract function modifier is equivalent to the Standard C++ syntax for declaring a pure virtual function. An abstract function must be explicitly declared virtual.

As the constructor should only be called from a derived class, the constructor defined in case 5 is protected. The business of this constructor is simple: It sets the new transaction's type to that passed to it, and sets the time/date stamp to the current time by calling the getter of the public property DateTime::Now. In the case of the transaction type passed in, being of a value class type, nullptr is not permitted, and with the strong type checking of CLI enum types, the compiler only allows enumerators of that type to be passed, or instances of that type, which, of course, could only have been initialized with enumerators of that type.

Ordinarily, the constructor would run as quickly as possible; however, to get more interesting results from our test program, rather than place a delay in that program—so the time/date stamp changes for each transaction—in case 6, the constructor "sleeps" for a random amount of time before initializing its data members. Every program has at least one thread of execution, and certain characteristics of that thread can be set or retrieved via members of the sealed ref class System::Threading::Thread. The function Thread::Sleep suspends execution of the current thread for the specified number of milliseconds.

To vary the suspension period, the ref class System::Random is used to generate a sequence of pseudorandom numbers. The Next function overload used in case 6 retrieves the next number in the sequence that is in the range "greater than or equal to 1000 and less than 2001"; that is, you want a delay of between one and two seconds.

The Deposit, Withdrawal, and Transfer Classes

Listing 3 defines the Deposit class. Why is the class sealed? If you haven't thought through whether it is robust enough to be used as a base class, then don't allow it to be used in that manner.

The CLI supports a single-inheritance model only. As such, ref and value classes can have only one direct base class, which, by default, is System::Object. In case 1, Deposit is derived directly from Transaction. Note the absence of the access specifier public. The CLI only supports public inheritance, so while you could have written ": public Transaction" instead, that would be redundant. (For native classes, inheritance is public by default when the derived type is a struct, and private by default when it is a class.)

Because the CLI library supports a type suitable for financial calculations (namely, System::Decimal), you use that to represent the deposit amount in case 2.

For convenience, two constructors are provided: one that takes an amount expressed as Decimal, the other expressed in double. Note how the name-scoping requirements of CLI enums require that the TransactionType enumerator Deposit be fully qualified in both constructor definitions.

You fulfill the base class's abstract requirement by providing an implementation of PostTransaction in case 4. DateTime is a value type, so when an instance of it is passed, it is boxed to match the overload of WriteLine that expects an Object^. The expression this has type Deposit^, which is a type derived from Object^, the type in the WriteLine overload. In both cases, the hierarchy is walked until the corresponding ToString function is reached and then called.

You could have declared the function PostTransaction to be sealed, so it could not be overridden. However, given that the parent class itself is sealed, that function can never be overridden.

The format specifier {0,10:0.00} used in case 5 says to right-justify the amount in a width of 10 print positions, to round to two decimal places, and to have at least one digit prior to the decimal point.

The type Deposit relies directly on types Transaction and TransactionType, so it makes sense for the assemblies for both of these types to be made available to the compilation of Deposit. By doing so, however, the compiler may issue a warning to the affect that TransactionType has been "imported" twice, once directly and once indirectly via Transaction. You can safely ignore that warning.

The class Withdrawal is defined in Listing 4, and the class Transfer is defined in Listing 5.

Although all three implementations of PostTransaction are identical, in a real transaction processing system, that is unlikely to be the case.

The Test Program

Listing 6 tests the transaction types by creating an array of concrete-typed transactions, and iterating over that array, calling each element's PostTransaction function. Figure 1 is output produced from one execution. The default date/time format used is that for the U.S.; that is, month, day, year, with a 12-hour clock.

Enumerators and Inheritance

A CLI enum type is implemented as a value class that implicitly inherits from System::Enum. As such, the static and instance members of that type, its base class System::ValueType, and that type's base class, System::Object, become available to you in the context of a CLI enum type or any instance of that type. Listing 7 produces the output in Figure 2.

In case 1, you call Enum::GetName to find the name of a given enumerator in the specified enum type. The first argument must have type System::Type, and one way to get that is by calling Object::GetType on the variable of interest.

You call Enum::GetNames in case 2 to find the names of all enumerators in the specified enum type. The first argument must have type System::Type, and a way to get that is by calling Type::GetType on a string representation of the name of the type of the variable of interest.

You call Enum::GetUnderlyingType in cases 3 and 4 to find the underlying types of the two CLI enum types. Here I used an even easier way to find a type's Type object—you simply use the new form of the typeid operator.

Arrays and Inheritance

Every CLI array type is derived implicitly from the abstract ref class type System::Array. In previous installments, I've shown numerous examples of array manipulation that involve a property called Length, which is inherited from the base type. As such, every public member of Array and Object is available when you are working with a CLI array; see Listing 8. Figure 3 is the output produced by Listing 8. The member functions of Array that are called here are straightforward.

Overriding versus Hiding

In a virtual function invocation, the runtime type of the instance for which that invocation takes place determines the actual function implementation to invoke. In a nonvirtual function invocation, the compile-time type of the instance is the determining factor.

As you know from Standard C++, the implementation of a virtual function can be superseded by derived classes. The process of superseding such an implementation is known as "overriding," which is achieved via the use of the override function modifier. Whereas a virtual function declaration introduces a new function, an override function declaration specializes an existing inherited virtual function by providing a new implementation of that function. An override function must be explicitly declared virtual.

When a class redeclares a name that it inherited, in the presence of the new function modifier, that class is said to hide that name.

Using the classes defined in Listing 9, look at a series of variable definitions and their use in calling these members functions:

A^ a = gcnew A();
a->F0();   // calls A::F0
a->F1();   // calls A::F1
a->F2();   // calls A::F2

a->F0(). A::F0 is a nonvirtual function, so the compile-time type of a (that is, A) is used, resulting in A::F0 being called.

a->F1(). A::F1 is a virtual function, so the runtime type of a (that is, A) is used, resulting in A::F1 being called.

a->F2(). Like A::F1, A::F2 is a virtual function, so the runtime type of a (that is, A) is used, resulting in A::F2 being called.

B^ b = gcnew B();
b->F0();   // calls B::F0
b->F1();   // calls B::F1
b->F2();   // calls B::F2

b->F0(). B::F0 is a nonvirtual function, so the compile-time type of b (that is, B) is used, resulting in B::F0 being called.

b->F1(). B::F1 overrides the virtual function A::F1, so the runtime type of b (that is, B) is used, resulting in B::F1 being called.

b->F2(). B::F2 hides (via new) the virtual function A::F2, so the compile-time type of b (that is, B) is used, resulting in B::F2 being called. This hiding function is also virtual, allowing classes derived from B to override this new function.

a = b;
a->F0();   // calls A::F0
a->F1();   // calls B::F1
a->F2();   // calls A::F2

a->F0(). A::F0 is a nonvirtual function, so the compile-time type of a (that is, A) is used, resulting in A::F0 being called.

a->F1(). A::F1 is a virtual function, so the runtime type of a (that is, B) is used, resulting in B::F1 being called.

a->F2(). A::F2 is a virtual function that is hidden by the function B::F2, so the compile-time type of a (that is, A) is used, resulting in A::F2 being called. (Remember, the dynamic lookup process only works if there is a subsequent override function; in this case, there is not.)

C^ c = gcnew C();
c->F0();   // calls C::F0
c->F1();   // calls C::F1x
c->F2();   // calls C::F2x

c->F0(). C::F0 is a nonvirtual function, so the compile-time type of c (that is, C) is used, resulting in C::F0 being called.

c->F1(). C::F1x is a virtual function, so the runtime type of c (that is, C) is used. However, in the case of C::F1x, a named override is used; that is, the function being overridden has a different name from the one doing the overriding. This results in C::F1x being called.

c->F2(). C::F2x overrides the virtual function B::F2, so the runtime type of c (that is, C) is used, resulting in C::F2x being called. (As you can see, in this named override, the explicit override modifier has to be omitted.)

b = c;
b->F0();   // calls B::F0
b->F1();   // calls C::F1x
b->F2();   // calls C::F2x

b->F0(). B::F0 is a nonvirtual function, so the compile-time type of b (that is, B) is used, resulting in B::F0 being called.

b->F1(). B::F1 overrides the virtual function A::F1, so the runtime type of b (that is, C) is used, resulting in C::F1x being called.

b->F2(). B::F2 is a virtual function, so the runtime type of b (that is, C) is used, resulting in C::F2x being called.

a = c;
a->F0();   // calls A::F0
a->F1();   // calls C::F1x
a->F2();   // calls A::F2

a->F0(). A::F0 is a nonvirtual function, so the compile-time type of a (that is, A) is used, resulting in A::F0 being called.

a->F1(). A::F1 is a virtual function, so the runtime type of a (that is, C) is used, resulting in C::F1x being called.

a->F2(). A::F2 is a virtual function that is hidden by the function B::F2, so the compile-time type of a (that is, A) is used, resulting in A::F2 being called. (Remember, the dynamic lookup process only works if there is a subsequent override function. In their case, there is not.)

Access Specifiers

Standard C++ supports three member access specifiers: public, protected, and private. To accommodate assemblies, C++/CLI adds another three. The complete set is:

Members can be made less accessible by a more restrictive access specifier on their parent type. Don't confuse member name accessibility with type visibility (which can only be public or private).

Reader Exercises

Here are some things you might want to consider trying:

  1. Using the assembly for TransactionType, confirm that an enumerator has the type of its parent enum. What is the (reserved) name of the public instance data member in each CLI enum that is used to hold the value of an instance of that type?
  2. Many enum types are defined simply to have a set of named constants with distinct values. Others are defined with enumerators whose initial values are explicitly initialized with distinct values that are a power of two (as in 1, 2, 4, 8, and so on). Define a CLI enum type having the latter and create an instance whose value is the bitwise-or of multiple enumerators. Display the value of that instance. Then apply the attribute [Flags] to that CLI enum type and run the program again. What is the difference? Read the documentation on the type System::FlagsAttribute.
  3. The name of a native enum can be omitted; for example, enum Color {Red, Blue, Green}; is permitted. Just by reasoning, determine if this is true for a CLI enum.
  4. Look at the documentation for the type System::DateTime.
  5. Could the properties defined in cases 3(a) and 3(b) be implemented as trivial properties, yet still have exactly the same semantics as those defined here?
  6. Look at the specification of the CLI library types System::Threading::Thread and System::Random.
  7. Look at the documentation for the type System::Enum and System::Array to see what members are available to users of CLI enum and array types, respectively.