Share via


C++: The Most Powerful Language for .NET Framework Programming

 

Kenny Kerr
Software Consultant

July 2004

Applies to:
   Microsoft Visual C++ 2005
   Microsoft Visual C++ .NET
   Common Language Runtime (CLR)
   Microsoft Visual Studio 2005

Summary: Explore the design and rationale for the new C++/CLI language introduced with Visual C++ 2005. Use this knowledge to write powerful .NET applications with the most powerful programming language for .NET programming. (15 printed pages)

Contents

Introduction
Object Construction
Memory Management vs. Resource Management
Memory Management
Resource Management
Types Revisited
Boxing
Authoring Reference and Value Types
Accessibility
Properties
Delegates
Conclusion

Introduction

The Visual C++ team spent a lot of time listening to users and working with .NET and C++, and decided to redesign the support for the Common Language Runtime (CLR) in Visual C++ 2005. This redesign is called C++/CLI, and is intended to provide a more natural syntax for consuming and authoring CLR types. In this article I explore the new syntax, comparing it to current C# and Managed C++, the two most closely-related languages that target the CLR. Where appropriate I also illustrate analogous concepts in native C++.

The Common Language Infrastructure (CLI) is a group of specifications on which the Microsoft .NET initiative is based. The CLR is the Microsoft implementation of the CLI. The C++/CLI language design is aimed at providing natural C++ support for the CLI, while the Visual C++ 2005 compiler implements C++/CLI for the CLR.

There are two big messages that come through when you examine the upcoming Visual C++ 2005 compiler and the C++/CLI language design. Firstly, Visual C++ is positioning itself as the lowest level programming language for targeting the CLR. There should be no cause to use any other language, not even Microsoft intermediate language (MSIL). Secondly, .NET programming should be as natural as native C++ programming. As you read this article these two messages should become clear.

This article is for C++ programmers. I am not going to try to convince you to switch from C# or Visual Basic .NET. If you love C++ and want to use all the power that C++ has traditionally offered, but also want the productivity of C#, then this article is for you. In addition, this article does not provide an introduction to the CLR or the .NET Framework. Rather, the focus is on highlighting how Visual C++ 2005 allows you to write more elegant and efficient code targeting the .NET Framework.

Object Construction

The CLR defines two types, a value type and a reference type. Value types are designed to be efficiently allocated and accessed. They behave much like the built-in types in C++, and you can also create your own. This is what Bjarne Stroustrup calls concrete types. Reference types, on the other hand, are designed to provide all the features you would expect from object-oriented programming, such as the ability to create class hierarchies and all that goes along with that: derived classes and virtual functions, for example. Reference types, through the CLR, also provide additional runtime features such as automatic memory management, known as garbage collection. The CLR also provides sophisticated runtime type information for both reference and value types. This capability is referred to as reflection.

Value types are allocated on the stack. Reference types are allocated on the managed heap. This is the heap that is managed by the CLR's garbage collector (GC). If you are developing your assembly in C++, you can also allocate native C++ types on the CRT heap as you have always done. In the future, the Visual C++ team would like to allow you to even allocate native C++ types on the managed heap. After all, garbage collection is an equally attractive proposition for native types.

Native C++ lets you choose where to create a given object. Any type can be allocated on the stack or the CRT heap.

// allocated on the stack
std::wstring stackObject; 

// allocated on the CRT heap
std::wstring* heapObject = new std::wstring; 

As you can see, the choice of where to allocate the object is independent of the type, and is totally in the hands of the programmer. In addition, the syntax for stack versus heap allocation is distinctive.

C#, on the other hand, lets you create value types on the stack and reference types on the heap. The System.DateTime type, used in the next few examples, is declared a value type by its author.

// allocated on the stack
System.DateTime stackObject = new System.DateTime(2003, 1, 18); 

// allocated on the managed heap
System.IO.MemoryStream heapObject = new System.IO.MemoryStream(); 

As you can see, nothing about the way you declare the object gives any hint at whether the object is on the stack or the heap. That choice is left completely up to the type's author and the runtime.

