
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Class Inheritance and Virtual Functions
The GlassBox class is derived from the Box class and therefore inherits the implementation of IContainer. The GlassBox class definition needs no changes at all to accommodate the introduction of the IContainer interface class.
The IContainer interface class has the same role as a base class in polymorphism. You can use a handle of type IContainer to store the address of an object of any class type that implements the interface. Thus a handle of type IContainer can be used to reference objects of type Box or type GlassBox and obtain polymorphic behavior when calling the functions that are members of the interface class. Let’s try it.
Try It Out |
Implementing an Interface Class |
Create the CLR console project Ex9_15 and add the IContainer.h and Box.h header files with the contents from the previous section. You should also add copies of the Stack.h and GlassBox.h header files from Ex9_14 to the project. Finally, modify the contents of Ex9_15.cpp to the following:
// Ex9_15.cpp : main project file. |
|
#include “stdafx.h” |
|
#include “Box.h” |
// For Box and IContainer |
#include “GlassBox.h” |
// For GlassBox (and Box and IContainer) |
#include “Stack.h” |
// For the stack class with nested struct |
Item |
|
using namespace System;
int main(array<System::String ^> ^args)
{
array<IContainer^>^ containers = { gcnew Box(2.0, 3.0, 4.0), gcnew GlassBox(2.0, 3.0, 4.0), gcnew Box(4.0, 5.0, 6.0), gcnew GlassBox(4.0, 5.0, 6.0)
};
Console::WriteLine(L”The array of containers have the following volumes:”);
for each(IContainer^ container in containers) container->ShowVolume(); // Output the volume of a box
Console::WriteLine(L”\nNow pushing the containers on the stack...”);
Stack^ stack = gcnew Stack; // Create the stack for each(IContainer^ container in containers)
stack->Push(container);
Console::WriteLine(
L”Popping the containers off the stack presents them in reverse order:”); Object^ item;
while((item = stack->Pop()) != nullptr) safe_cast<IContainer^>(item)->ShowVolume();
Console::WriteLine(); return 0;
}
529

Chapter 9
This example produces the following output:
The array of containers have the following volumes:
CBox usable volume is 24
CBox usable volume is 20.4
CBox usable volume is 120
CBox usable volume is 102
Now pushing the containers on the stack...
Popping the containers off the stack presents them in reverse order:
CBox usable volume is 102
CBox usable volume is 120
CBox usable volume is 20.4
CBox usable volume is 24
Press any key to continue . . .
How It Works
You create an array of elements of type IContainer^ and initialize the elements with the addresses of
Box and GlassBox objects:
array<IContainer^>^ containers = { gcnew Box(2.0, 3.0, 4.0), gcnew GlassBox(2.0, 3.0, 4.0), gcnew Box(4.0, 5.0, 6.0), gcnew GlassBox(4.0, 5.0, 6.0)
};
The Box and GlassBox classes implement the IContainer interface so you can store addresses of objects of these types in variables of type handle to IContainer. The advantage of doing this is that you’ll be able to call the function members of the IContainer interface class polymorphically.
You list the volumes of the Box and GlassBox objects in a for each loop:
for each(IContainer^ container in containers) container->ShowVolume(); // Output the volume of a box
The loop body shows polymorphism in action; the ShowVolume() function for the specific type of object referenced by container is called, as you can see from the output.
You push the elements of the containers array on to the stack in essentially the same way as the previous example. Popping the elements off the stack is also similar to the previous example:
Object^ item;
while((item = stack->Pop()) != nullptr) safe_cast<IContainer^>(item)->ShowVolume();
The loop body shows that you can cast a handle to an interface type using safe_cast in exactly the same way as you would cast to a ref class type. You are then able to use the handle to call the ShowVolume() function polymorphically.
530

