C/C++ Users Journal February, 2005
Last month I introduced a reasonably complete and useful ref class called Point, which models a two-dimensional point. This month, I expand on various aspects of that type's implementation, and look at some new topics.
In traditional C++ design and implementation, you define each type being modeled, in its own header, with that header containing the type's name, its members' names and types, and the inline definition of relatively trivial member functions.
Rather than having separately compiled source files share information via a header, in C++/CLI such information is shared via an assembly. As with the Point class, it was compiled on its own, resulting in an assembly called "Point.dll." Any application needing that type's definition must be compiled and linked against that type's assembly. This requires that that type's definition be at least complete enough for it to be compiled and linked to DLL form. As such, all functions declared in that type must also be defined; otherwise, linker errors result.
You can declare the member function GetHashCode, for example, inside the class Point and define it outside that class, but in the same source file (see Listing 1). However, placing that member function's definition in a separate source file is not an option, even if that source file were compiled at the same time as that for Point.cpp, as input to the same assembly. To compile such a file requires access to the assembly Point.dll, yet that is precisely the assembly being generated by this compilation! (Note the absence of inline on the function's definition; I'll discuss that later.)
The implication of not using headers is that to compile and link any assembly, all of the type assemblies on which that assembly depends must already have been compiled and linked.
In Point, the definition of every member function is written inline, and this is no accident. Apart from having the flexibility to define them out-of-line, but later in the same source file, member functions cannot be defined in source files separate from the type definition itself.
The traditional model of code inlining is that the declaration of a function as inline is a hint to the compiler to actually inline it as it sees fit, possibly trading program size for speed. However, such optimizations are limited to that compilation, yet exploit having such inline function definitions defined in a header. When class Point is compiled, inlining can be applied by the compiler on calls to member functions from within that type. For example, all gets and sets of the X and Y properties within the definition of Point itself can be inlined.
What about code that uses Point from a different assembly? Can its calls to Point's member functions also be inlined? In theory, yes. After all, to compile application code, the compiler needs access to Point's assembly so it could see exactly how any given member function was implemented, allowing references to that function to be optimized.
Consider the case of GetHashCode. Given its simple content, it is a likely candidate for inlining. Assume calls to it from an application assembly are inlined, then later that function is reimplemented using a different algorithm. If the application assembly were not rebuilt, it would continue to use the hashcode algorithm inlined inside it rather than the new version. Since this is almost certainly not the desired behavior, inlining across assembly boundaries is a bad idea. But then, so is not inlining gets and sets of the trivially implemented X and Y properties.
Fortunately, optimization is possible at other than compile time. For example, in the simplest execution model, each time a program is run, its CIL instructions are executed. However, a Just-in-Time (JIT) compiler can recognize certain code patterns, and perform various optimizations, such as code inlining. Large, complex programs can be installed such that they are compiled to native code once per installation. That way, the optimizations don't need to be done each time the program is executed.
The out-of-line definition of GetHashCode was not declared inline. If this approach were used in a header, multiple includes of that header would result in multiple definitions of the same name, giving rise to linker errors, at least on some systems. However, since this approach is in an assembly rather than a header, no such error occurs; there is only ever one definition of this function. Ordinarily, you can use inline in this context, but it isn't necessary. In fact, in this particular case, it isn't allowed since any function declared to be an override cannot also be marked inline.
Last month, I pointed out that while a property's getter and setter can have different accessibilities, doing so can hinder language interoperability. One of the goals of CLI is to promote such interoperability, without requiring it. It does this by defining a Common Language Specification (CLS) [1] and a set of CLS rules. For example, Rule 25 states, "The accessibility of a property's accessors shall be identical."
When implementing a type for use in a CLI environment, you need to consider the way in which you export various aspects of that type, such as member function signatures. For example, not all CLI-based languages support unsigned integer or pointer types, and few of them understand const- and volatile-qualified types.
The CLS requires that languages not supporting certain features can still access them via function-call syntax. It is for this reason that the getter and setter for a property called X are really called get_X and set_X, respectively, in the metadata. Similarly, there are metadata names for operator functions, so they can be called from languages having no notion of operator overloading.
For a ref class, equality is implemented via a function called Equals, rather than by overloading operator==. However, it is possible to override that operator (see Listing 2).
In case 1, you reject null-valued handles. However, if you had used p1 and p2 instead of o1 and o2, you'd have been calling yourself recursively; hence the implicit conversions to Object^. By making this function static, it becomes CLS compliant. (Nonstatic operator functions are not CLS compliant.)
Except for the handle notation, this operator function's signature looks much like the corresponding version written in traditional C++. However, the big difference comes when you use this operator. Consider if (p == q), where p and q both have type Point^. The problem is that the reader is likely to think that two handles are being compared when, in fact, it's the two Points those handles refer to that are being compared. To actually compare the handles, you need to write something like:
if (static_cast<Object^>(p1) == static_cast<Object^>(p2))
Although you can provide this operator function for class Point, you still need to provide Equals. If you don't, anyone calling Equals on a Point will get the version in System::Object, and that version tests for referential equality not value equality. That is, it returns true if the specified instance of Object and the current instance are the same instance; otherwise, it returns false.
As they exist in Standard C++, arrays are just like those in C, so they have the same advantages and disadvantages. Namely, they are allocated space at compile time, they have a fixed size, and array-bounds checking is not required. There is no such thing as a multidimensional array; instead, you can have an array of array, of array, and so on. I refer to such arrays as "native arrays."
In the CLI world, arrays are objects and are allocated on the garbage-collected heap. Their size need not be known at compile time, array-bounds checking is automatically done at runtime, and true multidimensional arrays are supported. As such, you need new syntax to express such CLI arrays.
Consider Listing 3. Like instances of ref classes, CLI array objects have no name, per se; rather, they are accessed via handles to them. As we can see in cases 1, 2, 3, and 4, an array type is written using template-like notation, as in array<int> and array<Point^>. (In earlier implementations of C++/CLI, it was necessary to have a using directive for namespace cli::language for the compiler to understand this notation. This is no longer necessary.) Note carefully that in cases 1 and 2, I am defining handles to arrays, not arrays, and that in case 2, the array type is "array of handles to Point," not "array of Point."
By default, automatic handles take on the value nullptr; however, in both these cases, I have initialized the handle to a block of memory allocated via gcnew. This operator is followed by the type of the array, an optional parenthesized element count, and an optional brace-delimited initializer list. If the initializer list is omitted, the elements take on their default value. If the element count (along with its delimiting parentheses) is omitted, the number allocated is the number of expressions in the initializer list. If the specified count is greater than the number in the list, the remaining elements take on their default value. (For example, numbers[4] is zero.) The count and initializer cannot both be omitted. The count and initializer expressions need not be constants. An element count can be zero, and an initializer list can be empty; both indicate an array of zero elements, which is quite different than having no array at all.
Note that in case 2, the element count is omitted. If it were specified as 3, for example, and the same initializer list were used, the third element would be initialized to nullptr, not to a handle to a Point constructed with the default constructor, as you might think or want.
In case 3, I set the int array handle numbers to a new locationone that contains an array of three ints. That results in one less handle to the array of five ints, and if that was the only handle to that space, it will eventually be recovered by the garbage collector. So although an array has a fixed size, a handle to an array of some dimension can be made to refer to any array of that type and dimension, regardless of its element count (which is maintained by the system).
As you can see in cases 5a, 5b, and 5c, a one-dimensional CLI array can be subscripted in the expected manner.
The Display1DArray function displays the text given as its first argument, followed by the number of elements in the array designated by the second argument, and the value of each of those elements (if any). Here is the output produced:
numbers 5: 10 20 30 40 0 points 2: (3,4) (5,7) numbers 3: 55 66 77 numbers 0: points[0] is (2,5)
Listing 4 is the source for Display1DArray. (For now, treat the keyword generic as if it were template.)
Clearly, the parameter ary is a handle to a CLI array of type T; however, not only can ary be given a handle to such an array, but that handle could have the value nullptr. So in case 6, you guard against that.
In case 7, I display the text passed in, along with the number of elements in the array. You obtain the latter from the read-only property Length, which all arrays have. (All CLI arrays are implicitly derived from class System::Array, which has this property.)
You then proceed to cycle through the array in case 8, displaying each element on the same line as you go, terminating that line with a newline in case 9. Rather than using for to vary an integer index from zero to ary->Length - 1, you use the new loop statement, for each. (Together, these two tokens make up a keyword.) This construct allows the elements of a collection to be enumerated. I won't go into the details here of how this works, but suffice it to say that a CLI array is a CLI collection, so its elements can be traversed using this construct. Using this approach, however, you cannot get access to each element's index as you go. The reason for this is that not all collections are linear (for example, a binary tree), in which case, indexing makes no sense.
When you subscript a CLI array, you are not using operator[] on that array; instead, you are using an indexed property called Item, defined on System::Array. While a scalar property has a single value, an indexed property can have many values, with each one being accessed via subscript notation.
To make Display1DArray be array-type agnostic, it makes perfect sense to make it a template function. However, template use is a compile-time operation, resulting in one copy of the function generated for each array type used in the program. In V2, the CLI has added support for generics, a template-like facility that uses a single copy of code at runtime. generic is a context-dependent identifier. (As you might expect, you can also have generic types.)
C++/CLI supports true multidimensional arrays (see Listing 5).
The pseudotemplate array takes an optional second argument, which is the rank (that is, the number of dimensions) of the array. This defaults to 1. As such, in the previous example, array<int> could have been written as array<int,1>. (Like all nontype arguments to templates, this one has to be a compile-time constant.)
In case 2, I define a handle to a two-dimensional array of handles to String, but I don't mention the number of rows or columns. I then allocate memory for a 2×3 array of that type and initialize the six elements with the five strings shown, plus a nullptr. Here is the output:
names has 6 elements names has 2 dimensions names[0,0] is John names has 35 elements
The Length property gives the total number of elements, while the Rank property gives the number of dimensions.
Note carefully in case 4 that accessing an element in a multidimensional array involves only one set of brackets, which contains a comma-separated list of indexes. In this context, the comma behaves as a punctuator rather than an operator.
You can make names refer to any two-dimensional array of String^. For example, in case 5, I make it refer to a 5×7 array whose elements' values are all nullptr.
Last month, I introduced the following overload of String::Concat:
static String^ Concat(... array<Object^>^ list);
The ellipses notation at the beginning of the final (in this case, the only) parameter declaration (which must have a CLI array type) indicates that this parameter accepts an arbitrary number of arguments of the given element type.
Listing 6 presents a function that takes a variable number of Point handles as arguments and returns the left-most X coordinate. (Of course, this function could be made a static member function of class Point.)
The output produced is as follows:
LeftMostX is 1 LeftMostX is -5
In case 2, I call LeftMostX, passing it three Point handles. However, behind the scenes, that function really only takes one argumenta handle to an array of "handle to Point." As such, the compiler marshals the three Point handles passed into an array, and passes a handle to that. You can take advantage of this knowledge by passing a handle to an array of Points directly, as in case 3.
The only thing new in the definition of LeftMostX is case 4. Each of the primitive C++ types maps to a corresponding implementation-defined type in the CLI library. For example, in the Microsoft implementation, short maps to System::Int16, int maps to System::Int32, and long long maps to System::Int64. Each of these types is a value class type, instances of which are allocated on the stack rather than on the garbage-collected heap.
MaxValue is a public static field in type Int32 that has the value 2,147,483,647, the largest value for a signed 32-bit twos-complement integer. (Strictly speaking, MaxValue is a literal field.)
To reinforce the material covered, you might want to perform the following:
[1] See the CLI Standard, Partition I, Clauses 7 and 10 (http://www.ecma-international.org/publications/index.html).