Managed Extensions for C++, referred to as Managed C++ for short, introduced the ability to mix managed code with native C++ code. Following the rules in the C++ standard, extensions were added to C++ to support a whole range of CLR constructs. Unfortunately, there were just too many extensions, and writing a lot of managed code in C++ became a real pain.

// allocated on the stack
DateTime stackObject(2003, 1, 18);

// allocated on the managed heap
IO::MemoryStream __gc* heapObject = __gc new IO::MemoryStream;

Allocating a value type on the stack looks quite normal to a C++ programmer. The managed heap example, however, looks a bit strange. __gc is one of the keywords in the Managed C++ extensions. Arguably, Managed C++ can infer what you mean in some cases, so this example could be rewritten without the __gc keywords. This is known as the defaulting rules.

// allocated on the managed heap
IO::MemoryStream* heapObject = new IO::MemoryStream;

This looks a lot more like native C++. The problem is that heapObject is not a real C++ pointer. C++ programmers have come to rely on a pointer having a stable value, but the garbage collector can move objects around in memory at any time. The other drawback is that there is no way to tell just by looking at the code whether the object will be allocated on the native or managed heap. You would need to know how the type was defined by its author. There are a lot of other convincing reasons why overloading the meaning of the C++ pointer is a bad idea.

C++/CLI introduces the concept of a handle to distinguish a CLR object reference from a C++ pointer. By not overloading the C++ pointer, a lot of ambiguity is removed from the language. In addition, much more natural support for the CLR can be provided through handles. For example, you can make use of operator overloads on reference types directly in C++, because operator overloading is supported on handles. This could not have been possible with "managed" pointers, since C++ forbids operators to be overloaded on pointers.

// allocated on the stack
DateTime stackObject(2003, 1, 18);

// allocated on the managed heap
IO::MemoryStream^ heapObject = gcnew IO::MemoryStream;

Again, there is nothing surprising about the declaration of the value type. The reference type, however, is distinctive. The ^ operator declares the variable as a handle to a CLR reference type. Handles track, meaning the handle's value is automatically updated by the garbage collector as the object to which it refers, is moved in memory. In addition, they are rebindable, which allows them to point to a different object, just like a C++ pointer. The other thing you should notice is the gcnew operator that is used in place of the new operator. This clearly indicates that the object is being allocated on the managed heap. The new operator is no longer overloaded (no pun intended) for managed types, and will only allocate objects on the CRT heap, unless of course you provide your own operator new. Don't you just love C++!

That is object construction in a nutshell: Native C++ pointers are clearly distinguished from CLR object references.

Memory Management vs. Resource Management

It is useful to distinguish memory management from resource management when you are dealing with an environment that includes a garbage collector. Typically, the garbage collector is interested in allocating and freeing the memory that will contain your objects. It does not care about other resources that your objects may own, such as database connections or handles to kernel objects. In the two sections that follow. I will be talking about memory management and resource management individually, as they are each vitally important topics to understand well.

Memory Management

Native C++ provides the programmer direct control over memory management. Allocating an object on the stack means that the memory for the object will be allocated when the particular function call is entered, and the memory will be released when the function returns and the stack unwinds. Dynamically allocating an object is accomplished using the new operator. This memory is allocated from the CRT heap and it is freed explicitly when the programmer uses the delete operator on the pointer to the object. This precise control over memory is one of the reasons that C++ can be used to write extremely efficient code, but is also the cause of memory leaks when the programmer is not careful. Arguably, you do not have to resort to a garbage collector to avoid memory leaks, but that is the approach taken by the CLR and is a very effective approach, indeed. Of course there are other benefits to a garbage collected heap, such as improved allocation performance and advantages related to locality of reference. All of these things could be achieved in C++ through library support, but the thing that stands out about the CLR is that it provides a single memory management programming model that is common to all programming languages. Think of all the work needed to interoperate and marshal data types from COM automation object models from C++, and you should be able to realize the significance of this. Having a common garbage collector that spans programming languages is huge.

