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