11
■ ■ ■
CHAPTER 2
A Quick Tour of the C++/CLI
Language Features
T
he aim of this chapter is to give you a general idea of what C++/CLI is all about by providing
a brief look at most of the new language features in the context of an extended example, saving
the details for later chapters. By the end of this chapter, you’ll have a good idea of the scope of
the most important changes and will be able to start writing some code.
Primitive Types
The CLI contains a definition of a new type system called the common type system (CTS). It is
the task of a .NET language to map its own type system to the CTS. Table 2-1 shows the
mapping for C++/CLI.
Table 2-1. Primitive Types and the Common Type System
CLI Type C++/CLI Keyword Declaration Description
Boolean bool bool isValid = true; true or false
Byte unsigned char unsigned char c = 'a'; 8-bit unsigned integer
Char wchar_t wchar_t wc = 'a' or L'a'; Unicode character
Double double double d = 1.0E-13; 8-byte double-precision
floating-point number
Int16 short short s = 123; 16-bit signed integer
Int32 long, int int i = -1000000; 32-bit signed integer
Int64 __int64, long long __int64 i64 = 2000; 64-bit signed integer
SByte char char c = 'a'; Signed 8-bit integer
Single float float f = 1.04f; 4-byte single-precision
floating-point number
Hogenson_705-2C02.fm Page 11 Friday, October 13, 2006 2:14 PM
12
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
In this book, the term managed type refers to any of the CLI types mentioned in Table 2-1,
or any of the aggregate types (ref class, value class, etc.) mentioned in the next section.
Aggregate Types
Aggregate types in C++ include structures, unions, classes, and so on. C++/CLI provides managed
aggregate types. The CTS supports several kinds of aggregate types:
• ref class and ref struct, a reference type representing an object
• value class and value struct, usually a small object representing a value
• enum class
• interface class, an interface only, with no implementation, inherited by classes and
other interfaces
• Managed arrays
• Parameterized types, which are types that contain at least one unspecified type that may
be substituted by a real type when the parameterized type is used
Let’s explore these concepts together by developing some code to make a simple model of
atoms and radioactive decay. First, consider an atom. To start, we’ll want to model its position
and what type of atom it is. In this initial model, we’re going to consider atoms to be like the
billiard balls they were once thought to be, before the quantum revolution changed all that. So
we will for the moment consider that an atom has a definite position in three-dimensional
space. In classic C++, we might create a class like the one in the upcoming listing, choosing to
reflect the atomic number—the number of protons, which determines what type of element it
is; and the isotope number—the number of protons plus the number of neutrons, which deter-
mines which isotope of the element it is. The isotope number can make a very innocuous or a
very explosive difference in practical terms (and in geopolitical terms). For example, you may
have heard of carbon dating, in which the amount of radioactive carbon-14 is measured to
determine the age of wood or other organic materials. Carbon can have an isotope number
of 12, 13, or 14. The most common isotope of carbon is carbon-12, whereas carbon-14 is a
radioactive isotope. You may also have heard a lot of controversy about isotopes of uranium.
UInt16 unsigned short unsigned short s = 15; Unsigned 16-bit
signed integer
UInt32 unsigned long,
unsigned int
unsigned int i = 500000; Unsigned 32-bit
signed integer
UInt64 unsigned __int64,
unsigned long long
unsigned __int64 i64 = 400; Unsigned 64-bit integer
Void void n/a Untyped data or no data
Table 2-1. Primitive Types and the Common Type System (Continued)
CLI Type C++/CLI Keyword Declaration Description
Hogenson_705-2C02.fm Page 12 Friday, October 13, 2006 2:14 PM
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
13
There’s a huge geopolitical difference between uranium-238, which is merely mildly radioactive,
and uranium-235, which is the principal ingredient of a nuclear bomb.
In this chapter, together we’ll create a program that simulates radioactive decay, with
specific reference to carbon-14 decay used in carbon dating. We’ll start with a fairly crude
example, but by the end of the chapter, we’ll make it better using C++/CLI constructs. Radio-
active decay is the process by which an atom changes into another type of atom by some kind
of alteration in the nucleus. These alterations result in changes that transform the atom into a
different element. Carbon-14, for example, undergoes radioactive decay by emitting an electron
and changing into nitrogen-14. This type of radioactive decay is referred to as β
-
(beta minus or
simply beta) decay, and always results in a neutron turning into a proton in the nucleus, thus
increasing the atomic number by 1. Other forms of decay include β
+
(beta plus or positron)
decay, in which a positron is emitted, or alpha decay, in which an alpha particle (two protons
and two neutrons) is ejected from the nucleus. Figure 2-1 illustrates beta decay for carbon-14.
Figure 2-1. Beta decay. Carbon-14 decays into nitrogen-14 by emitting an electron. Neutrons are
shown in black; protons in gray.
Listing 2-1 shows our native C++ class modeling the atom.
Listing 2-1. Modeling an Atom in Native C++
// atom.cpp
class Atom
{
private:
double pos[3];
unsigned int atomicNumber;
unsigned int isotopeNumber;
public:
Atom() : atomicNumber(1), isotopeNumber(1)
{
// Let's say we most often use hydrogen atoms,
// so there is a default constructor that assumes you are
// creating a hydrogen atom.
pos[0] = 0; pos[1] = 0; pos[2] = 0;
}
Hogenson_705-2C02.fm Page 13 Friday, October 13, 2006 2:14 PM
14
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
Atom(double x, double y, double z, unsigned int a, unsigned int n)
: atomicNumber(a), isotopeNumber(n)
{
pos[0] = x; pos[1] = y; pos[2] = z;
}
unsigned int GetAtomicNumber() { return atomicNumber; }
void SetAtomicNumber(unsigned int a) { atomicNumber = a; }
unsigned int GetIsotopeNumber() { return isotopeNumber; }
void SetIsotopeNumber(unsigned int n) { isotopeNumber = n; }
double GetPosition(int index) { return pos[index]; }
void SetPosition(int index, double value) { pos[index] = value; }
};
You could compile the class unchanged in C++/CLI with the following command line:
cl /clr atom.cpp
and it would be a valid C++/CLI program. That’s because C++/CLI is a superset of C++, so any
C++ class or program is a C++/CLI class or program. In C++/CLI, the type in Listing 2-1 (or any
type that could have been written in classic C++) is a native type.
Reference Classes
Recall that the managed types use ref class (or value class, etc.), whereas the native classes
just use class in the declaration. Reference classes are often informally referred to as ref classes
or ref types. What happens if we just change class Atom to ref class Atom to see whether that
makes it a valid reference type? (The /LD option tells the linker to generate a DLL instead of an
executable.)
C:\ >cl /clr /LD atom1.cpp
atom1.cpp(4) : error C4368: cannot define 'pos' as a member of managed 'Atom':
mixed types are not supported
Well, it doesn’t work. Looks like there are some things that we cannot use in a managed
type. The compiler is telling us that we’re trying to use a native type in a reference type, which
is not allowed. (In Chapter 12, you’ll see how to use interoperability features to allow some
mixing.)
I mentioned that there is something called a managed array. Using that instead of the
native array should fix the problem, as in Listing 2-2.
Listing 2-2. Using a Managed Array
// atom_managed.cpp
ref class Atom
{
private:
array<double>^ pos; // Declare the managed array.
unsigned int atomicNumber;
unsigned int isotopeNumber;
Hogenson_705-2C02.fm Page 14 Friday, October 13, 2006 2:14 PM
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
15
public:
Atom()
{
// We'll need to allocate space for the position values.
pos = gcnew array<double>(3);
pos[0] = 0; pos[1] = 0; pos[2] = 0;
atomicNumber = 1;
isotopeNumber = 1;
}
Atom(double x, double y, double z, unsigned int atNo, unsigned int n)
: atomicNumber(atNo), isotopeNumber(n)
{
// Create the managed array.
pos = gcnew array<double>(3);
pos[0] = x; pos[1] = y; pos[2] = z;
}
// The rest of the class declaration is unchanged.
};
So we have a ref class Atom with a managed array, and the rest of the code still works. In
the managed type system, the array type is a type inheriting from Object, like all types in the
CTS. Note the syntax used to declare the array. We use the angle brackets suggestive of a template
argument to specify the type of the array. Don’t be deceived—it is not a real template type.
Notice that we also use the handle symbol, indicating that pos is a handle to a type. Also, we use
gcnew to create the array, specifying the type and the number of elements in the constructor
argument instead of using square brackets in the declaration. The managed array is a reference
type, so the array and its values are allocated on the managed heap.
So what exactly can you embed as fields in a managed type? You can embed the types
in the CTS, including primitive types, since they all have counterparts in the CLI: double is
System::Double, and so on. You cannot use a native array or native subobject. However, there
is a way to reference a native class in a managed class, as you’ll see in Chapter 12.
Value Classes
You may be wondering if, like the Hello type in the previous chapter, you could also have
created Atom as a value type. If you only change ref to value and recompile, you get an error
message that states “value types cannot define special member functions”—this is because of
the definition of the default constructor, which counts as a special member function. Thanks
to the compiler, value types always act as if they have a built-in default constructor that initializes
the data members to their default values (e.g., zero, false, etc.). In reality, there is no constructor
emitted, but the fields are initialized to their default values by the CLR. This enables arrays of
value types to be created very efficiently, but of course limits their usefulness to situations
where a zero value is meaningful.
Let’s say you try to satisfy the compiler and remove the default constructor. Now, you’ve
created a problem. If you create an atom using the built-in default constructor, you’ll have
atoms with atomic number zero, which wouldn’t be an atom at all. Arrays of value types don’t
call the constructor; instead, they make use of the runtime’s initialization of the value type
Hogenson_705-2C02.fm Page 15 Friday, October 13, 2006 2:14 PM
16
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
fields to zero, so if you wanted to create arrays of atoms, you would have to initialize them after
constructing them. You could certainly add an Initialize function to the class to do that, but
if some other programmer comes along later and tries to use the atoms before they’re initial-
ized, that programmer will get nonsense (see Listing 2-3).
Listing 2-3. C++/CLI’s Version of Heisenberg Uncertainty
void atoms()
{
int n_atoms = 50;
array<Atom>^ atoms = gcnew array<Atom>(n_atoms);
// Between the array creation and initialization,
// the atoms are in an invalid state.
// Don't call GetAtomicNumber here!
for (int i = 0; i < n_atoms; i++)
{
atoms[i].Initialize( /* ... */ );
}
}
Depending on how important this particular drawback is to you, you might decide that a
value type just won’t work. You have to look at the problem and determine whether the features
available in a value type are sufficient to model the problem effectively. Listing 2-4 provides an
example where a value type definitely makes sense: a Point class.
Listing 2-4. Defining a Value Type for Points in 3D Space
// value_struct.cpp
value struct Point3D
{
double x;
double y;
double z;
};
Using this structure instead of the array makes the Atom class look like Listing 2-5.
Listing 2-5. Using a Value Type Instead of an Array
ref class Atom
{
private:
Point3D position;
unsigned int atomicNumber;
unsigned int isotopeNumber;
Hogenson_705-2C02.fm Page 16 Friday, October 13, 2006 2:14 PM
CHAPTER 2
■
A QUICK TOUR OF THE C++/CLI LANGUAGE FEATURES
17
public:
Atom(Point3D pos, unsigned int a, unsigned int n)
: position(pos), atomicNumber(a), isotopeNumber(n)
{ }
Point3D GetPosition()
{
return position;
}
void SetPosition(Point3D new_position)
{
position = new_position;
}
// The rest of the code is unchanged.
};
The value type Point3D is used as a member, return value, and parameter type. In all cases
you use it without the handle. You’ll see later that you can have a handle to a value type, but as
this code is written, the value type is copied when it is used as a parameter, and when it is returned.
Also, when used as a member for the position field, it takes up space in the memory layout of
the containing Atom class, rather than existing in an independent location. This is different
from the managed array implementation, in which the elements in the pos array were in a
separate heap location. Intensive computations with this class using the value struct should be
faster than the array implementation. This is the sweet spot for value types—they are very effi-
cient for small objects.
Enumeration Classes
So, you’ve seen all the managed aggregate types except interface classes and enumeration
classes. The enumeration class (or enum class for short) is pretty straightforward. It looks a lot
like a classic C++ enum, and like the C++ enum, it defines a series of named values. It’s actually
a value type. Listing 2-6 is an example of an enum class.
Listing 2-6. Declaring an Enum Class
// elements_enum.cpp
enum class Element
{
Hydrogen = 1, Helium, Lithium, Beryllium, Boron, Carbon, Nitrogen, Oxygen,
Fluorine, Neon
// ... 100 or so other elements omitted for brevity
};
Hogenson_705-2C02.fm Page 17 Friday, October 13, 2006 2:14 PM