The CLR retains the concept of a stack as a place where value types can be allocated for obvious reasons of efficiency. The CLR, however, provides a newobj intermediate language (IL) instruction to allocate an object on the managed heap. This instruction is provided for you when you use the new operator in C# on a reference type. There is no equivalent function to the C++ delete operator for the CLR. The memory that was previously allocated will eventually get reclaimed, when the application no longer refers to it and the garbage collector decides to do a collection.

Managed C++ also generated the newobj instruction when the new operator was applied to reference types. It is, however, illegal to use the delete operator with such a managed, or garbage collected, pointer. This is certainly a nasty inconsistency, and yet another reason why representing reference types with the C++ pointer concept is a bad idea.

C++/CLI does not provide anything else new in the area of memory management, other than what we have already covered in the section about object construction. Resource management, however, is where C++/CLI really excels.

Resource Management

As far as resource management goes, nothing beats native C++. Bjarne Stroustrup's "resource acquisition is initialization" technique basically defines that each resource type should be modeled as a class with constructors and a destructor to release the resource. These types are then used as local objects on the stack, or as members of more complex types. Their destructors then take care of automatically releasing the resources being held. As Stroustrup puts it, "C++ is the best language for garbage collection principally because it creates less garbage."

Perhaps somewhat surprisingly, the CLR does not provide any explicit runtime support for resource management. The CLR does not support the C++ concept of a destructor. Rather, the .NET Framework has promoted a pattern for resource management centered on a core interface type called IDisposable. The idea is that types that encapsulate resources should implement this interface's single Dispose method, and then callers should call the Dispose method when they no longer need the resource. Needless to say, C++ programmers tend to think of this as a step backwards, because they are used to writing code whose cleanup is correct by default.

The trouble with having to call a method to free any resources is that it makes it harder to write exception safe code. You cannot simply place a call to the object's Dispose method at the end of a block of code, as an exception may be thrown at any time and then you would have leaked the resources owned by the object. C# gets around this by providing try-finally blocks and the using statement, to provide a reliable way of calling the Dispose method in the face of exceptions. These constructs do tend to get in the way at times, however; worse, you have to remember to write them, and if you forget then your code still compiles but has a silent error by default. The need for try-finally blocks and the using statement is arguably an unfortunate necessity in a language that lacks a real destructor.

using (SqlConnection connection = new SqlConnection("Database=master; Integrated Security=sspi"))
{
    SqlCommand command = connection.CreateCommand();
    command.CommandText = "sp_databases";
    command.CommandType = CommandType.StoredProcedure;

    connection.Open();

    using (SqlDataReader reader = command.ExecuteReader())
    {
        while (reader.Read())
        {
            Console.WriteLine(reader.GetString(0));
        }
    }
}

The story is much the same for Managed C++. You need to use a try-finally statement, which is a Microsoft extension to C++. Managed C++ did not introduce an equivalent of the C# using statement, though it is easy enough to write a simple Using template class that wraps a GCHandle and calls the managed object's Dispose method in the template class's destructor.

Using<SqlConnection> connection(new SqlConnection
    (S"Database=master; Integrated Security=sspi"));

SqlCommand* command = connection->CreateCommand();
command->set_CommandText(S"sp_databases");
command->set_CommandType(CommandType::StoredProcedure);

connection->Open();

Using<SqlDataReader> reader(command->ExecuteReader());

while (reader->Read())
{
    Console::WriteLine(reader->GetString(0));
}

Considering C++'s traditionally strong support for resource management, it is fitting that the C++/CLI language design has gone to great lengths to make resource management a breeze in C++. First, let us look at authoring a class that manages a resource. One of the burdens shared by most, if not all, languages targeting the CLR is implementing the Dispose pattern correctly. It is just not as easy as implementing a classic destructor in native C++. When writing your Dispose method, you need to make sure you call the base class Dispose method, if any. In addition, if you choose to implement the class's Finalize method by calling the Dispose method, you need to worry about concurrent access, since the Finalize method is called on a separate thread. Additionally, you need to be careful about freeing managed resources if the Dispose method is actually being called from the Finalize method, as apposed to normal application code.

