C++/CLI
From Wikipedia, the free encyclopedia
C++/CLI (Common Language Infrastructure) is the newer language specification due to supersede Managed Extensions for C++. Completely revised to simplify the older Managed C++ syntax, it provides much more clarity over code readability than Managed C++. C++/CLI is standardized by ECMA. It is currently only available on Visual C++ 2005. Some aspects of the language are protected by patents or patent applications.
Contents |
[edit] 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 of 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 (similar to unmanaged C++ templates, but quite different).
[edit] 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 unmanaged code is mixed; it clarifies which objects are under .NET automatic garbage collection and which must be destroyed.
// 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); } };
[edit] Tracking references
A tracking reference in C++/CLI is a handle that is passed by reference rather than by value. They correspond 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 = gcnew String(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.
[edit] 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 unmanaged 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 auto; // Not a handle, no initialization: compiler calls constructor here // Use auto anywhere in the method // Equivalent user code: MyClass^ user = gcnew MyClass(); try { /* Use user here */ } finally { delete user; } // Compiler calls auto's destructor in the finally of a try containing the whole method } };
// C# class MyClass : IDisposable { public MyClass() {} // constructor ~MyClass() {} // destructor (non-deterministic) (C++/CLI finalizer => protected override void Finalize()) public void Dispose() {} // Dispose method (C++/CLI deterministic destructor) public static void Test() { using(MyClass auto = new MyClass()) { /* Use auto here */ } // Compiler calls auto.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(); } } }
[edit] 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. 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, will often enough be used with on-the-stack semantics and implemented with a copy contructor 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
[edit] Advanced 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; } };