Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C++/CLI: Value Class Types


May, 2005: C++/CLI: Value Class Types

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].


Until now, all the class types I've used in this series have been ref classes, which means that instances of them—including those declared on the stack—are managed by the garbage collector. This month, I look at what is often referred to as a "lightweight" class mechanism; namely, the value class, instances of which are not managed by the garbage collector.

Value class types are particularly useful for reasonably small data structures that have value semantics. Examples include points in a coordinate system and complex numbers. Typically, a good candidate for implementation as a value class will have only a few data members, will not require inheritance, and will not be expensive in terms of passing and returning by value or copying during assignment.

Point As a Value Class

Let's take the Point class from the March 2005 installment of this series, and turn it into a value class (see Listing 1). This is done by replacing ref with value. Like ref class, value class is a keyword containing whitespace. And as you should expect, the only difference between a value class and a value struct is that the default accessibility for the former is private, while that for the latter is public.

A value class automatically derives from System::ValueType (which, in turn, derives from System::Object); however, this cannot be declared explicitly. A value class is also implicitly "sealed"; that is, it cannot be used as a base class. (As a result, there is no point in giving a value class member an access specifier of protected; however, that is not prohibited.) Any value (or ref) class X can be declared sealed explicitly:

value class X sealed {/*...*/};

Note the absence of a default constructor, which, in the ref class version, sets the x- and y-coordinates to zero. For a value class, the CLI itself sets all fields in any instance of that class to all-bits-zero; you cannot provide your own default constructor. For our Point type, this means a default set of coordinates of (0,0), and that is acceptable; however, zero, false, and/or nullptr might not be appropriate default values for fields in other types, so this requirement may rule out a value class's being used instead of a ref class for certain types. (A conforming C++/CLI implementation is required to represent the values false and nullptr as all-bits-zero.)

Another restriction on value classes is that they come with a default copy constructor and assignment operator, both of which perform bitwise copies. You cannot provide your own versions of these functions.

Function Equals can be made a bit simpler than the ref class version, but not much. Remember, we are overriding the version defined in System::Object, and that takes an Object^. Since an argument of that type could have the value nullptr, I deal with it first. The step you can omit is that which checks to see if you are being asked to compare something against itself. For ref class implementations of Equals, this step is necessary, as an infinite number of handles can refer to the same object. However, in the case of a value class, no two instances can ever represent the same instance. Two instances can represent points having the exact same coordinates, but changing the x-coordinate of one Point does not change that in the other.

When an instance of a Point is passed to Equals, being of a value class type (which is ultimately derived from System::Object), boxing occurs. That is, an instance of Object is allocated on the garbage-collected heap, and that instance contains a copy of the Point passed. Because I have just created a new object, and there is only one handle to it, it can never be the same Point as any other Point.

The operator== function previously taking Point handles has been reduced to one line. Instead of handles, it now takes Point values, and the arrow member selection operators have each been replaced by a dot operator. Given that a value class type is sealed, the only thing compatible with a parameter of type value class Point, is a value of that exact type. As such, you can dispense with checking against nullptr, checking to see if you are being compared with yourself, and whether the two objects passed have exactly the same type.