Class Inheritance and Virtual Functions
Using interface classes is not only a useful way of defining sets of functions that represent standard class interfaces but also a powerful mechanism for applying polymorphism in your programs.
Classes and Assemblies
A C++/CLI application always resides in one or more assemblies so C++/CLI classes always reside in an assembly. The classes we have defined for each example up to now have all been contained in a single simple assembly that is the executable, but you can create assemblies that contain your own library classes. C++/CLI adds visibility specifiers for classes that determine whether a given class is accessible from outside the assembly in which it resides which is referred to as its parent assembly. In addition to the public, private, and protected member access specifiers that you have in native C++, C++/ CLI has additional access specifiers for class members that determine from where they may be accessed in different assemblies.
Visibility Specifiers for Classes and Interfaces
You can specify the visibility of a non-nested class, interface, or enum as private or public. A public class is visible and accessible outside the assembly in which is resides whereas a private class is only accessible within its parent assembly. Classes, interfaces and enum classes are private by default and therefore only visible within their parent assembly. To specify a class as public, you just use the public keyword, like this:
public interface class IContainer
{
// Details of the interface...
};
The IContainer interface here is visible in an external assembly because you have defined it as public. If you omit the public keyword, the interface would be private by default and only usable within its parent assembly. You can specify a class, enum, or interface explicitly as private if you want, but it is not necessary.
Access Specifiers for Class and Interface Members
C++/CLI adds three more access specifiers for class members: internal, public protected, and private protected. The effects of these are described in the comments in the class definition:
public ref class MyClass // Class visible outside assembly
{
public:
// Members accessible from classes inside and outside the parent assembly
internal:
// Members accessible from classes inside the parent assembly
public protected:
//Members accessible in types derived from MyClass outside the parent assembly
//and in any classes inside the parent assembly
private protected:
// Members accessible in types derived from MyClass inside the parent assembly
};
531

