LearningC++CLI
C++/CLI is a programming language created by Microsoft that allows to use the .NET API from C++. This page documents my learning curve when I first encountered C++/CLI.
Managed C++
C++/CLI is often called "Managed C++", at least by the people in my work environment. After a bit of research I learned that this is not entirely correct:
- First of all, the official term is not "Managed C++" but "Managed Extensions for C++" (Wikipedia article).
- Second, in 2004 Managed C++ was revised and re-published under the name C++/CLI (Wikipedia article). Since that time, Managed C++ is deprecated.
When it was published, C++/CLI became part of the Visual Studio 2005 IDE. If you have a problem and are looking for a solution, you should use the term "C++/CLI" for your research (especially on Stack Overflow).
References
- Quick C++/CLI - Learn C++/CLI in less than 10 minutes: Tutorial article on codeproject.com
- C++/CLI Primer - Enter the World of .NET Power Programming: Another primer, but with more in-depth coverage of how to destroy objects
- MSDN article about gcroot
- MSDN article about Tracking References
- Overview of Marshaling in C++: MSDN article on how to convert from native C++ to .NET data types, and vice versa
Glossary
- CLI
- Common Language Infrastructure
- CLR
- Common Language Runtime
- Handle
- A reference to an object that was allocated on the CLI heap. You recognize that something is a handle when it is declared using the character "^". Example:
System::Object^ anObject = ...
. Objects that are referenced by handles are managed by the Garbage Collector. - Reference Type
- A managed class that is declared using the keyword
ref
. - Tracking Reference
- Reference to a Handle. You recognize that something is a tracking reference when it is declared using the character "%". Example:
System::Object% anObject = ...
. A tracking reference is to a handle what a traditional C++ reference ("&") is to a traditional C++ pointer ("*").
New project
You need a Visual C++ project if you want to write C++/CLI code in Visual Studio. The distinguishing property that you need to set on the project is this (Visual Studio 2010): Project Settings > Configuration Properties > General > Common Language Runtime Support = Common Language Runtime Support.
A new, clean C++/CLI project without any old cruft is created like this (in Visual Studio 2010):
- File > New Project
- Open the "Visual C++" tree and select the entry "CLR"
- Select one of the templates, e.g. "Class Library"
- Choose a project name, select a folder where to store the project, then click "OK"
After creating the project, I usually "groom" the project a bit until it looks after my taste:
- If necessary, add property sheets and remove project-specific settings that are now defined by the property sheet(s) ("inherit from parent or project defaults")
- Delete the useless file
ReadMe.txt
- Delete
app.ico
(the file as well as the reference in the resources) - Remove all unnecessary comments
Managed class declaration
Example:
namespace foo { public ref class Bar { public: void doIt(); }; }
Notes:
- The keyword
ref
is required to mark the class as "managed" (native C++ classes are declared as usual, i.e. without "ref"). The class is called a "reference type". - A visibility modifier ("public" in the example) can be added to the class
- Unfortunately, the semicolon (";") at the end of the class delaration that is so typical for C++ is still required. Weird compiler errors occur if the character is forgotten (weirder still than the errors that pop up when the semicolon is missing from a native C++ class)
Creating objects
On the heap
Examples how to create managed objects:
void doIt() { gcnew foo::Bar(); foo::Bar^ anObject1 = gcnew foo::Bar(); System::Object^ anObject2 = gcnew foo::Bar(); }
Notes:
- To create a managed object on the heap, you use
gcnew
(instead ofnew
to create a native C++ object) - The object is created on the managed heap (which is a different heap than the native C++ heap)
- Managed objects are referenced with a so-called "handle". The handle is declared using the "^" character (instead of the "*" character in native C++, i.e. a "pointer")
- Namespaces are separated with "::" just as in native C++ (instead of "." in C#)
On the stack? Impossible!
Instances of reference types are always created on the managed heap, even if the syntax suggests that the instance is created on the stack:
foo::Bar anObject;
In this example, the object is created on the heap behind the scenes. When the variable goes out-of-scope, the object's destructor (or rather its Dispose() method) is invoked - see the section titled "Destructor" for details. Finally, the object is marked as available for garbage collection.
Destructor
- Ideally, a "ref" class does not need a destructor because it only has references to other managed objects
- If a destructor is needed it can be declared/implemented using the usual C++ idiom (with the "~" character)
- When the compiler finds a destructor, it adds the following to the class:
- The class inherits from
IDisposable
- A default implementation of
Dispose()
is added to the class. The default implementation follows a defined design pattern, the details of which are not explained here. Read one of the articles in the References section above. - The generated
Dispose()
method executes the destructor code as part of its implementation
- The class inherits from
- In order for the destructor to be actually executed:
- In C++/CLI the object must be explicitly destroyed using
delete
(exactly the same as in native C++). The only exception: If the managed object was declared using the "create-on-the-stack" syntax (see further up), the compiler automatically inserts a call toDispose()
at the time when the variable goes out-of-scope. - In C# the
Dispose()
method must be explicitly invoked
- In C++/CLI the object must be explicitly destroyed using
Example:
public ref class Bar { public: Bar(); ~Bar(); // Compiler derives from IDisposable and generates implementation of Dispose() }; void doThis() { Bar^ anObject = gcnew Bar(); delete anObject; // Because the object was created with gcnew, we need to explicitly call delete } void doThat() { Bar anObject; // Because anObject was declared using the "create-on-the-stack" syntax, // the compiler automatically inserts a call to Dispose() when anObject // goes out-of-scope at the end of the method. }
nullptr
A reference to "nothing" is achieved using the keyword nullptr
:
System::Object^ anObject = nullptr;
Checking for nullptr
is simple:
if (nullptr == anObject) ...
Enumerations
public enum class SomeColors { Red, Yellow, Blue}; public enum class SomeColors: char { Red, Yellow, Blue};
Notes:
- In the second example, the type of the individual enumeration members is explicitly declared
Arrays
// Create and initialize array with 3 elements cli::array<int>^ array1 = gcnew cli::array<int> {1, 2, 3}; // Iterate over all elements of an array for each (int v in array1) { Console::WriteLine("value = {0}", v); } // Create array with 100 elements, but initialize only the first 3 elements cli::array<int>^ array2 = gcnew cli::array<int>(100) {1, 2, 3}; // Multi-dimensional array cli::array<int, 3>^ array3 = gcnew cli::array<int, 3>(4,5,2); // Array consisting of System::String objects cli::array<System::String^>^ array4 = gcnew array<System::String^> {"Hello", "World"}; // Initialize an array in a loop. A tracking reference is used to refer to // the array's elements so that the elements' value can be changed. If a // handle were used, the assignment would merely cause the handle to refer // to a new string object. cli::array<System::String^>^ array5 = gcnew cli::array<System::String^>(5); for each (System::String^% s in array5) { s = gcnew System::String("abc"); }
Properties
public ref class Foo { public: property int Age { int get() { return m_age; } void set(int value) { m_age = value; } } property String^ Name { String^ get() { return m_name; } private: void set(String^ value) { m_name = value; } } private: int m_age; String^ m_name; };
Notes:
- The property "Name" is read-only to external clients; the setter is available only from within the class
Delegates
Delegate
Example for a delegate declaration:
delegate void FooDelegate(int, float);
Event Source
Example for an event source declaration which uses the delegate from the example further up:
interface struct IFooEventSource { public: event FooDelegate^ FooEvent; void fire(int, float); }; ref class FooEventSource : public IFooEventSource { public: virtual event FooDelegate^ FooEvent; virtual void fire(int i, float f) { FooEvent(i, f); } };
Event Handler
If the event handler is part of a native/unmanaged class, then the handler must be declared static
.
class FooEventReceiver { public: static void FooEventHandlerStatic(int i, float f); } // Register event handler IFooEventSource^ eventSource = gcnew FooEventSource(); FooEventReceiver^ eventReceiver = gcnew FooEventReceiver(); eventSource->FooEvent += gcnew FooDelegate(FooEventReceiver::FooEventHandlerStatic); // Trigger event eventSource->fire(42, 3.14); // Unregister event handler eventSource->FooEvent -= gcnew FooDelegate(FooEventReceiver::FooEventHandlerStatic);
If the event handler is part of a managed class, the handler may be declared as an instance method. A function pointer is used to register the handler.
ref class FooEventReceiver { public: void FooEventHandler(int i, float f); } // Register event handler IFooEventSource^ eventSource = gcnew FooEventSource(); FooEventReceiver^ eventReceiver = gcnew FooEventReceiver(); eventSource->FooEvent += gcnew FooDelegate(eventReceiver, &FooEventReceiver::FooEventHandler); // Trigger event eventSource->fire(42, 3.14); // Unregister event handler eventSource->FooEvent -= gcnew FooDelegate(eventReceiver, &FooEventReceiver::FooEventHandler);
Abstract classes and pure virtual methods
public ref class BaseClass abstract { public: void doSomething(); virtual void doSomethingElse() abstract; }; public ref class SubClass : public BaseClass { public: virtual void doSomethingElse() override; };
Notes:
- In C++/CLI, the C++ concept of a "pure virtual method" is achieved using the the keyword
abstract
- If a method is declared
abstract
, its class must be declaredabstract
, too - A class can be declared
abstract
even if none of its methods is declaredabstract
. The effect is that that class simply cannot be instantiated directly. - A method in a subclass must specify
override
to indicate that it overrides its abstract base class counterpart
Interaction between managed/unmanaged code
Managed wrapper for an unmanaged object
ref class ManagedClass { private: UnmanagedClass* m_unmanagedReference; public: ManagedClass(UnmanagedClass* unmanagedReference) { System::Diagnostics::Debug::Assert(unmanagedReference != nullptr); m_unmanagedReference = unmanagedReference; } };
Notes:
- The reference to the unamanged object is made with a native C++ pointer
- It is possible to compare native C++ pointers with
nullptr
Unmanaged wrapper für managed object
#include <vcclr.h> // for gcroot class UnmanagedClass { private: gcroot<ManagedClass^> m_managedReference; public: UnmanagedClass(ManagedClass^ managedReference) { System::Diagnostics::Debug::Assert(managedReference != nullptr); this->m_managedReference = managedReference; bool gcrootReferenceIsNull = System::Object::ReferenceEquals(this->m_managedReference, nullptr); } };
Notes:
- A reference to a managed object within a non-managed context is achieved using the
gcroot
construct gcroot
is an unmanaged construct that is capable of keeping a reference to a managed object- A special header file must be included to to be able to access
gcroot
- A reference in a
gcroot
construct cannot be directly compared tonullptr
; one way how to make such a comparison is the static methodSystem::Object::ReferenceEquals
Marshalling data
In the case of primitive data types (Int32
, Int64
, etc.) the compiler automatically performs marshalling, i.e. you don't need to write any explicit marshalling code. For some complex data types (notably string types), Visual Studio provides marshalling functions in a special support library. Read this MSDN article for details.
The support library's central function is named marshal_as
. An example for its simple usage is the marshalling of CString
to System::String
:
#include <msclr/marshal_atl.h> // destination type is CString, so we can't juse include marshal.h CString unmanagedString = "foo"; System::String^ managedString = msclr::interop::marshal_as<System::String^>(unmanagedString);
In the example above, the destination data type is an object type. If the destination data type is not an object type but a chunk of memory on the heap (e.g. char*
), someone must make sure that the chunk of memory is de-allocated after marshalling is complete. The marshal_context
class can be used for this. As its name says, the class provides a so-called marshalling context: As long as the marshal_context
object exists it keeps the marshalling result alive on the heap. When the marshal_context
object goes out-of-scope it is automatically destroyed and "garbage-collects" the marshalling result.
Example for marshalling System::String
to LPCTSTR
:
#include <msclr/marshal.h> // destination type is LPCSTR, i.e. const char*, so we don't need marshal_atl.h System::String^ managedString = "foo"; msclr::interop::marshal_context^ marshalContext = gcnew msclr::interop::marshal_context(); LPCTSTR unmanagedString = marshalContext->marshal_as<LPCSTR>(managedString);
Debug assertions
Unlike native debug assertions made with the C function assert()
(which actually is a preprocessor macro), managed debug assertions made with System.Diagnostics.Debug.Assert()
cannot be filtered out automatically by the C++/CLI compiler. The reason for this possibly is that the compiler cannot evaluate the following attribute which is used in the .NET API declaration of the System.Diagnostics.Debug.Assert
method:
[Conditional("DEBUG")]
As a workaround, all debug assertions must be surrounded by an #ifdef DEBUG
preprocessor statement. Example:
#ifdef DEBUG System::Diagnostics::Debug::Assert(anObject != nullptr); #endif
This limitation of the C++/CLI compiler, and the workaround, is officially documented in this MSDN article.
typeof() operator
The typeof()
operator known from C# does not exist in C++/CLI. Instead you have to use the keyword typeid
. For instance:
System::Type^ typeToCheck = [...]; if (typeToCheck == System::Boolean::typeid) [...] if (typeToCheck == cli::array<System::String^>::typeid) [...]
The official documentation can be found in this MSDN article.
#pragma make_public
Native C++ does not have the concept of "public" or "private" classes. Nevertheless, in some circumstances C++/CLI may force you to mark a native C++ class as public. You cannot do so by adding the keyword public
to the class declaration in a .h file, because that is a C++/CLI keyword and would make the class declaration invalid when the .h file is included from a native C++ project (i.e. the compiler would spit out a compiler error).
The workaround is the following #pragma:
#pragma make_public(NamespaceFoo::ClassFoo)
TODO: Add better explanation of problem scenario.
Exceptions
In C++/CLI the throw
keyword can be used to throw not only native but also managed exceptions:
std::exception nativeException; throw nativeException; System::Exception^ managedException = gcnew System::Exception(); throw managedException;
The catch
keyword has also been extended in C++/CLI so that it is possible to catch not only native but also managed exceptions. In addition you can write a finally
clause, which is not possible in native C++ (although the Visual C++ compiler knows the __finally
keyword, to catch SEH exceptions, that is not native C++)).
try { [...] } catch (std::exception& nativeException) { [...] } catch (System::Exception^ managedException) { [...] } finally { [...] }
The examples above merely show the most common exception handling use cases, however there are quite a few other things that it might be useful to know (e.g. as stack unwinding, boxing, evaluation order of catch
clauses, etc.). Details can be found in this MSDN article.
async / await
async
and await
are C# keywords, which means they are part of the C# programming language and are supported by the C# compiler. As far as I know the C# compiler transforms the keywords into code that works with System.Threading.Tasks.Task
from the Task Parallel Library (TPL).
Microsoft has never added corresponding keywords to C++/CLI, and of course standard C++ also doesn't know the keywords, therefore you simply can't use async
and await
in C++/CLI.
If you want to use asynchronous programming in C++/CLI, it is best to directly use stuff from the TPL, especially the aforementioned System.Threading.Tasks.Task
. Unfortunately you will have to manually write all the boilerplate code that the C# compiler generates for you when it encounters async
and await
. An alternative is the upcoming C++17 standard - apparently there are plans to add better support for asynchronous programming to that version of the C++ programming language (keywords are resumable
and await
).