C++/CLI
C++/CLI (Common Language Infrastructure) is Microsoft's language specification intended to supersede Managed Extensions for C++. Completely revised to simplify the older Managed C++ syntax (which is now deprecated), it provides much more clarity and code readability than Managed C++. C++/CLI is standardized by Ecma as ECMA-372. It is currently available only in Visual Studio 2005 and 2008 (also included in the Express Editions).
Syntax changes
C++/CLI should be thought of as a language of its own (with a new set of keywords, for example), instead of the C++ superset-oriented Managed C++ (MC++) (whose non-standard keywords were styled like __gc
or __value
). Because of this, there are some major syntactic changes, especially related to the elimination of ambiguous identifiers and the addition of .NET-specific features.
Many conflicting syntaxes, such as the multiple versions of operator new()
in MC++ have been split: in C++/CLI, .NET reference types are created with the new keyword gcnew
. Also, C++/CLI has introduced the concept of generics (conceptually similar to standard C++ templates, but quite different in their implementation).
Handles
Back in MC++, there were two different types of pointers: __nogc
pointers were normal C++ pointers, while __gc
pointers worked on .NET reference types. In C++/CLI the only type of pointer is the normal C++ pointer, and the .NET reference types are accessed through a "handle", with the new syntax ClassName^
instead of ClassName*
. This new construct is especially helpful when managed and standard C++ code is mixed; it clarifies which objects are under .NET automatic garbage collection and which objects the programmer must remember to explicitly destroy.
// Managed extensions for C++
#using <mscorlib.dll>
using namespace System::Collections;
__gc class referencetype
{
protected:
String* stringVar;
int intArr __gc[];
ArrayList* doubleList;
public:
referencetype(String* str, int* pointer, int number) // Which one is managed?
{
doubleList = new ArrayList();
System::Console::WriteLine(str->Trim() + number.ToString());
}
};
// C++/CLI
#using <mscorlib.dll>
using namespace System::Collections::Generic;
ref class referencetype
{
protected:
String^ stringVar;
array<int>^ intArr;
List<double>^ doubleList;
public:
referencetype(String^ str, int* pointer, int number) // Ambiguous no more
{
doubleList = gcnew List<double>();
System::Console::WriteLine(str->Trim() + number);
}
};
Tracking references
A tracking reference in C++/CLI is a handle of a passed by reference variable. It corresponds to the "ref
" keyword applied to types in C#, or "ByRef
" in Visual Basic .NET. C++/CLI uses a "^%
" syntax to indicate a tracking reference to a handle. It is similar in concept to using "*&
" (reference to a pointer) in Standard C++.
The following code shows an example use for tracking references. Replacing the tracking reference with a regular handle variable would leave the resulting string array with 10 uninitialized string handles, as only copies of the string handles in the array would be set, due to them being passed by value rather than by reference.
int main()
{
array<String^>^ arr = gcnew array<String^>(10);
int i = 0;
for each(String^% s in arr)
s = i++.ToString();
return 0;
}
The code above additionally serves as an example in how .NET languages allow users to do slightly different things, as e.g. C# does not allow the user to use foreach
loops in the directly corresponding way. That is; foreach(ref string s in arr)
is illegal in C# as it does not allow foreach
loops to pass values by reference, and other workarounds need to be used in this case.
Finalizers and automatic variables
Another change in C++/CLI is the introduction of the finalizer syntax !ClassName()
, a special type of nondeterministic destructor that is run as a part of the garbage collection routine, so now the destructor syntax ~ClassName()
better reflects the "traditional" C++ semantics of deterministic destruction (that is, destructors that can be called by user code). Moreover, the destruction of all managed objects with a defined destructor in a method can be made automatic with the new syntax shown in the example.
In the raw .NET paradigm (for example, direct programming in CIL), the deterministic destruction model is implemented through the IDisposable
interface method Dispose
(which is just what the C++/CLI compiler turns the destructor into), while the nondeterministic one overrides the protected Finalize
method of the root Object
class.
Usually, deterministic destruction is recommended when non-managed resources are involved or when system-wide limited resources (network/database connections, file streams, etc.) are used. In all other cases, the non-deterministic approach should suffice. Whenever a deterministic destructor is used, the programmer can still benefit from the Garbage Collector, to avoid possible standard C++ memory/resource leaks, by also creating a finalizer (GC-destructor) that just checks whether the destructor has been invoked and calls it if it hasn't (thus adding little performance penalty).
// C++/CLI
ref class MyClass // : IDisposable (this is added by the compiler)
{
public:
MyClass(); // constructor
~MyClass(); // (deterministic) destructor (turned into IDisposable.Dispose() by the compiler)
protected:
!MyClass(); // finalizer (non-deterministic destructor) (former destructor syntax => virtual void Finalize())
public:
static void Test()
{
MyClass automatic; // Not a handle, no initialization: compiler calls constructor here
// Use 'automatic' anywhere in the method
// Equivalent user code:
MyClass^ user = gcnew MyClass();
try { /* Use user here */ } finally { delete user; }
// Compiler calls automatic's destructor in the finally of a try containing the whole method
}
};
// C#
class MyClass : IDisposable
{
public MyClass() {} // constructor
public void Dispose() {} // Dispose method (C++/CLI deterministic destructor)
~MyClass() {} // destructor (non-deterministic) (C++/CLI finalizer => protected override void Finalize())
public static void Test()
{
using(MyClass automatic = new MyClass()) { /* Use automatic here */ }
// Compiler calls automatic.Dispose(), in the finally of a try containing the using block
// Equivalent user code:
MyClass user = new MyClass();
try { /* Use user here */ } finally { user.Dispose(); }
}
}
Operator overloading
Operator overloading works analogously to unmanaged C++. Every * becomes a ^, every & becomes an %, but the rest of the syntax is unchanged. Except for an important addition: Operator overloading is possible not only for classes themselves, but also for references to those classes. This feature is necessary to give a ref class the semantics for operator overloading expected from .Net ref classes. In effect, this also means that for .Net framework ref classes, reference operator overloading is often implemented.
This means that comparing two distinct String references via the operator == will give true whenever the two strings are equal. Of course, operator overloading is static, as is and always should be, not only when writing managed code[neutrality is disputed]. Thus, casting to Object ^ will remove the overloading semantics.
The standard semantics would be to only overload by object (the classic C++ way) for native and value types, and to overload only by reference (^) for ref classes. Of course, in C++-only projects, it may be reasonable to decide against overloading by reference, and stick to classic C++ operator overloading semantics also for ref classes, which, as experience tells[citation needed], will often enough be used with on-the-stack semantics and implemented with a copy constructor and assignment operator.
//effects of reference operator overloading
String ^s1 = "abc";
String ^s2 = "ab" + "c";
Object ^o1 = s1;
Object ^o2 = s2;
s1 == s2; // true
o1 == o2; // false
Template syntax
// a template ref class with operator overloading, copy constructor and assignment operator
template <typename T>
public ref class ptr_wrapper sealed
{
private:
T *m_ptr;
ptr_wrapper(T *i_ptr)
:m_ptr(i_ptr)
{
if (i_ptr == 0)
{
throw gcnew System::Exception("Trying to initialize ptr_wrapper with null pointer");
}
}
public:
ptr_wrapper(const T &i_ref)
:m_ptr(new T(i_ref))
{
}
ptr_wrapper(const ptr_wrapper %i_other)
:m_ptr(new T(const_cast<const T&>(*i_other)))
{
}
static ptr_wrapper take(T *i_ptr)
{
return ptr_wrapper(i_ptr);
}
~ptr_wrapper()
{
delete m_ptr;
}
ptr_wrapper % operator = (const ptr_wrapper %other)
{
if (other.m_ptr != m_ptr)
{
T* new_ptr = new T(*other);
delete m_ptr;
m_ptr = new_ptr;
}
}
static T& operator * (ptr_wrapper<T> %inst)
{
return *(inst.m_ptr);
}
static const T& operator * (const ptr_wrapper %inst)
{
return *(inst.m_ptr);
}
static T* operator -> (ptr_wrapper %inst)
{
return inst.m_ptr;
}
static const T* operator -> (const ptr_wrapper<T> %inst)
{
return inst.m_ptr;
}
};
External links
- ECMA 372: C++/CLI Language Specification
- MSDN documentation for C++/CLI
- Patent application regarding whitespace in keywords
- Bjarne Stroustrup's (developer of C++) views on C++/CLI
- Stanley B. Lippman. Hello, C++/CLI
- Stanley B. Lippman. Why C++/CLI Supports both Templates for CLI Types and the CLI Generic Mechanism
- Herb Sutter. C++/CLI keywords: Under the hood
- Herb Sutter. C++/CLI Rationale
- A first look at C++/CLI. By Nishant Sivakumar