Chapter 9
Obviously the class must be public for the member access specifiers to allow access from outside the parent assembly. Where the access specifier involves two keywords such as private protected the less restrictive keyword applies inside the assembly and the more restrictive keyword applies outside the assembly. You can reverse the sequence of the keyword pairs so protected private has the same meaning as private protected.
To use some of these you need to create an application that consists of more than one assembly, so let’s recreate Ex9_15 as a class library assembly plus an application assembly that uses the class library.
Try It Out Creating a Class Library
To create a class library you can first create a CLR project with the name Ex9_16lib using the Class Library template. The project contains a header file, Ex9_16lib.h, with the following content:
// Ex9_16lib.h
#pragma once
using namespace System;
namespace Ex9_16lib
{
public ref class Class1
{
// TODO: Add your methods for this class here.
};
}
A class library has its own namespace and here the namespace name is Ex9_16lib by default. You could change this name to something more suitable if you want. The names of the classes in the library are qualified by the namespace name so you need a using directive for the namespace name in any external source file that is accessing any of the classes in the library. The definitions of the classes that are to be in the library go between the braces for the namespace. There’s a default ref class defined within the namespace, but you replace this with your own classes. Note that the Class1 class is public; all classes that are to be visible in another assembly must be specified as public.
Modify the contents of Ex9_16lib.h to:
// Ex9_16lib.h
#pragma once
using namespace System;
namespace Ex9_16lib
{
// IContainer.h for Ex9_16
public interface class IContainer
{
virtual |
double Volume(); |
// |
Function |
for calculating a volume |
virtual |
void ShowVolume(); |
// |
Function |
to display a volume |
532

Class Inheritance and Virtual Functions
};
// Box.h for Ex9_16
public ref class Box : IContainer
{
public:
//Function to show the volume of an object virtual void ShowVolume()
{
Console::WriteLine(L”CBox usable volume is {0}”, Volume());
}
//Function to calculate the volume of a Box object
virtual double Volume()
{ return m_Length*m_Width*m_Height; }
// Constructor
Box() : m_Length(1.0), m_Width(1.0), m_Height(1.0){}
// Constructor
Box(double lv, double wv, double hv)
: m_Length(lv), m_Width(wv), m_Height(hv){}
public protected: double m_Length; double m_Width; double m_Height;
};
// Stack.h for Ex9_16
public ref class Stack
{
private:
// Defines items to store in the stack ref struct Item
{ |
|
Object^ Obj; |
// Handle for the object in this item |
Item^ Next; |
// Handle for next item in the stack or nullptr |
// Constructor
Item(Object^ obj, Item^ next): Obj(obj), Next(next){}
}; |
|
Item^ Top; |
// Handle for item that is at the top |
public: |
|
// Push an object on to the stack |
|
void Push(Object^ obj) |
|
{ |
|
Top = gcnew Item(obj, Top); |
// Create new item and make it the top |
} |
|
// Pop an object off the stack |
|
Object^ Pop() |
|
533

Chapter 9
{ |
|
if(Top == nullptr) |
// If the stack is empty |
return nullptr; |
// return nullptr |
Object^ obj = Top->Obj; |
// Get box from item |
Top = Top->Next; |
// Make next item the top |
return obj; |
|
} |
|
}; |
|
} |
|
The IContainer interface class, the Box class, and the Stack class are now in this library. The changes to the original definitions for these classes are shaded. Each class is now public, which makes them accessible from an external assembly. The fields in the Box class are public protected, which means that they are inherited in a derived class as protected fields but are public so far as classes within the parent assembly are concerned. You don’t actually refer to these fields from other classes within the parent assembly so you could have left the fields in the Box class as protected in this case.
When you have built this project successfully, the assembly containing the class library is in a file Ex9_16lib.dll that is in the debug subdirectory to the project directory if you built a debug version of the project or in a release subdirectory if you built the release version. The .dll extension means that this is a dynamic link library or DLL. You now need another project that uses your class library.
Try It Out |
Using a Class Library |
Add a new CLR console project with the name Ex9_16 in its own solution as always. You can then modify Ex9_16.cpp as follows:
//Ex9_16.cpp : main project file.
//Using a class library in a separate assembly
#include “stdafx.h” #include “GlassBox.h” #using <Ex9_16lib.dll>
using namespace System; using namespace Ex9_16lib;
int main(array<System::String ^> ^args)
{
array<IContainer^>^ containers = { gcnew Box(2.0, 3.0, 4.0), gcnew GlassBox(2.0, 3.0, 4.0), gcnew Box(4.0, 5.0, 6.0), gcnew GlassBox(4.0, 5.0, 6.0)
};
Console::WriteLine(L”The array of containers have the following volumes:”); for each(IContainer^ container in containers)
container->ShowVolume(); // Output the volume of a box
Console::WriteLine(L”\nNow pushing the containers on the stack...”);
Stack^ stack = gcnew Stack; // Create the stack
534

Class Inheritance and Virtual Functions
for each(IContainer^ container in containers) stack->Push(container);
Console::WriteLine(
L”Popping the containers off the stack presents them in reverse order:”); Object^ item;
while((item = stack->Pop()) != nullptr) safe_cast<IContainer^>(item)->ShowVolume();
Console::WriteLine(); return 0;
}
You also need to add the GlassBox.h header to the project with the same code as in Ex9_15 so you can copy the file to this project directory and then add it to the project by right-clicking Header Files in the Solution Explorer tab and selecting Add > Existing Item from the context menu. Of course, the GlassBox class is derived from the Box class, so the compiler needs to know where to find the Box class definition. In this case it’s in the library you created in the previous project, so add the following directive to the GlassBox.h header file after the #pragma once directive:
#using <Ex9_16lib.dll>
The Box class name is defined within the Ex9_16lib namespace, so you also need to add a using statement for that following the #using directive:
using namespace Ex9_16lib;
To enable the compiler to find the library, copy the Ex9_16lib.dll file from the Ex9_16lib project to the debug subdirectory to the Ex9_16 solution directory that contains the Ex9_16.exe file. You could specify the full path to the assembly in the #using directive, but it is more usual to put any class libraries that a project uses in the directory that contains the executable for an application. It’s easy to get the directories muddled here. The Ex9_16lib.dll file is in the debug subdirectory to the Ex9_16lib solution directory, not the debug subdirectory to the Ex9_16lib project directory. You are copying the library file to the debug subdirectory of the Ex9_16 solution directory. Make sure you copy the .dll file to the correct directory or the library won’t be found.
Because the classes in the external assembly are in their own namespace, you have a using directive for the Ex9_16lib namespace name. Without this you would have to qualify the IContainer, Box, and Stack names with the namespace name so you would write Ex9_16lib::Box instead of just Box for example.
The remainder of the code is exactly the same as in the main() function for Ex9_15; no changes are necessary because you are now using classes from an external assembly. If you execute the program, you’ll see the output is the same as that from Ex9_15.
535

Chapter 9
Functions Specified as new
You have seen how you use the override keyword to override a function in a base class. You can also specify a function in a derived class as new, in which case it hides the function in the base class that has the same signature and the new function does not participate in polymorphic behavior. To define the Volume() function as new in a class NewBox that is derived from Box you code it like this:
ref class NewBox : Box |
// Derived class |
{ |
|
public: |
|
//New function to calculate the volume of a NewBox object virtual double Volume() new
{ return 0.5*m_Length*m_Width*m_Height; }
//Constructor
NewBox(double lv, double wv, double hv): Box(lv, wv, hv){}
};
This version of the function hides the version of the Volume() function that is defined in Box, so if you cal the Volume() function using a handle of type NewBox^ the new version is called. For example:
NewBox^ newBox = gcnew NewBox(2.0, 3.0,4.0);
Console::WriteLine(newBox->Volume()); // Output is 12
The result is 12 because the new Volume() function hides the polymorphic version that the NextBox class inherits from Box.
The new Volume() function is not a polymorphic function so for polymorphic calls using a handle to a base class type, the new version is not called. For example:
Box^ newBox = gcnew NewBox(2.0, 3.0,4.0);
Console::WriteLine(newBox->Volume()); // Output is 24
The only polymorphic Volume() function in the NewBox class is the one that is inherited from the Box class so that is the function that is called in this case.
Delegates and Events
An event is a member of a class that enables an object to signal when a particular event has occurred, and the signaling process for an event involves a delegate. A mouse click is a typical example of an event and the object that originated the mouse click event would signal that the event has occurred by calling one or more functions that are responsible for dealing with the event. Let’s look at delegates first and return to events a little later in this chapter.
The idea of a delegate is very simple — it’s an object that can encapsulate one or more pointers to functions that have a given parameter list and return type. Thus a delegate provides a similar facility in C++/CLI to a function pointer in native C++. Although the idea of a delegate is simple, however, the detail of creating and using delegates can get a little confusing so it’s time to concentrate.
536