C++/CLI does not make all of these issues go away, but it does provide a good deal of help. Before we look at what it offers, let us quickly review what the approach for C# and Managed C++ is today. This example assumes that Base derives from IDisposable. If it did not, the Derived class would need to.

class Derived : Base
{
    public override void Dispose()
    {
        try
        {
            // free managed and unmanaged resources
        }
        finally
        {
            base.Dispose();
        }
    }

    ~Derived() // implements/overrides the Object.Finalize method
    {
        // free unmanaged resources only
    }
}

Managed C++ is much the same. What looks like a destructor is actually a Finalize method. The compiler takes care to effectively insert a try-finally block and call the base class's Finalize method, so C# and Managed C++ make it relatively easy to write a Finalize method but do not provide any help in writing a Dispose method, which is arguably much more important. Programmers often use the Dispose method as a pseudo-destructor just to be able to have some code execute at the end of some scope, not necessarily to free any resource.

C++/CLI acknowledges the importance of the Dispose method by making it the logical "destructor" for a reference type.

ref class Derived : Base
{
    ~Derived() // implements/overrides the IDisposable::Dispose method
    {
        // free managed and unmanaged resources
    }

    !Derived() // implements/overrides the Object::Finalize method
    {
        // free unmanaged resources only
    }
};

This feels a lot more natural to a C++ programmer. I can free my resources in my destructor like I've always done. The compiler will emit the necessary IL to implement the IDisposable::Dispose method correctly, including suppressing the garbage collector's call to any Finalize method on the object. In fact, it is not legal in C++/CLI to explicitly implement the Dispose method. Inheriting from IDisposable will result in a compiler error. Of course, once the type is compiled, all CLI languages that consume it will see the Dispose pattern implemented in whatever way is most natural for each language. In C# you can call the Dispose method directly, or use a using statement just as if the type were defined in C#; but what about C++? How do you normally call the destructor of a heap based object? By using the delete operator, of course! Applying the delete operator to a handle will call the object's Dispose method. Recall that the object's memory is managed by the garbage collector. We are not concerned about freeing that memory, but only about freeing the resources the object contains.

Derived^ d = gcnew Derived();

d->SomeMethod()

delete d;

So if the expression passed to the delete operator is a handle, the object's Dispose method is called. If there are no more roots connected to the reference type, the garbage collector is free to collect the object's memory at some point. If the expression is a native C++ object, the object's destructor is called before the memory is returned to the heap.

Certainly we're getting closer to the natural C++ syntax for object lifetime management, but it is still error-prone to have to remember to call the delete operator. C++/CLI allows you to employ stack semantics with reference types. What this means is that you can introduce a reference type using the syntax reserved for allocating objects on the stack. The compiler will take care of providing you the semantics that you would expect from C++, and under the covers meet the requirements of the CLR by actually allocating the object on the managed heap.

Derived d;

d.SomeMethod();

When d goes out of scope, its Dispose method will be called to allow its resources to be released. Again, since the object is actually allocated from the managed heap, the garbage collector will take care of freeing it in its own time. Going back to our ADO.NET example, it would be written like this in C++/CLI.

SqlConnection connection("Database=master; Integrated Security=sspi");

SqlCommand^ command = connection.CreateCommand();
command->CommandText = "sp_databases";
command->CommandType = CommandType::StoredProcedure;

connection.Open();

SqlDataReader reader(command->ExecuteReader());

while (reader.Read())
{
    Console::WriteLine(reader.GetString(0));
}

Types Revisited

Before we talk about boxing, it is useful to refresh our memory on why there is a distinction between value types and reference types.

You can think of instances of value types as simple values, and instances of reference types as objects. Besides the memory required to store the fields of an object, every object has an object header that allows it to provide the basic services of object-oriented programming, such as class hierarchies with virtual methods, as well as metadata that can be tapped into for all kinds of uses. The memory overhead of the object header, however, combined with the indirection of virtual methods and interfaces, is often too costly when all you want is a simple value with a static type and some compiler-enforced operations on that value. Arguably, compilers can optimize away this object overhead in some cases, but not all. Clearly there is benefit in having values and value types if you are at all concerned about performance in managed code. There just isn't such a big split in the type system of native C++. Of course, C++ does not impose any programming paradigm, so it is possible to build such distinct type systems through the creation of libraries on top of C++.