The operator== function previously taking tracking references can be kept almost as is, but with the test for "same type" being removed. However, you can only keep one of these operator functions, as having more than one makes calls of the form point1 == point2 ambiguous. (In the declaration of this function's parameters, you can use the Standard C++ reference notation & instead of %, as these are interchangeable for native and value class types. Since instances of such types do not reside on the garbage-collected heap, their location cannot change during garbage collection, so their location need not be tracked.)

Listing 2 exercises most of the value class's members. The most obvious aspect of this program is that it contains instances of type Point that have static storage duration; that cannot be done for ref class types. In fact, not only can't you have a static instance of a ref class, you can't even have a static handle to such a type!

In the first call to Console::WriteLine, I pass a Point by value; however, what this method is expecting is an object reference. As such, the Point value is automatically boxed, and the reference to the resulting object is passed instead.

In its definition, p5 is initialized via the default copy constructor, while in the following statement, the default assignment operator assigns a bitwise copy of p2 to p5.

Assigning Unique Point IDs, Revisited

In the April 2005 installment, I showed how to add a unique ID facility to Point, and that involved a discussion of static constructors, basic file I/O, and event handlers. I revisit that topic and look at whether changing Point from a ref class to a value class makes any difference. (The short answer is, "It makes a great deal of difference!")

Again, you cannot define a default constructor, copy constructor, or assignment operator for a value class. Unfortunately, these are major problems for our ID solution. While the default constructor in our ref class version set both coordinates to zero, the same as for a value class, it also got the next available ID and assigned that to the ID instance field. With the value class version, we're stuck with a default ID of zero for every new Point constructed in that manner, and that is unacceptable; you simply need every new Point to be automatically assigned a unique ID!

A similar problem results from the lack of an explicit copy constructor operator. The whole purpose of such an operator is to initialize a brand new object as it is being born, yet the bitwise copy done for value classes causes the new object's ID to be the same as that from which its value is being copied.

In the case of assignment, we're setting the value of an existing Point, so that Point's ID must not change; that is, while either or both of its coordinates might change, it's still the same Point object. However, the bitwise copy done for value classes causes the destination Point's ID to be overwritten with that of the source.

There is some good news, however. Like a ref class, a value class can have static and nonstatic members (including a static constructor) as well as properties and virtual and nonvirtual functions and operators.

Although the source of the Point value class with ID "support" is not shown here (available at http://www.cuj.com/code/), a simple test program shows its deficiencies; see Listing 3. Here is the first declaration of that program, along with the output produced for each of the four Points after the first execution:

Point p1, p2(3,7), p3(9,1), p4 = p2;
p1 = [0](0,0)
p2 = [0](3,7)
p3 = [1](9,1)
p4 = [0](3,7)

Point p1 is created using the default constructor. As such, it gets an ID of zero, which is accidentally the correct ID for the first Point. And the default coordinate values are also zero, as required. In the case of p2, the programmer-written constructor was used, which assigned the next available ID, namely, zero. You now have duplicate IDs.

Likewise, p3 is given the next available ID, 1. Then, when p4 is given a bitwise copy of p2, p4 takes on the same ID as p2. In the case of assignment,

p2 = p1;
p2 = [0](0,0)

the bitwise copy of p1 to p2 results in both Points having p1's ID.

In the second execution of the test program, the output is:

p1 = [0](0,0)
p2 = [2](3,7)
p3 = [3](9,1)
p4 = [2](3,7)
p2 = [0](0,0)

As you can see, p1 always has ID zero.

Clearly, ref and value class types each have their own advantages, and not every use of one is interchangeable with the other.

Fundamental Type Mapping

As I've suggested in previous installments, all of the fundamental types in C++/CLI are really synonyms for value types defined in the CLI Library.

In the spirit of Standard C++, the mapping of fundamental types to CLI value types is almost entirely implementation defined. However, for Microsoft's Visual C++, that mapping is as listed in Table 1. (The only mapping required by the C++/CLI Standard is that for signed and unsigned char; these must be signed and unsigned 8-bit values, respectively.)

You've already seen another useful value type, namely System::Decimal; however, that has no corresponding C++/CLI type, and must be used directly by that name.

All public methods in a fundamental value's corresponding value class type are available to that fundamental value. Consider these expressions; each involves accessing a static or instance member of one of the aforementioned CLI value types:

Int32::MaxValue
Double::Parse("123.45e-1")
10.2f.ToString()
(10 + 5.9).ToString()
(100).ToString()
100 .ToString()

Using Visual C++ mapping, the type of 10.2f is float, which maps to System::Single, whose ToString function is then called. Similarly, (10 + 5.9) has type double, so System::Double's ToString() is called. Clearly, from a semantic point of view, the parentheses around the first 100, and the space following the second 100, are redundant. However, if both were omitted, the 100 followed by the period would be parsed as a double constant followed by an identifier, resulting in a syntax error.

Complex Numbers

A Complex number (that is, one that has a real and an imaginary part) is another example of a value class type (Listing 4 shows the important parts of a double version of such a class, although some important operator functions are missing.)

The CLI requires IEEE floating-point representation (more formally known as "IEC 10559"), and in that representation, zero in single and double precision is represented by all-bits-zero. As such, you can safely take the CLI-provided default constructor values.

Programmers trafficking in complex numbers like to use i, which represents the square root of -1. As such, a complex type would do well to provide a public read-only constant having that value. Complex achieves this via a combination of a public static initonly member and a static constructor. Because Complex is not a primitive type, you can't make i a readonly member. In any event, that would also require you to initialize it with a constant expression, but no such thing exists. So we do the next best thing by making i initonly and initializing it in the static constructor.

Listing 5 is a test program and the output produced.

Some Miscellaneous Issues

A value class type shall not contain:

  • A data member whose type is a native C++ array, native class type, or bit field.
  • A member function containing a local class.
  • A member that is a friend.
  • A destructor.

A value class can be passed to, and returned from, a function by value, address, reference, or tracking reference.

Within an instance constructor or member function of a ref class T, the type of this is "handle to T." However, for a value class type, the type of this is interior_ptr<T>, an obscure type whose meaning we'll leave for some other time. For now, "It just works!"

While instances of simple value class types such as Point and Complex are completely self-contained, a value class need not be. For example, like any ref class type, a value class type could contain pointers into the native heap and handles to objects on the garbage-collected heap. In such cases, cleaning up is not simply a matter of freeing the memory occupied by the instance of the value class type itself; it also requires cleaning up after each of that type's data members. But I leave the topic of destruction and finalization to another time.

Reader Exercises

Here are some things you might want to do to reinforce what I've presented:

  1. Take a look at the members of System::ValueType, the base type of all value classes.
  2. Why might it be a good habit to develop, to make all your ref classes sealed, unless you have good reason to do otherwise? (Because Standard C++ has no equivalent, C++ programmers have never had a reliable way to prohibit inheritance, in general, so they won't intuitively think of using a feature they've never had before.)
  3. Define a ref or value class with a data member of type char, and use ildasm to look at the metadata generated. Change the default signedness of a plain char from signed to unsigned (using Properties, C/C++, Language, Default Char Unsigned) and look at the difference in the metadata. Compare that with the metadata generated for two other members, having types signed char and unsigned char, respectively. (Remember, there are three distinct types of char in C++, so you can overload functions differing only on arguments of these types.)
  4. System::Char contains a family of Is* functions much like those available via the C Standard Library's ctype header. However, do Char::IsLetter and isalpha do the same thing? To be sure, try giving characters such as --, , and to each.
  5. Take a look at the complete set of members available in the CLI Library types to which the C++/CLI types map. (Amongst those that should be familiar are Equals, GetHashCode, and ToString.)
  6. What are the advantages or disadvantages of implementing Complex::i as a public static read-only property?
  7. Can the sizeof operator or the cstddef macro offsetof be applied to a value class type?
  8. The CLI has no concept of a union, per se. However, a value class can be forced to have an explicit layout, and each data member can be given a specific byte offset. By giving two data members the same byte offset, those members overlap. Implement a value class that contains a double, which is overlapped exactly by two ints, as well as eight unsigned chars. Store a value in the double member and display the corresponding values in the ints and unsigned chars. Use this code as a starting point:

using namespace System::Runtime::
  InteropServices;
   [StructLayout(LayoutKind::Explicit)]
   public value class Overlap
   {
      [FieldOffset(0)] double d;
      // ...
   };

  1. The two constructs inside [ and ] are attributes, and they correspond to calls to a constructor of a CLI class type having that name suffixed with Attribute. For example, FieldOffset(0) is a call to the constructor for the CLI Standard class FieldOffsetAttribute that takes one argument. (In a future installment, I use attributes to pass data structures to/from native C/C++ code.) In general, attributes can be applied to pretty much any kind of program element; however, each particular attribute type can only be applied to those program element types specified in its documentation. For example, StructLayout can only be applied to a ref or value class, while FieldOffset can only be applied to a data member of a ref or value class.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.