LearningC++CLI

From HerzbubeWiki
Jump to navigation Jump to search

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


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 of new 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
  • 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 to Dispose() at the time when the variable goes out-of-scope.
    • In C# the Dispose() method must be explicitly invoked

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 declared abstract, too
  • A class can be declared abstract even if none of its methods is declared abstract. 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 to nullptr; one way how to make such a comparison is the static method System::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).