Boxing

So what is boxing? Boxing is a mechanism to bridge the gap between values and objects. Although the CLR requires every type to derive, directly or indirectly, from Object, values really do not. A simple value like an integer on the stack is just a block of memory that the compiler allows certain operations on. If you really want to treat a value as an object, it must really be an object. You should be able to call methods inherited from Object on your value. To make this possible, the CLR provides the notion of boxing. It is useful to know a little bit about how boxing actually works. First, a value is pushed onto the stack using the ldloc IL instruction. Next the box IL instruction is used, which does the heavy lifting. The compiler provides the static type of the value, for example Int32, and the CLR goes ahead and pops the value off of the stack and allocates enough memory to store the value and the object header. A reference to the newly created object is pushed onto the stack. All this was done as a result of the box instruction. Finally, to get the object reference, the stloc IL instruction is used to pop the reference off the stack and store it in some local variable.

Now, the question is whether boxing a value should be represented as an implicit or explicit operation by a programming language. In other words, should an explicit cast, or some other construct, be used? The C# language designers decided to make it an implicit conversion. After all, an integer is an Int32 type which derives indirectly from Object.

int i = 123;
object o = i;

The problem, as we've learnt, is that boxing is not a simple upcast. Rather, it is a conversion from a value to an object, a potentially expensive operation. For this reason, Managed C++ makes boxing explicit with the use of the __box keyword.

int i = 123;
Object* o = __box(i);

Of course, in Managed C++, you do not have to lose static type information when you box a value. This is something that C# does not provide.

int i = 123;
int __gc* o = __box(i);

A strongly-typed boxed value has the advantage of allowing conversion back to a value type, otherwise known as unboxing, without using a dynamic_cast, simply by dereferencing the object.

int c = *o;

Of course the syntactic overhead of the explicit boxing in Managed C++ proved to be too much in most cases. For this reason, the course of the C++/CLI language design changes, and comes in line with C# by making boxing implicit. At the same time, it retains the type-safety of being able to directly express strongly typed boxed values that other .NET languages cannot express.

int i = 123;
int^ hi = i;
int c = *hi;
hi = nullptr;

Of course, this implies that a handle that is not pointing to an object cannot be initialized with zero, as pointers are, since this would simply box the value zero. This is the purpose of the nullptr constant. It can be assigned to any handle. This is equivalent to the C# null keyword. Although nullptr is a new reserved word in the C++/CLI language design, it is proposed as an addition to Standard C++ for use with pointers by Herb Sutter and Bjarne Stroustrup.

Authoring Reference and Value Types

In the next few sections we are going to cover some details of authoring CLR types.

C# uses the class keyword to declare reference types and the struct keyword to declare value types.

class ReferenceType {}
struct ValueType {}

C++ already has well-defined meanings for both class and struct, so this wouldn't work for C++. In the original language design, the __gc keyword was placed before the class to indicate a reference type and the __value keyword was used for value types.

__gc class ReferenceType {};
__value class ValueType {};

C++/CLI introduces spaced keywords in places where this will not conflict with user identifiers. For declaring reference types, you add ref before a class or struct. Similarly, you use value for declaring value types.

ref class ReferenceType {};
ref struct ReferenceType {};

value class ValueType {};
value struct ValueType {};

The choice of using class versus struct is the same as it has always been in terms of the default visibility of members. The main difference is that CLR types only support public inheritance. Using private or protected inheritance will result in a compiler error, so explicitly declaring public inheritance is legal but redundant.

Accessibility

The CLR defines a number of accessibility modifiers that go beyond that which native C++ provides for class member functions and variables. Not only that, you can define the accessibility of namespace types, not just nested types. In keeping with the C++/CLI goal of being the lowest level language, it provides more control over accessibility than any other high-level language targeting the CLR.

