Rex Jaeschke
C++/CLI: Static Constructors,
IO, and Event Handlers
Initialization before, cleanup after
For certain class types, it is convenient to have some initialization occur the first time that class is used by a program. It can also be useful to have some cleanup done once that class is no longer needed by that program. This month, I show how to do both these things. Along the way, I examine how to read from-and write to-a-text file, as well as look at event handlers and delegates, CLI's language-independent approach to function pointers.
The Problem
Consider the case in which each instance of some type needs to have its own unique ID. (Perhaps it's some sort of transaction type, and each transaction must be traceable during processing, or later on when auditors view data files.) For the purpose of this discussion, let's use an ID that's a signed integer starting at 0.
It is straightforward to keep a next ID value in memory and to increase it by 1 each time a new instance is constructed; however, to have the IDs be unique across consecutive executions of the program requires you to be able to save that value each time the program terminates, and to be able to restore that ID the next time the program starts. There is no way to do this in Standard C++. In fact, there's no way to do it using the Standard CLI Library either. With the help of several library extensions available in the .NET implementation of the CLI, however, you can do both.
An Example of
Using the Solution
I again use the Point class, since points with unique IDs are as good an example as any. Listing 1 is the application program that produces this output:
hp1: [0](0,0)
hp1: [0](6,7)
hp2: [1](3,4)
p1: [2](0,0), p2: [3](-1,-2)
p1: [2](-1,-2)
At program startup, the next-available-ID value is read from a text file and used to initialize a private static field in the Point class. Initially, this file contains the value zero.
Based on the value of the public static bool property TraceID, the string produced by Point's ToString function can optionally contain the Point's ID as a prefix having the form [id]. If that property's value is true, the ID prefix is included; otherwise, it's not. By default, this property is set to false, so I set it to true in case 1.
In case 2, I allocate memory for a Point using the default constructor, and display its ID, 0, and value, (0,0). In case 3, I simply change the location of that Point's x- and y-coordinates via the function Move, which should not change the Point's ID. After all, it's still the same instance - it just has a different value. Then, in case 4, I allocate memory for another Point using the constructor taking two arguments, and display its ID, 1, and value, (3,4).
I create two stack-based instances of Point in case 5, and display their IDs and values. Being the third and fourth Points to be created, they have IDs of 2 and 3, respectively.
In case 6, p1 is assigned a new value; however, p1 is still the same Point as it was before, so its IDs is not changed.
Running the program a second time results in this output:
hp1: [6](0,0)
hp1: [6](6,7)
hp2: [7](3,4)
p1: [8](0,0), p2: [9](-1,-2)
p1: [8](-1,-2)
As shown, the four new instances are assigned consecutive ID values that are distinct from the first execution. However, IDs 4 and 5 "went missing." A close look at case 6 and the definition of the function F shows why. The Point argument is passed to that function by value; a Point is also returned by value. As such, both involve a call to the copy constructor, which dutifully creates a new instance, and each new instance must have a unique ID. So when p2 is passed in by value, a temporary Point is created with ID 4. Then, when that copy is returned by value, another copy is made with ID 5. Both copies are discarded. When the program terminates, the next available ID written out to the file is 6, and that's what's used for the first Point allocated in the next execution.
The Solution
Listing 2 presents the new or changed parts of the revised Point class. Clearly, each instance must now contain an extra field (here called ID) to hold the ID. The type chosen for that field was int, and although Standard C++ allows that to be as small as 16 bits, in a CLI environment, it will be at least 32. If you start at zero, that gives some 2 billion instances before the ID rolls over. Of course, you could start at minus 2 billion instead, which will double the range. And by using the type long long int, you get at least 64 bits, allowing for a very large number of instances, provided you're happy to double the size of the ID field. Should ID be unsigned? It could be if its value were not exported outside its parent class; remember, unsigned integer types are not CLS compliant. (System::Decimal is another alternative, although an instance of that occupies 128 bits.)
Being static, the members defined in cases 2-5 belong to the class, not to any instance. Being private, they are an implementation detail.
First Use of a Class
C++/CLI introduces the notion of a static constructor in nonnative class types. As this term suggests, its name is that of the class, and it is declared static, as in case 6. Although a static constructor is called before that class is first used, what does "used" mean? The execution of a static constructor for a ref class is triggered by the first reference to a static data member in that class.
According to the Ctt/CLI Standard:
A static constructor shall not have a ctor-initializer. Static constructors are not inherited, and cannot be called directly. If a class contains any static fields with initializers, those fields are initialized immediately prior to the static constructor's being executed and in the order in which they are declared.
The metadata generated for static constructors is always marked private, regardless of their declared or implied access specifier. (The compiler can issue warning C4567: "Accessibility on class constructor was ignored.") At this writing, it is still an open issue as to whether a static constructor given an access specifier other than private should be diagnosed, since that access specified will be ignored anyway.
A ref class having no explicit static constructor behaves as if it had one with an empty body.
In case 6a, you obtain the application domain for the current thread from the AppDoma in class. According to the CLI Standard Library:
Application domains, which are represented by System::AppDomain objects, provide isolation, unloading, and security boundaries for executing managed code. Multiple application domains can run in a single process; however, there is not a one-to-one correlation between application domains and threads. Several threads can belong to a single application domain, and while a given thread is not confined to a single application domain, at any given time, a thread executes in a single application domain.
The text file that keeps track of the next-available-ID between program executions has the name "PointID.txt," and resides in the same directory as the program's executable, as in case 6b. (Concat is given a Unicode-or wide-string and a narrow string. In the latter's case, this is automatically converted to a wide string at compile time.) That file is opened in case 6d, read from in case 6e, the input string is converted to an integer in case 6f, and the file is closed in case 6g. The try/catch block is present to deal with (an incomplete subset of the) I/O exceptions that might be thrown.
The read-only properties BaseDirectory and CurrentDomain are Microsoft extensions to the Standard CLI Library.
The types used in I/O, such as StreamReader and File, reside in the namespace System::IO.
Case 6h registers a handler function to be called when the program is about to terminate. Note that there is no such thing as a static destructor for a class.
The Finally Clause
C++/CLI supports an extension to the try/catch block construct, namely, a finally clause. Its block is always executed, regardless of whether the corresponding try block results in an exception. That is, the finally clause is executed after the try block terminates normally, or after any catch block associated with that try block terminates.
In case 6j, the finally clause simply sets the block-scope handle appDom to the null value, thereby giving up access to the AppDoma in object. Since that would happen anyway when the parent block exits, this usage is superfluous; however, it's a good opportunity to introduce this facility.
Event Handling
The CLI supports the notion of events. Simply stated, an event is a nonnative class member that enables an object or class to provide notifications. The standard CLI class System::AppDoma in contains several such events, and Microsoft's extended version contains even more, including ProcessExit, which is referenced in case 6h.
When a given event occurs, the functions that have been associated with that event are called in the order in which they were associated. In the simplest form, only one function is associated with an event, and this is achieved by simple assignment; that is, a delegate that encapsulates that one function is assigned to the event member. In the more general form, any number of functions can be associated with an event, at different times, via the compound-assignment operator +=.You use this operator in case 6h because you have no idea if that event already has associated handlers. If it did and you used simple assignment instead, those functions would no longer be associated with the event.
Each event has a type. In the case of ProcessExit, that type is System::EventHandler, which is a delegate type that can encapsulate functions taking two arguments of type System: :Object^ and System::EventArgs^, respectively, and having a void return type. And function ProcessExitHandler, defined in case 7, has exactly that signature. As such, in case 6h, you are registering that function as a handler to be called in the event of the process exiting. When that function is called, it overwrites the text file, writing out the ID value to be used for the next execution. The arguments passed in are ignored.
Delegates
According to the C++/CLI Standard:
A delegate definition defines a class that is derived from the class System::Delegate. A delegate instance encapsulates one or more member functions in an invocation list, each of which is referred to as a callable entity. For instance functions, a callable entity consists of an instance and a member function on that instance. For static functions, a callable entity consists of just a member function.
Given a delegate instance and an appropriate set of arguments, one can invoke all of that delegate instance's functions with that set of arguments. [Note: Unlike a pointer to member function, a delegate instance can be bound to members of arbitrary classes, as long as the function signatures are compatible with the delegate's type. Tills makes delegates suited for "anonymous" invocation.]
In this case, you're dealing with a delegate type defined in the CLI Library, namely, System::EventHandler. However, you can define your own delegate types using the contextually reserved identifier delegate. You create an instance of a delegate via gcnew, as in case 6h. Since the function being encapsulated is static, the constructor call is given only one argument, a pointer-to-member to the function ProcessExitHandler, whose signature must match that of the delegate. (To encapsulate an instance function, an extra [first] argument that is a handle to the instance itself must be supplied.)
Other Changes to Point
The read/write TraceID property is defined in case 8, and used in case 12.
As the three constructors (cases 9, 10, and 11) all create new instances of Point, they need to assign a unique value to ID. Since all other member functions operate on an existing instance, they do not change any IDs. Initialization occurs only when an object is created, and hence needs a new ID, while assignment happens after the object is created, so no new ID is needed.
In case 12, GetHashCode returns an int, which is exactly the type of ID. As such, this function could simply return that value, thereby guaranteeing a unique hash value. (Of course, if ID's type were unsigned or long long, you'd need to somehow reduce it to an int.)
The decision of whether to include the ID prefix is made in ToString, in case 13.
initonly Fields
A field declared with the initonly identifier in a nonnative class is an 1value only within the ctor-initializer and the body of a constructor, or within a static constructor; in all other contexts, it is an rvalue. (Specifically, a static initonly field can only be modified by a static constructor, while an instance initonly field can only be modified by an instance constructor.) This allows that field to be treated as read-only, except when a class is first used or when an instance is constructed. For example, consider some sort of engineering data type that has a static table of coefficients, whose values must be read from a file each time the application is started, but thereafter, should be treated as read-only. Listing 3 contains such a scenario.
The static array handle coefficients are declared to be initonly in case 1. In the static constructor, a file of coefficients is opened, the number is determined, an array of that number of elements is allocated, and the values are read from the file and stored in the array.
Rather than make the array handle public and let the application programmer subscript it directly, the array is hidden behind a read-only named indexed property. (The presence of the square brackets makes it an indexed property.) These delimit a comma-separated list of index types, in this case, one, an int, which means that you can index this class using one subscript. (The index types need not be an integer.) You index the property in case 4 by using the obvious notation. (Like multidimensional array subscripts, indexed accesses to an indexed property use a comma-separated list of indexes inside a single set of []; the comma acts as a punctuator.)
C++/CLI allows the keyword default as the "name" of an indexed property, in which case, an instance's name is indexed directly, without a member name being used. However, this is only permitted for instance-indexed properties, so we can't use it here. As such, the property has been given the name Coeff.
Unlike literal fields (discussed last month), an initonly field is not a named compile-time constant, so it need not contain an initializer having a constant value. Nor is an initonly field limited to having a scalar type.
If a class contains any initonly fields having initializers, they are initialized in the order in which they are declared, immediately prior to the static constructor being executed.
Could we have made the nextAvailableID in the Point class initonly? After all, it is only modified in constructors. Unfortunately, you can't because it's a static member, and that can only be updated in a static constructor, while you need to update it in three different instance constructors.
Reader Exercises
Here are some things you might want to do to reinforce what I've presented:
• Read up on the AppDomain, File, StreamReader, and StreamWriter classes.
• Add extra catch blocks for other file-open and read-related errors.
• What if the string read-in can't be converted to an integer? (Look at the documentation for Int32::Parse.)
• The file is opened, read, and closed at startup, then created, written, and closed at shutdown. Would it be useful to keep the file open throughout the execution and rewrite the ID? If so, which specific I/O functions would be needed?
• Look at the CLI Library exception hierarchy, starting with System::IO::FileNotFoundException and going backwards up the tree.
• Can the same function be registered multiple times for a given event?
• Given that a registered event-handler function can be disassociated from an event, which operator might be used to do that?
• Conceptually, what would it take to support multiple programs (or multiple threads in the same program) running concurrently that all needed to assign unique Point IDs?
• Use ildasm to show that in metadata, a static constructor has the name .cctor, whereas an instance constructor is called .ctor.
• Define a class with no static constructor and a static initonly field that has a nonconstant initializer (such as a function call). How does the initializer code get executed if it isn't inside any function? (Hint: Use ildasm.)
Listing 1
using namespace System;
Point F(Point p) {
return p;
}
int main()
{
/*1*/ Point::TraceID = true :
/*2*/ Point^ hp1 = gcnew Point;
Console::Writeline("hp1: {D}", hp1);
/*3*/ hp1 - > Move (6, 7);
Console::Writeline("hp1: {0}", hp1) ;
/*4*/ Point^ hp2 = gcnew Point(3,4);
Console: :Writeline("hp2: {0}", hp2);
/*5*/ Point p1, p2(-1,-2);
Console::Writeline("p1: {0}. p2: [1])", %p1, %p2);
/*6*/ p1- F(p2);
Console::Writeline("p1: {0}", %p1);
}
Listing 2
using namespace System;
using namespace System::IO;
public ref class Point
{
int x;
int y;
/*1*/ int ID;
/*2*/ static int nextAvailableID;
/*3*/ static int nextAvailableID() { return nextAvailableiD++; }
/*4*/ static bool traceID = false;
/*5*/ static String^ masterFileLocation;
/*6*/ static Point()
{
/*6a*/ AppDomain^ appDom = AppDomain::CurrentDomain;
/*6b*/ masterFilelocation = String::Concat(appDom - >BaseDirectory,"\\PointiD.txt");
/*6c*/ try {
/*6d*/ StreamReader^ inStream = File::OpenText(masterFilelocation);
/*6e*/ String^ s = inStream- >Readline();
/*6f/ nextAvailableID = Int32::Parse(s);
/*6g*/ inStream- >Close();
/*6h*/ appDom- >ProcessExit += gcnew
EventHandler(&Point::ProcessExitHandler);
}
/*6i*/ catch (FileNotFoundException^ ioFNFEx)
{
//take appropriate action
}
/*6j*/ finally
{
appDom = nullptr;
}
}
/*7*/ static void ProcessExitHandler(Object^ sender, EventArgs^ e)
{
/*7a*/ StreamWriter^ outStream = File::CreateText(masterFilelocation);
/*7b*/ outStream- >Writeline ( "{0}", nextAvailable ID);
/*7c*/ outStream- >Close();
}
public :
// ...
(
/*8*/ static property bool TraceiD
{
bool get() [ return traceiD; J
void set(bool val) { trace ID - val: }
}
// define instance constructors
Point()
{
/*9*/ ID = GetNextAvailableID();
X = 0;
y = 0;
}
Point(int xor, int yor)
{
/*10*/ ID = GetNextAvailableID();
X = xor;
Y = yor;
}
Point(Point% p) //copy constructor
{
/*11*/ ID = GetNextAvailableID();
X = p. X;
y = p. Y;
}
// ...
/*12*/ virtual int GetHashCode() override
{
// ...
}
virtual String^ ToString() override
{
/*13*/ if (traceID)
{
return String::Format("[{0}]({1},{2})", ID, X, Y);
}
else
{
return String::Format("({O},{l})", X, Y);
}
}
};
Listing 3
using namespace System;
public ref class EngineeringData
{
/*l*/ static initonly array<double>^ coefficients;
/*2*/ static EngineeringData()
{
int elementCount;
// figure out how big array should be
// elementCount = ...
coefficients = gcnew array<double>(elementCount);
for (int i = 0; i < elementCount; ++i)
{
// coefficients [i] = ...
}
}
public:
/*3*/ static property double Coeff[int] {
double get (int index) { return coefficients[index]; }
}
};
int main()
{ doubled;
try {
/*4*/ d = EngineeringData::Coeff[2];
}
catch (IndexOutOfRangeException^ ex)
{
// handle exception
}
}
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 [email protected].
C/C++ Users Journal - Advanced Solutions For Professional Developers, Vol. 23, No 4, April 2005, Pg 44-48