Class Inheritance and Virtual Functions
Declaring Delegates
The declaration for a delegate looks like a function prototype preceded by the delegate keyword but in reality it defines two things: the reference type name for a delegate object, and the parameter list and return type of the functions that can be associated with the delegate. A delegate reference type has the System::Delegate class as a base class so a delegate type always inherits the member of this class. The declaration for a delegate looks like a function prototype preceded by the delegate keyword but in reality it defines a reference type for the delegate, and the signature of the functions that can be associated with the delegate. Here’s an example of a declaration for a delegate:
public delegate void Handler(int value); |
// Delegate declaration |
This defines a delegate reference type Handler where the Handler type is derived from System::Delegate. An object of type Handler can contain pointers to one or more functions that have a single parameter of type int and a return type that is void. The functions pointed to by a delegate can be instance functions or static functions.
Creating Delegates
Having defined the delegate type, you can now create delegate objects of this type. You have a choice of two constructors for a delegate: one that accepts a single argument and another that accepts two arguments.
The argument to the delegate constructor that accepts one argument must be a static function member of a class or a global function that has the return type and parameter list specified in the delegate declaration. Suppose you define a class with the name HandlerClass like this:
public ref class HandlerClass
{
public:
static void Fun1(int m)
{ Console::WriteLine(L”Function1 called with value {0}”, m); }
static void Fun2(int m)
{ Console::WriteLine(L”Function2 called with value {0}”, m); }
void Fun3(int m)
{ Console::WriteLine(L”Function3 called with value {0}”, m+value); }
void Fun4(int m)
{ Console::WriteLine(L”Function3 called with value {0}”, m+value); }
HandlerClass():value(1){}
HandlerClass(int m):value(m){} protected:
int value;
};
The class has four functions with a parameter of type int and a return type of void. Two of these are static functions and two are instance functions. It also has two constructors including a no-arg constructor. This class doesn’t do much except produce output where you’ll be able to determine which function was called and for instance functions what the object was.
537

Chapter 9
You could create a Handler delegate like this:
Handler^ handler = gcnew Handler(HandlerClass::Fun1); |
// Delegate object |
The handler object contains the address of the static function, Fun1, in the HandlerClass class. If you call the delegate, the HandlerClass::Fun1() function is called with the argument the same as you pass in the delegate call. You can write the delegate call like this:
handler->Invoke(90);
This calls all the functions in the invocation list for the handler delegate. In this case there is just one function in the invocation list, HandlerClass::Fun1(), so the output is:
Function1 called with value 90
You could also call the delegate with the following statement:
handler(90);
This is shorthand for the previous statement that explicitly called the Invoke() function and this is the form of delegate call you see generally.
The + operator is overloaded for delegate types to combine the invocation lists for two delegates into a new delegate object. For example, you could apparently modify the invocation list for the handler delegate with this statement:
handler += gcnew Handler(HandlerClass::Fun2);
The handler variable now references a delegate object with an invocation list containing two functions: Fun1 and Fun2. However, this is a new delegate object. The invocation list for a delegate cannot be changed so the + operator works in a similar way to the way it works with String objects(you always get a new object created. You could invoke the delegate again with this statement:
handler(80);
Now you get the output:
Function1 called with value 80
Function2 called with value 80
Both functions in the invocation list are called and they are called in the sequence in which they were added to the delegate object.
You can effectively remove an entry from the invocation list for a delegate by using the(operator:
handler -= gcnew Handler(HandlerClass::Fun1);
This creates a new delegate object that contains just HandlerClass::Fun2() in its invocation list because the effect is to remove the functions that are invocation list of the right side
538