The main difference between native C++ accessibility and accessibility as defined by the CLR is that, whereas native C++ access specifiers are used to restrict access to members from other code in the same program, the CLR needs to define the accessibility of types and their members not only to other code in the same assembly, but also to other assemblies that may reference it.

A namespace, or non-nested, type, such as a class or delegate type, can specify its visibility outside the assembly by adding public or private before the type definition.

public ref class ReferenceType {};

If you do not specify the visibility explicitly, the type is assumed to be private to the assembly.

Access specifiers for members have been extended to allow you to use two keywords together to specify the internal and external access to the names that follow it. The more restrictive of the two defines the access outside the assembly, whereas the other defines the access inside the assembly. If only a single keyword is used, it applies to both internal and external access. This design provides a great deal of flexibility for defining the accessibility of types and members that you define. Here is an example.

public ref class ReferenceType
{
public:
    // visible inside and outside assembly
private public:
    // visible inside assembly
protected public:
    // visible to derived types outside and all code inside assembly
};

Properties

Apart from nested types, CLR types can only contain methods and fields. To allow the programmer to convey intensions more clearly, metadata can be used to indicate that certain methods should be treated as properties by programming languages. Strictly speaking, a CLR property is a member of its containing type; however, the property has no allocated storage, and is simply a named reference to the respective methods implementing the property. Different compilers need to generate the required metadata when it encounters the appropriate syntax for properties in the source code. That way, consumers of the type can use the property syntax of their language to access the get and set methods that implement the property. Unlike native C++, C# has first-class support for properties.

public string Name
{
    get
    {
        return m_name;
    }
    set
    {
        m_name = value;
    }
}

The C# compiler will generate the corresponding get_Name and set_Name methods, as well as include the necessary metadata to indicate the association. Managed C++ introduced the __property keyword to indicate that a method plays a part in implementing property semantics.

__property String* get_Name()
{
    return m_value;
}
__property String* set_Name(String* value)
{
    m_value = value;
}

Clearly this is not ideal. Not only do we need to use the ugly __property keyword, but there is nothing that clearly indicates that the two member functions actually belong together. This can lead to subtle bugs during maintenance. The C++/CLI design for properties is much more concise, and resembles the C# design much more closely. As you will see, however, it is even more powerful.

property String^ Name
{
    String^ get()
    {
        return m_value;
    }
    void set(String^ value)
    {
        m_value = value;
    }
}

That's a great improvement. The compiler will take care of generating the get_Name and set_Name methods, as well as the necessary metadata that declares this a property. What would also be nice is if you could allow code outside of your assembly to read the property value, but only allow code inside of your assembly to write it. You can use an access specifier inside the curly braces following the property name to achieve this.

property String^ Name
{
public:
    String^ get();
private public:
    void set(String^);
}

One last thing worth noting about the property support is that it supports a shorthand syntax for those cases where you do not need any special processing for getting and setting a property.

property String^ Name;

Again, the compiler will generate the get_Name and set_Name methods, but this time will also provide a default implementation backed by a private String^ member variable. The advantage of this, of course, is that in the future you can replace the simple property with a more interesting property implementation and it will not break the interface contract of the class. You get the simplicity of a field with the flexibility of a property.

Delegates

Function pointers in native C++ provide a mechanism to execute code asynchronously. You can store a pointer to a function, or more generally a functor, and invoke it at some later point in time. This may be used simply as a way to decouple an algorithm from some part of its implementation, such as comparing objects in a search. Alternatively, it could be used for truly asynchronous programming by invoking the functor on a different thread. Here is a simple example of a ThreadPool class that lets you queue a pointer to a function for execution on a worker thread.

class ThreadPool
{
public:

    template <typename T>
    static void QueueUserWorkItem(void (T::*function)(),
                                  T* object)
    {
        typedef std::pair<void (T::*)(), T*> CallbackType;
        std::auto_ptr<CallbackType> p(new CallbackType(function, object));

        if (::QueueUserWorkItem(ThreadProc<T>,
                                p.get(),
                                WT_EXECUTEDEFAULT))
        {
            // The ThreadProc now has the responsibility of deleting the pair.
            p.release();
        }
        else
        {
            AtlThrowLastWin32();
        }                
    }

private:

    template <typename T>
    static DWORD WINAPI ThreadProc(PVOID context)
    {
        typedef std::pair<void (T::*)(), T*> CallbackType;

        std::auto_ptr<CallbackType> p(static_cast<CallbackType*>(context));

        (p->second->*p->first)();
        return 0;
    }

    ThreadPool();
};

Using the thread pool is simple and feels natural in C++.

class Service
{
public:

    void AsyncRun()
    {
        ThreadPool::QueueUserWorkItem(Run, this);
    }

    void Run()
    {
        // some lengthy operation
    }
}

Obviously, my ThreadPool class is very limited in that it only works with function pointers with a specific signature. This is only a limitation of this example, and not of C++ itself. For a great discussion of generalized functors, pick up a copy of Andrei Alexandrescu's excellent book Modern C++ Design.

Where C++ programmers need to implement or obtain rich libraries for asynchronous programming, the CLR comes with built-in support. Delegates are similar to function pointers, except that the target object, or the type to which the method belongs, does not play a role in determining whether a delegate can be bound to a given method. As long as the signature matches, the method can be added to the delegate for later invocation. This is similar, at least in spirit, to my example above where I used C++ templates to allow any class's member function to be used. Of course, delegates provide far more than that and are an extremely useful mechanism for indirect method invocation. The following is an example of defining a delegate type using C++/CLI.

delegate void Function();

Using a delegate is straightforward.

ref struct ReferenceType
{
    void InstanceMethod() {}
    static void StaticMethod() {}
};

// create delegate and bind to instance member function
Function^ f = gcnew Function(gcnew ReferenceType,
                             ReferenceType::InstanceMethod);

// also bind to static member function by combining delegates
// to form delegate chain
f += gcnew Function(ReferenceType::StaticMethod);

// invoke both functions
f();

Conclusion

There is certainly a lot more to be said about C++/CLI, never mind the Visual C++ 2005 compiler, but I hope this article has provided you with a good introduction to what it has to offer for programmers targeting the CLR. The new language design provides unprecedented power and elegance to write rich .NET applications completely in C++ without sacrificing productivity, conciseness, or performance.

The following table provides a summary of the most common constructs for quick reference.

Description C++/CLI C#
Allocate reference type ReferenceType^ h = gcnew ReferenceType; ReferenceType h = new ReferenceType();
Allocate value type ValueType v(3, 4); ValueType v = new ValueType(3, 4);
Reference type, stack semantics ReferenceType h; N/A
Calling Dispose method ReferenceType^ h = gcnew ReferenceType;

delete h;

ReferenceType h = new ReferenceType();

((IDisposable)h).Dispose();

Implementing Dispose method ~TypeName() {} void IDisposable.Dispose() {}
Implementing Finalize method !TypeName() {} ~TypeName() {}
Boxing int^ h = 123; object h = 123;
Unboxing int^ hi = 123;

int c = *hi;

object h = 123;

int i = (int) h;

Reference type definition ref class ReferenceType {};

ref struct ReferenceType {};

class ReferenceType {}
Value type definition value class ValueType {};

value struct ValueType {};

struct ValueType {}
Using properties h.Prop = 123;

int v = h.Prop;

h.Prop = 123;

int v = h.Prop;

Property definition property String^ Name
{
    String^ get()
    {
        return m_value;
    }
    void set(String^ value)
    {
        m_value = value;
    }
}
string Name
{
    get
    {
        return m_name;
    }
    set
    {
        m_name = value;
    }
}

 

About the author

Kenny Kerr spends most of his time designing and building distributed applications for the Microsoft Windows platform. He also has a particular passion for C++ and security programming. Reach Kenny at https://weblogs.asp.net/kennykerr/ or visit his Web site: https://www.kennyandkarin.com/Kenny/.

© Microsoft Corporation. All rights reserved.