Uploaded by Kia Chuan Ch'ng

CPP CLI tutorial

advertisement
C++/CLI Tutorial
Author: Adam Sawicki, adam__DELETE__@asawicki.info, www.asawicki.info
Version 1.0, December 2011
Table of Contents
Table of Contents .............................................................................................................................................................. 1
Introduction ...................................................................................................................................................................... 2
What is C++/CLI? ............................................................................................................................................................... 2
Why Use C++/CLI? ......................................................................................................................................................... 2
What C++/CLI is Not? .................................................................................................................................................... 2
Hello World Example ........................................................................................................................................................ 3
Project Properties ............................................................................................................................................................. 3
Namespaces ...................................................................................................................................................................... 4
Classes and Pointers.......................................................................................................................................................... 5
Native Classes ............................................................................................................................................................... 5
Managed Classes ........................................................................................................................................................... 6
Managed Structures...................................................................................................................................................... 7
Managed Class Destructors .......................................................................................................................................... 7
Pointers and References ............................................................................................................................................... 9
Rules of Containment ................................................................................................................................................. 10
Additional Class Topics................................................................................................................................................ 12
Windows Example........................................................................................................................................................... 13
Delegates and Events ...................................................................................................................................................... 15
Strings ............................................................................................................................................................................. 16
Native Strings .............................................................................................................................................................. 16
Managed Strings ......................................................................................................................................................... 16
String Conversions ...................................................................................................................................................... 17
Conversions to and from Numbers ............................................................................................................................. 17
Building Strings............................................................................................................................................................ 18
Value Formatting ........................................................................................................................................................ 18
Decimal Mark Issues ................................................................................................................................................... 19
Enumerations .................................................................................................................................................................. 19
Type Casts ....................................................................................................................................................................... 20
Boxing.......................................................................................................................................................................... 21
Properties........................................................................................................................................................................ 21
Exceptions ....................................................................................................................................................................... 22
Arrays .............................................................................................................................................................................. 23
Native Arrays ............................................................................................................................................................... 23
Managed Arrays .......................................................................................................................................................... 23
Containers ....................................................................................................................................................................... 24
Locking ............................................................................................................................................................................ 25
Using Libraries ................................................................................................................................................................. 26
Using Native Libraries ................................................................................................................................................. 26
1
Using Managed Libraries............................................................................................................................................. 27
Native Types in Exported Functions............................................................................................................................ 28
Summary ......................................................................................................................................................................... 29
Introduction
Welcome to my C++/CLI tutorial. I’m not sure whether it can be called a proper tutorial, but anyway my intention
was to write an introductory article from which you can learn basics of this language. I’ve been using C++/CLI during
past 2 years in my previous work. Not that it was my decision to use this technology, but I now think my boss was
right in choosing C++/CLI for the kinds of projects we did in the company. C++/CLI is a nice language and has some
unique features, so I think it’s worth knowing. This article is far from being comprehensive or systematic. It’s more
practice oriented, based on my experiences in developing quite complex software in C++/CLI, so I described here
only these language features that I know well and I’ve found useful in my code, not all what is available in the
language syntax.
If you ask about a difficulty level of this text, I’d answer that it’s intermediate. It’s not advanced as it only introduces
basics of a programming language, but on the other hand I believe that to understand C++/CLI you must already
know both native programming in C++ (including subjects such as headers, pointers, classes) as well as .NET
(including knowledge about garbage collector, Windows Forms, .NET standard library).
What is C++/CLI?
C++/CLI is a separate programming language which can be viewed as an extension to C++ syntax, but at the same
time it’s one of the programming languages of .NET platform, just like C# or Visual Basic .NET. It’s designed and
implemented by Microsoft, so it’s not portable to Linux or other systems. It requires Microsoft Visual C++ IDE to
compile and debug code, so we have no alternative compilers like gcc or icc available for this language. Good news is
that the language is supported in Visual Studio 2005, 2008 and 2010, including corresponding versions of the free
Visual C++ Express Edition.
To run programs written in C++/CLI, just like for other .NET applications, user must have appropriate version of the
free Microsoft .NET Framework installed in his Windows. I didn’t succeed in running a C++/CLI program in Linux,
neither using Wine nor Mono.
Why Use C++/CLI?
Why learn and use such weird hybrid language if we have C# with nice syntax, designed specifically for .NET
platform? C++/CLI has an unique feature among other .NET languages: You can freely mix managed (.NET) and native
(C++) code in a single project, single source file, even single function code. It makes the language hard to replace in
some applications, like:



When you write software that needs to use some native as well as managed libraries.
When you write a library that links native with managed code, for example exposes an interface of some
native library to .NET code.
When you write a program that needs both efficient processing of some binary data (which is best done in
native C++ code using pointers) and has complex graphical interface (which is best done using Windows
Forms) – just like I did in my job.
What C++/CLI is Not?
You may think that C++/CLI is some ugly, proprietary hack to C++ introduced by bad “M$” to confuse programmers.
That’s not exactly true. Microsoft put effort to make this language really good. Some famous computer scientists and
2
C++ gurus were involved in its development, like Stanley B. Lippman and Herb Sutter. The language itself is
standardized as ECMA-372.
I must also clarify that C++/CLI is not the same as the old language called Microsoft Extensions for C++. That strange
language which extended C++ syntax with ugly keywords like __gc or __value was predecessor of C++/CLI and is now
deprecated. The syntax of C++/CLI is totally different and much more pleasant, as you will see in the next chapters.
Hello World Example
It’s time for the first example code. You can find it in attached archive as Hello_world subdirectory. I coded all
examples for this tutorial in Visual C++ 2010 Express. Traditionally first example will be a simple application that
prints “Hello World” text on its output. To make it I’ve started Visual C++ Express and created new project of type
CLR Console Application. So it is a project in C++/CLI that compiles to an EXE file. It’s .NET application – it requires
.NET Framework installed. It shows system console and no windows. Project contains some automatically generated
files, including AssemblyInfo.cpp where you can fill in some metadata about your program like product name and
version. But the main source file is Hello_world.cpp with following contents:
#include <iostream>
int main(array<System::String ^> ^args)
{
System::Console::WriteLine(L"Managed Hello World");
std::cout << "Native Hello World" << std::endl;
return 0;
}
This example shows two messages. First one is done using traditional .NET way – WriteLine static method of
System.Console class. Second is done using method recommended for C++ - std::cout stream from standard C++
library, which required #include <iostream>. As you can see, managed and native code are freely mixed here in a
single function code. That’s the biggest power of C++/CLI! Of course there are more strange things here that can
seem confusing to you, but don’t worry – I’ll explain them later. Right now here is the output of this program:
Managed Hello World
Native Hello World
Project Properties
It’s time to go deeper into some details of the language syntax. If you already know C++ and/or some .NET language
and Visual Studio IDE, I hope you know how to get to the Project Properties window. Project properties look totally
different in native C++ and in .NET. The window you see when coding in C++/CLI is like in native C++ project.
There is one thing about it I must emphasize at the beginning: an option in Configuration Properties / General
branch called Common Language Runtime Support. This option decides whether the compiler will treat and compile
your code as it is in native C++ or C++/CLI, but there are several options for C++/CLI, all starting from /clr. The one
you should choose is /clr and NOT the /clr:pure. I’m not sure what they do :) but switching this option as described
helped me many times fixing some strange compiler/linker errors, like the hated “Unresolved external symbol…”.
3
Namespaces
Namespaces work similar way in native and managed code. In C++/CLI they have a syntax like in C++ and can be
freely mixed. There is no distinguish between native and managed namespaces, like you will see for classes. Here is a
small example of defining a namespace (MyNamespace), qualifying identifier with a namespace
(MyNamespace::DoEverything) and importing a namespace with using namespace directive:
#include <iostream>
using namespace System;
using namespace std;
namespace MyNamespace
{
void DoEverything()
{
Console::WriteLine(L"Managed Hello World"); // System::Console
cout << "Native Hello World" << endl; // std::cout, std::endl
}
}
int main(array<System::String ^> ^args)
{
MyNamespace::DoEverything();
return 0;
}
4
Classes and Pointers
Here comes the biggest piece of knowledge you have to learn to use C++/CLI – the one about classes, objects,
pointers, references, destructors, garbage collector etc. Don’t worry – I will explain everything step by step, show
some simple examples and summarize everything at the end.
First thing you need to know is that there is strict distinction in C++/CLI between native and managed classes and
that some rules apply to some specific types. There are also separate pointer/reference operators for these two
types. Pointers to native objects work like in native C++. You have to free them manually – they are not garbage
collected. Objects of managed classes, on the other hand, behave like in C# and the rest of .NET platform – you have
to allocate them dynamically on the heap and they are automatically freed by the garbage collector. There is a
completely new syntax in C++/CLI for these managed objects that you will see soon.
Native Classes
Let’s start with something you already know – native C++ classes. You can define them and use them just like there
was no managed part, only the old good native C++. You can, for example, define a class like this:
class NativeMonster
{
public:
NativeMonster(int HP) : m_HP(HP) {
cout << "NativeMonster Constructor" << endl;
}
~NativeMonster() {
cout << "NativeMonster Destructor" << endl;
}
void TellHitPoints() {
cout << "NativeMonster has " << m_HP << " HP" << endl;
}
private:
int m_HP;
};
NativeMonster is a native class. It has constructor, destructor, a private field m_HP and a public method
TellHitPoints. You can use it in some code by creating a local object of this type by value, on the stack, like this:
int main(array<System::String ^> ^args) {
NativeMonster stack_monster(100);
stack_monster.TellHitPoints();
}
This program prints following output:
NativeMonster Constructor
NativeMonster has 100 HP
NativeMonster Destructor
You can also allocate native objects on the heap, using standard C++ operator new. Always remember to free them
with operator delete when no longer needed because native objects are not garbage collected – forgetting to free
them causes memory leaks! Following program prints same output, but here the object of type NativeMonster is
allocated dynamically on the heap and used by pointer, not by value.
int main(array<System::String ^> ^args) {
NativeMonster *monster_pointer = new NativeMonster(100);
monster_pointer->TellHitPoints();
delete monster_pointer;
}
5
As you can see, native classes can be coded in C++/CLI same way as in native C++. You use class keyword, implement
constructor, destructor, fields and methods (or not, depending on whether you need them) and then you can create
objects of this class in the stack or on the heap, allocate them with new, free them with delete and reference them
with pointers. You use * operator for declaring pointers and pointer dereference, & operator for declaring references
and getting object address (not shown in the example) and -> operator to access object members from a pointer –
all exactly like in C++.
Native structures look similarly so I won’t show them here. Just like in C++ you can define structures using struct
keyword and then use them by value or by native * pointer.
Managed Classes
Now it’s time to see how a managed class looks like in C++/CLI. Here is an example:
ref class ManagedMonster
{
public:
ManagedMonster(int HP) : m_HP(HP) {
cout << "ManagedMonster Constructor" << endl;
}
void TellHitPoints() {
cout << "ManagedMonster has " << m_HP << " HP" << endl;
}
private:
int m_HP;
};
First thing that strikes the eye here is that a special new keyword ref class is used. It tells the compiler that we
define a managed class. Rest is the same – you can write constructor and use C++ initialization list inside it, you can
define fields and methods, you can use private, protected and public keywords. You can also call native functions
(like using std::cout shown here) or managed function (like printing to console using System::Console::WriteLine)
no matter if you are inside a body of a native class or managed class method.
Objects of managed classes are never created on the stack but only on the heap. A new type of “pointer” – a
reference to a managed object, uses ^ character in C++/CLI. It is a managed heap this time so different rules apply to
allocation and deallocation of such objects. We allocate them using gcnew keyword. There is no delete operator
because managed objects are automatically freed by the garbage collector –in some unspecified future, possibly on
another background thread, but always after such objects is no longer referenced by any pointer. For example:
ManagedMonster ^monster_ref = gcnew ManagedMonster(120);
monster_ref->TellHitPoints();
This example prints following output:
ManagedMonster Constructor
ManagedMonster has 120 HP
If you want to clear the pointer, just assign null to it. But that’s not a normal “null”, you cannot use NULL macro or 0
here. You have to do it with special nullptr keyword, which is an empty value of type compatible with managed
pointers.
monster_ref = nullptr;
nullptr was introduced as keyword in C++/CLI. You can also use it as null value of native pointers. when coding in
C++/CLI. But same keyword is also being introduced to the new native C++11 standard (formerly known as C++0x) as
a replacement of 0 or NULL for pointers, so you can also use it in native C++.
6
Managed Structures
Difference between classes and structures in native C++ is very small – structures have public members and public
inheritance by default unless you explicitly state otherwise, while classes default to private. In C#, on the other hand,
structures are very different – they have limited functionality and they are passed by value, not by reference.
C++/CLI supports the former with class and struct keywords, as well as the latter by introducing the ref and value
keywords. So to define a managed structure in C++/CLI that will be stored by value like the System::DateTime or
System::Drawing::Color type, use value class or value struct. For example:
value class Position {
public: float x, y, z;
};
int main(array<System::String ^> ^args) {
Position pos;
pos.x = 0.0f;
pos.y = 1.0f;
pos.z = 7.0f;
Console::WriteLine(L"The position is ({0},{1},{2})", pos.x, pos.y, pos.z);
}
So to summarize the available types of classes and structures in C++/CLI, let’s see a table:
Keyword
class
struct
ref class
ref struct
value class
value struct
Domain
Native
Native
Managed
Managed
Managed
Managed
Default Access
private
public
private
public
private
public
Equivalent
class in C++
struct in C++
class in C#
class in C#
struct in C#
struct in C#
Used by
Value, native pointer or reference
Value, native pointer or reference
Managed reference
Managed reference
Value
Value
Managed Class Destructors
Despite managed objects being automatically freed by garbage collector, they can have destructors. What is more,
there are two kinds of such special methods in C++/CLI – a destructor and a finalizer. They have to be used
whenever:


Your class keeps some resources that will not be freed by garbage collector but have to be freed manually,
like a native pointer or an object that needs to have some Close() method called before destruction.
Your class keeps some resources that should not stay acquired for as long as garbage collector wants but
should be freed right after they are no longer needed because they occupy a lot of memory or keep lock of
some system resources, like a bitmap, opened file, network socket or database connection.
Finalizer – declared as !ClassName(); – is equivalent to overriding Object::Finalize method and to the class
destructor in C#. It is called by garbage collector right before an object is freed from memory. Such call can be made
in some unspecified future and on a separate background thread.
Destructor on the other hand – declared like C++ destructor as ~ClassName(); – is equivalent to implementing
IDisposable interface and its Dispose method, as well as calling GC::SuppressFinalize at the end. It all happens
automatically so you don’t have to explicitly inherit from IDisposable or anything. All you need to do is to define a
destructor.
Remember that this managed destructor – just like Dispose method you probably know from C# – is not guaranteed
to be called. It is available for the user of your class if he decides to explicitly order your object to free its resources in
7
some specific moment, but if he does not, then you have to do all the necessary cleanup in the finalizer. Here is an
example:
ref class ManagedMonster {
public:
ManagedMonster(int HP);
~ManagedMonster();
!ManagedMonster();
void TellHitPoints();
private:
int *m_DynamicHP;
};
ManagedMonster::ManagedMonster(int HP) : m_DynamicHP(new int(HP)) {
cout << "ManagedMonster Constructor" << endl;
}
ManagedMonster::~ManagedMonster() {
cout << "ManagedMonster Destructor" << endl;
this->!ManagedMonster();
}
ManagedMonster::!ManagedMonster() {
cout << "ManagedMonster Finalizer" << endl;
delete m_DynamicHP;
}
void ManagedMonster::TellHitPoints() {
cout << "ManagedMonster has " << *m_DynamicHP << " HP" << endl;
}
The class in this example has constructor, destructor and finalizer. There are multiple ways of using it. First way – the
one you have already seen goes like this:
int main(array<System::String ^> ^args) {
ManagedMonster ^monster_ref = gcnew ManagedMonster(120);
monster_ref->TellHitPoints();
}
// Garbage Collector frees allocated object at the end of the program.
Output of this program will be:
ManagedMonster Constructor
ManagedMonster has 120 HP
ManagedMonster Finalizer
As you can see, the destructor is not called, only the finalizer. That’s because we don’t make any use of IDisposable
implementation here. We dynamically allocate the managed object as usual, use it and let the garbage collector to
call its finalizer and free it at the end.
Next you will see two things that look very strange, so please pay attention to remember this syntax. In C# you order
an object of a class that implements IDisposable interface to free its resources by calling Dispose method. C++/CLI
equivalent is to use… delete operator on a managed object, like this:
int main(array<System::String ^> ^args) {
ManagedMonster ^monster_ref = gcnew ManagedMonster(120);
monster_ref->TellHitPoints();
delete monster_ref; // Call Dispose.
}
8
The output of this program is:
ManagedMonster
ManagedMonster
ManagedMonster
ManagedMonster
Constructor
has 120 HP
Destructor
Finalizer
Using delete operator calls IDisposable.Dispose method, implemented in our code as class destructor. Calling this
destructor automatically suppresses finalizer so the finalizer is not called before garbage collector frees the object –
we have to call it manually from destructor, following the practice recommended by Microsoft. Main difference
between this example and the previous example is that here we explicitly specify the moment where we want the
finalizer to be called, while in the previous example garbage collector is free to call finalizer in some distant future,
leaving some possibly heavy resources (like opened file, database connection or… a dynamically allocated native int
in our case) locked and loaded for a longer time.
Another, more convenient way of calling Dispose in C# is the using keyword. We can enclose construction of an
IDisposable object in the using(){} section and the Dispose method will be automatically called at the end of its
scope. C++/CLI has equivalent mechanics but it’s syntax is also weird. To do this you need to define an object of our
managed class by value, not by managed pointer ^ - like it was constructed on the stack, although managed objects
cannot really be on the stack, they are always dynamically allocated from the managed heap. When an object is
defined like this, its destructor is automatically called when we go out of the current scope.
int main(array<System::String ^> ^args) {
ManagedMonster monster(120);
monster.TellHitPoints();
} // End of scope calls Dispose.
Output of this program is same as the previous one.
Pointers and References
As you already seen, we have two types of pointers available in C++/CLI. First are native pointers. They look and
behave exactly like in native C++, so you allocate objects with new operator, you must manually free them with
delete operator and you use operators * and &. Second type of pointers are references to managed objects. We
allocate them with gcnew operator, don’t have to free them as they are tracked by the garbage collector and the
operator for them is ^. The only thing that is missing there is the opposing operator for managed pointers, like & for
native pointers that gets object address or defines a reference. There actually is such operator and it looks like this:
%. Example:
void AskMonster(ManagedMonster ^monster) {
monster->TellHitPoints();
}
int main(array<System::String ^> ^args) {
ManagedMonster ^monster_1 = gcnew ManagedMonster(100);
AskMonster(monster_1);
ManagedMonster monster_2(200);
AskMonster(%monster_2);
}
This program prints following output:
ManagedMonster
ManagedMonster
ManagedMonster
ManagedMonster
Constructor
has 100 HP
Constructor
has 200 HP
9
Another application of the % operator is to pass a parameter to a function by reference, to be able to return a new
value. It is so called output parameter. For example:
void Add(int %result, int a, int b) {
result = a + b;
}
int main(array<System::String ^> ^args) {
int result, a = 2, b = 3;
Add(result, a, b);
Console::WriteLine(L"{0} + {1} = {2}", a, b, result);
}
This program prints following output:
2 + 3 = 5
To summarize this topic, let’s see a table with all operators for pointers and references in C++/CLI:
Operation
Pointer definition
Pointer dereference
Reference definition
Address-of
Member access
Allocation
Deallocation
Native Code
Managed Code
*
^
&
%
->
->
new
gcnew
delete
delete (calls Dispose)
Rules of Containment
I’ve already shown that native and managed code can be freely mixed in a body of a single function. Unfortunately
there is no such freedom when it comes to defining variables/fields of different types. Some restrictions apply here.
First, we cannot define global variables of managed types. That’s probably because in .NET everything must lie inside
classes.
NativeMonster g_GlobalNativeMonster(100); // OK
ManagedMonster ^g_GlobalManagedMonster = gcnew ManagedMonster(200); // ERROR
The compilation error we get here is:
error C3145: 'g_GlobalManagedMonster' : global or static variable may not have managed type
'ManagedMonster ^' may not declare a global or static variable, or a member of a native type that refers
to objects in the gc heap
Workaround for this limitation is to define a managed class with public, static member of the managed type we need
as a global variable – like this:
ref class Globals {
public:
static ManagedMonster ^GlobalManagedMonster = gcnew ManagedMonster(200);
};
Second, managed class can have members of native pointer types, but native classes cannot contain managed
objects. That’s probably because it would be hard for garbage collector to track the location and lifetime of a
managed pointer if it would be contained in a native data structure.
ref class ManagedDungeonRoom {
NativeMonster *m_Goblin; // OK
10
ManagedMonster ^m_Boss;
};
// OK
class NativeDungeonRoom {
NativeMonster *m_Goblin; // OK
ManagedMonster ^m_Boss; // ERROR
};
The error we get here is:
error C3265: cannot declare a managed 'm_Boss' in an unmanaged 'NativeDungeonRoom' may not declare a
global or static variable, or a member of a native type that refers to objects in the gc heap
Fortunately there is a solution to this. You need to use a special smart pointer class: msclr::gcroot from
<msclr\gcroot.h> header. Here is an example:
#include <msclr\gcroot.h>
class NativeDungeonRoom {
private:
NativeMonster *m_Goblin; // OK
msclr::gcroot<ManagedMonster^> m_Boss; // Also OK
public:
NativeDungeonRoom()
: m_Goblin(new NativeMonster(10))
, m_Boss(gcnew ManagedMonster(1000)) {
}
~NativeDungeonRoom() {
delete m_Goblin;
}
void AskMonsters() {
m_Goblin->TellHitPoints();
m_Boss->TellHitPoints();
}
};
int main(array<System::String ^> ^args) {
NativeDungeonRoom dungeon_room;
dungeon_room.AskMonsters();
}
This program prints following output:
NativeMonster Constructor
ManagedMonster Constructor
NativeMonster has 10 HP
ManagedMonster has 1000 HP
NativeMonster Destructor
If you try to define member variables of different types by value, not by pointer, the result will be like this:
ref class ManagedDungeonRoom
NativeMonster m_Goblin; //
ManagedMonster m_Boss; //
Position m_Position;
//
};
{
ERROR
OK.
OK.
class NativeDungeonRoom {
NativeMonster m_Goblin; // OK.
ManagedMonster m_Boss; // ERROR
11
Position m_Position;
};
// OK.
NativeMonster cannot be contained inside ManagedDungeonRoom because of error:
error C4368: cannot define 'm_Goblin' as a member of managed 'ManagedDungeonRoom': mixed types are not
supported
While ManagedMonster cannot be contained inside NativeDungeonRoom because of these errors:
error C3265: cannot declare a managed 'm_Boss' in an unmanaged 'NativeDungeonRoom' may not declare a
global or static variable, or a member of a native type that refers to objects in the gc heap
error C3076: 'NativeDungeonRoom::m_Boss' : you cannot embed an instance of a reference type,
'ManagedMonster', in a native type
Additional Class Topics
You may be accustomed to this, but in fact C++ has quite weird syntax for object-oriented concepts. Method is
abstract if defined as pure virtual =0 while it still can but don’t have to have a body, class is abstract automatically if
it has some pure virtual methods, methods overridden in derived class can repeat virtual keyword but don’t have
to, there is no distinction for classes and interfaces… C#, on the other hand, has different syntax, with lots of
keywords for such OOP concepts, like abstract, override, new etc. To merge these two worlds, designers of C++/CLI
provided following syntax:
When a reference type inherits from another reference type, virtual functions in the base class must explicitly be
overridden (with override) or hidden (with new – new slot in vtable). The derived class functions must also be
explicitly marked as virtual.
ref class ManagedMonster {
public: virtual int GetMaxHP() { return 0; }
};
ref class Boss : public ManagedMonster {
public: virtual int GetMaxHP() override { return 1000; }
};
Interfaces can be defined using interface keywords, by typing interface class or interface struct (which mean
the same). Interfaces are not full classes – they can contain only declarations of public functions, events and
properties. They can also have static members.
interface class IMonster {
public: int GetMaxHP();
};
ref class Boss : public IMonster {
public: virtual int GetMaxHP() { return 1000; }
};
A class that does not implement all abstract methods declared in inherited classes and interfaces is automatically
abstract. A method or whole class can also be forced to be considered abstract by using abstract keyword. Abstract
class cannot be instantiated. Abstract method declared using abstract keyword is similar to a pure virtual method
declared using =0 syntax known from native C++.
ref class BaseMonster abstract {
public: virtual void MakeNoise() abstract;
};
12
sealed keyword can be used for classes and methods. For classes it means that no other classes can inherit from this
one.
ref class BossOfLevel1 sealed : public ManagedMonster {
// ...
};
For methods the sealed keyword means that the virtual method cannot be further overridden in derived classes.
ref class Boss : public ManagedMonster {
public: virtual int GetMaxHP() sealed { return 1000; }
};
Static class – a class that can contain only static members and cannot be instantiated – is defined in C++/CLI using
abstract sealed keywords:
ref class Globals abstract sealed {
public: static ManagedMonster ^g_GlobalMonster;
};
Last but not least, managed classes require declaration prior to usage and can have forward declarations, just like
native C++ classes. So for example if you define a member variable of some managed class in another class in your
code:
ref class ManagedDungeonRoom {
NativeMonster *m_Goblin;
ManagedMonster ^m_Boss;
};
Then you need to either #include header with definition of this class:
#include "ManagedMonster.h"
or forward declare this class, which is a good idea to do wherever possible because it reduces dependencies
between headers and reduces compilation time:
ref class ManagedMonster;
Windows Example
I think now it’s time for another, bigger example. It will be GUI application this time, not console one. You can find
the source code and compiled EXE in the Form_app subdirectory of attached archive. To create this project I selected
New Project command in Visual C++ 2010 and selected CLR / Windows Forms Application.
This created a project with some files already filled at start: resources containing default icon, AssemblyInfo.cpp file,
precompiled header stdafx.h, Form_app.cpp source file with main() function and, what is most important here, an
initial Windows Forms window split into two files: Form1.h (intended to be modified in text editor) and Form1.resX
(manged by visual window designer). By right-clicking on the Form1.h node in Solution Explorer and selecting View
Designer, you open visual form designer where you can design your window, put controls onto it using Toolbox panel
and change their properties using Properties panel. Everything exactly like in C# and other .NET languages. By rightclicking on the Form1.h and selecting View Code, you open the Form1.h file in text editor. There you can see definition
of Form1 class with InitializeComponent method enclosed in #pragma region, filled with code that creates and
initializes form controls you have designed.
Visual C++ creates only header file for the form. You could theoretically write all your code there, but what I like to
do is creating corresponding cpp file and writing definitions of the class methods there, like we usually do in native
13
C++. To do it in this project I added New Item of type C++ File (.cpp) and named it Form1.cpp. I then moved
definition of class constructor and destructor there, leaving only declarations in header file. So at this stage the
content of the Form1.cpp file is:
#include "stdafx.h"
#include "Form1.h"
namespace Form_app {
Form1::Form1()
{
InitializeComponent();
}
Form1::~Form1()
{
if (components)
delete components;
}
} // namespace Form_app
The logic I’ve put into this application is a simple calculator that performs addition of two numbers. To achieve this I
used three TextBox controls (two for arguments, third for result), a button and some labels. Additionally I’ve added a
“History” ListBox to add information about each operation performed to the list. The method that handles button
click and performs all these operations contains following code:
System::Void Form1::button1_Click(System::Object^
{
try {
double a = double::Parse(textBox1->Text);
double b = double::Parse(textBox2->Text);
double c = a + b;
textBox3->Text = c.ToString();
sender, System::EventArgs^
e)
System::Text::StringBuilder sb;
sb.AppendFormat(L"{0} + {1} = {2}", a, b, c);
listBox1->Items->Add(sb.ToString());
}
catch (System::Exception ^ex) {
MessageBox::Show(this, ex->Message, L"Error", MessageBoxButtons::OK, MessageBoxIcon::Error);
}
}
Despite its simplicity a lot of things happen here. Many of them can be new to you. But don’t worry – I’ll explain all
of them in following chapters of this tutorial.
14
Delegates and Events
There is one feature of a programming language that is especially useful when working with GUI. It’s called
delegates, events, sometimes signals, slots, callbacks or pointers to members. It is one of these things that modern,
high level programming languages have, while native C++ does not. Delphi has it, C# and other languages of .NET
platform also have it. You may say that C++ also has pointers to class methods in its syntax, but I mean something
completely different. Pointers to members in C++ remember just an offset – point to some method of matching
declaration but only inside given particular class and do not remember a particular object of this class. This make
them not very useful. The pointers to members I mean here should be able to point to a PARTICULAR method with
matching declaration but in a PARTICULAR object of ANY class. That’s what is needed when we code a class
representing a window and we want to implement some methods that will be called when a button on this form is
pressed, a textbox is being changed etc.
To compensate for the lack of this feature, all GUI libraries for native C++ (like wxWidgets, Qt or MFC) provide their
own solution for such pointers. There are also some independent libraries for this, like FastDelegate by Don Clugston
or The Impossibly Fast C++ Delegates by Sergey Ryazanov. They work this way: each pointer to member holds two
fields: a pointer to some object and an offset to method in its class.
Fortunately C++/CLI provides such mechanism just like whole .NET does in form of delegates and events. You can see
the example usage in the code we talk about here, as method Form1::button1_Click reacting on clicking button1.
Documentation says that Button class has Click event of type event EventHandler^, where EventHandler is defined
as following delegate (using C++/CLI syntax):
delegate void EventHandler(Object^ sender, EventArgs^ e)
After double-clicking on the button1 in form designer, Visual C++ creates the button1_Click method with signature
compatible with this delegate and binds it to the button1.Click event. The syntax for registering method to react on
some event can be found in automatically-generated code inside InitializeComponent method and looks like this:
this->button1->Click += gcnew System::EventHandler(this, &Form1::button1_Click);
As you can see, using events in C++/CLI requires:
1. Creating a delegate with gcnew. It takes two arguments – pointer to some object (this) and pointer to some
method of its class that has signature compatible with the event (Form1::button1_Click).
2. Adding delegate to the event with += operator.
15
Strings
There are different ways of handling character strings in C and C++. Programs written in C use char* type. Standard
library of C++ defines std::string class. .NET library has its own built-in type for that – System::String class aliased
in C# as just string keyword. We have all of this available in C++/CLI so we have to learn how to deal with such
different kinds of strings and how to convert between them.
Native Strings
First, we can use old good null-terminated C strings of type char*. We can also #include <string> and use
std::string class from C++ standard library (STL). Unicode versions (wchar_t* and std::wstring types) are also
available to use. Here are example variable declarations and initialization:
const char
*native_sz
= "Native ANSI C string";
const wchar_t *native_wsz = L"Native Unicode C string";
std::string
native_str = "Native ANSI C++ string";
std::wstring
native_wstr = L"Native Unicode C++ string";
Console::WriteLine(L"The length of native_sz is {0}", strlen(native_sz));
Console::WriteLine(L"Comparing two strings returned {0}", wcscmp(native_wsz, native_wstr.c_str()));
This program produces following output:
The length of native_sz is 20
Comparing two strings returned -1
Unicode strings are different subject and I won’t explain it in details here. I hope you know it a bit. In case you don’t,
here is a quick introduction: Traditional string characters of type char are single-byte and encoded in so called ANSI
code page. It means they contain ASCII characters and the meaning of upper 128 codes (or rather negative codes, as
char in C and C++ is really a signed integer number) change meaning depending on the current system codepage. For
example, in Poland we use Windows-1250 (CP1250) codepage so there are some values assigned to diacritic letters
we use in our language, like ą, ć, ł, ż.
Unicode, on the other hand, is a standard that allows encoding characters from different languages in a single string,
but requires more bytes per character. There are multiple ways of encoding Unicode strings into bytes, like UTF-8
widely used in the Web. But what we mean here by Unicode is the UTF-16 encoding, in which every character is a
16-bit unsigned number (takes two bytes). That’s what wchar_t type does (at least in Windows, in Visual C++
compiler) and also the std::wstring class. To write an Unicode string in C++ source code, we have to precede it with
L letter, like you can see in the example above. Such string is of type const wchar_t*.
Managed Strings
As I said before, .NET platform has its own, standard way of handling strings – the System::String class. They must
be used by managed reference ^ and allocated with gcnew. Characters of these strings are of type wchar_t – 16-bit
Unicode. You can initialize them with Unicode constants as there is an implicit conversion between const wchar_t*
and System::String. Initializing managed strings with ANSI strings also works and the characters are automatically
converted to Unicode.
Managed strings can be concatenated with + operator. You can also call its methods like ToUpper.
using System::String;
String ^managed_from_wide = gcnew String(native_wsz);
String ^managed_from_ansi = gcnew String(native_sz);
Console::WriteLine(L"managed_from_wide contains: " + managed_from_wide);
Console::WriteLine(L"managed_from_ansi contains: " + managed_from_ansi);
Console::WriteLine(L"ToUpper() returned: " + managed_from_wide->ToUpper());
The example above prints following output:
16
managed_from_wide contains: Native Unicode C string
managed_from_ansi contains: Native ANSI C string
ToUpper() returned: NATIVE UNICODE C STRING
String Conversions
If we have so many different ways of storing strings in C++/CLI, a question arises about how to convert between
them. Best solutions is of course not to do it – it’s always more efficient to keep data in the most natural format, the
one that is finally needed. But sometimes conversion is necessary. Here is how I do it:
You’ve already seen a conversion from C string (const char* or const wchar_t*) to managed string (System::String).
It’s done by the String class constructor. To convert STL string (std::string) to managed string, I just retrieve its C
string with c_str() method and then create managed string, like this:
std::string native_str = "Native ANSI C++ string";
String ^managed_from_stl = gcnew String(native_str.c_str());
Same effect can be achieved with PtrToStringAnsi method from Marshal class. The
System::Runtime::InteropServices::Marshal static class contains many methods useful for interoperability between
managed and native world (for pointers, data buffers, strings, errors, COM stuff, memory allocations and more) so I
recommend taking a look at it.
String ^managed_from_stl = System::Runtime::InteropServices::Marshal::PtrToStringAnsi(
(IntPtr)(void*)native_str.c_str());
Conversion in the opposite way is a little bit more complicated as it requires to somehow allocate a native buffer for
characters. The way I do it in my code is using Marshal::StringToHGlobalAnsi method that allocates and fills a native
memory buffer with null-terminated, ANSI string initialized from a managed string. I can then copy these data to
desired place, like into an STL string, before I have to free the buffer with call to Marshal::FreeHGlobal. My function
for converting managed to native strings is:
static void ClrStringToStdString(std::string &outStr, String ^str) {
IntPtr ansiStr = System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(str);
outStr = (const char*)ansiStr.ToPointer();
System::Runtime::InteropServices::Marshal::FreeHGlobal(ansiStr);
}
Conversions to and from Numbers
Many programming languages treat atomic types (like int) a little bit like objects and allows calling methods on
them. In .NET there are even class for it. Basic types like int or double are aliases to System::Int32 and
System::Float. Native C++ doesn’t have such feature, but as C++/CLI is a .NET language, it does. C++/CLI has no
separate types for native and managed bools, ints, floats etc. It means you can call methods on atomic types in
C++/CLI, like very useful ToString:
int i = 1016;
String ^i_str = i.ToString(); // Method call on atomic type!
String ^message = L"The number is: " + i_str;
Console::WriteLine(message); // The number is: 1016
Conversion from string to number is done by static method Parse or TryParse, existing in all numeric types like int,
short, float, double etc. Difference between them is that TryParse returns false in case of parsing error, while Parse
throws an exception is such case. Example:
String ^i_str = gcnew String(L"1016");
int i = int::Parse(i_str); // Static method call on atomic type!
Console::WriteLine(L"Parsed number is: {0}", i);
// Parsed number is: 1016
17
Building Strings
Strings in .NET are immutable. It means that once created, a string cannot be changed. Other than native
std::string, there are no methods in System::String for inserting or removing characters. Even single characters
accessed with [] operator are read-only. To change a managed string, you have to create a new one and assign it to
your String^ variable. That’s why ToUpper method returns a new string, it cannot make characters uppercase in
place.
So, how do we construct some complex string, you may ask? There are many ways.
1. First is to concatenate parts with + operator into a final string.
unsigned year = 2011, month = 12, day = 24;
String ^date = year.ToString() + L"-" + month.ToString() + L"-" + day.ToString();
// date == L"2011-12-24"
2. It can be done more conveniently (and also more efficiently I believe) with String::Format static method, taking
formatting string and variable number of arguments of any type. The formatting string is in special format used by
.NET, with places for data referred by index in curly braces, like {0}. It’s completely different from formatting strings
in printf from C standard library, which use % sign.
unsigned year = 2011, month = 12, day = 24;
String ^date = String::Format(L"{0}-{1}-{2}", year, month, day);
// date == L"2011-12-24"
3. Because strings in .NET are immutable, there is a separate class for building and modifying strings called
StringBuilder. Object of this type holds a mutable buffer of characters and the class defines a lot of methods to
manipulate it. You can append and insert data of different types (automatically converted to string), as well as
remove parts of the string. There is also AppendFormat method that takes formatting string and variable number of
arguments, like String::Format method. Finally you call ToString and get a normal String object.
unsigned year = 2011, month = 12, day = 24;
System::Text::StringBuilder sb;
sb.Append(year);
sb.Append(L'-');
sb.Append(month);
sb.Append(L'-');
sb.Append(day);
String ^date = sb.ToString();
// date == L"2011-12-24"
Value Formatting
Sometimes default way of converting number to string is not enough. Then we can use special format strings. For
example, "D2" means that a number should be shown as decimal and have at least two digits, padded with zeros if
necessary. This is needed when formatting days and months of dates. Such format string can be passed as optional
parameter to ToString method or appended in a bigger format string after a colon, e.g. when used in String::Format
or Console::WriteLine. For example:
unsigned year = 2011, month = 1, day = 3;
String ^day_bad = day.ToString();
// "3"
String ^day_good = day.ToString(L"D2"); // "03"
String ^date_bad = String::Format(L"{0}-{1}-{2}",
year, month, day); // "2011-1-3"
String ^date_good = String::Format(L"{0:D4}-{1:D2}-{2:D2}", year, month, day); // "2011-01-03"
Other format string is hexadecimal form. If you want to show the value of a pointer or other 32-bit identifier, you
would probably want to see it as 8-digit hexadecimal number with upper letters. This can be achieved with:
18
int variable;
int *pointer = &variable;
unsigned pointer_val = (unsigned)pointer;
String ^pointer_str = pointer_val.ToString(L"X8"); // "0012F380" on my machine
One more format string I’d like to show is precision of floating point numbers. It can be specified using format string
like "F2", where 2 is a number of digits after decimal mark. For example:
double pi = Math::PI;
Console::WriteLine(pi.ToString());
// 3,14159265358979
Console::WriteLine(pi.ToString(L"F2")); // 3,14
Decimal Mark Issues
As you see in the last example, my system converted floating-point number to string using comma as decimal mark.
If you expected period instead, you have to know this depends on the current system locale. English-speaking
countries use period as decimal mark (and comma to separate thousands), while we in Poland, like in other
European countries, use comma as decimal mark. That’s a real issue in programming – believe me, I’ve seen
commercial software that didn’t work because it was expecting period while operating system provided numbers
with comma.
.NET framework applies current locale to all conversions between strings and numbers. That’s not different than
what other API-s do. Event functions from standard C library like gcvt or printf produce comma as decimal mark, if
setlocale(LC_ALL, "polish"); is called before. But .NET automatically selects locale for you so if you want to
overcome this problem, you have to actively request your conversion to be “culture invariant”, thus default to
period as decimal mark. To parse string as floating-point number using period as decimal mark:
String ^pi_str = L"3.14";
double my_pi = double::Parse(
pi_str,
System::Globalization::NumberStyles::Float,
System::Globalization::CultureInfo::InvariantCulture);
// my_pi == 3.14
To convert floating-point number to string using invariant culture, pass the appropriate static culture object to the
ToString method:
double pi = Math::PI;
Console::WriteLine(pi.ToString(
System::Globalization::CultureInfo::InvariantCulture)); // 3.14159265358979
Locale can also be set globally for the current thread like in the code below. Pay attention however that locale
currently set for .NET libraries does not affect the native standard C/C++ library and vice versa. They can be different.
System::Threading::Thread::CurrentThread->CurrentCulture =
System::Globalization::CultureInfo::InvariantCulture;
Enumerations
Just like with classes, C++/CLI also differentiates native and managed enums. Their syntax is very different because
former follow rules of native C++, while latter follow rules that apply to enums in C# and the whole .NET. Native
enums are defined using enum keyword, while managed enums start with enum class keyword. For example:
enum NativeMood {
NativeHappy, NativeSad
};
enum class ManagedMood {
19
ManagedHappy, ManagedSad
};
These two enums don’t look very different right now, but as you will see, they have to be used differently. Of course
we can use both types to define variables and store them by value. All in all enum is just a number and the rest is the
matter of language syntax. But to use one of the values defined inside, native enum must be qualified with enum
name followed by :: and then value name, while native enum should not be qualified as enums in C++ do not form
scope, values from inside pollute global namespace. Let’s see the example:
// Native enum should be unqualified.
NativeMood native_mood;
// OK
native_mood = NativeHappy;
// warning C4482: nonstandard extension used: enum 'NativeMood' used in qualified name
native_mood = NativeMood::NativeHappy;
// Managed enum should be qualified.
ManagedMood managed_mood;
// error C2065: 'ManagedSad' : undeclared identifier
managed_mood = ManagedSad;
// OK
managed_mood = ManagedMood::ManagedSad;
Another difference is that value from native enum can be used as integer number because these types can be
implicitly converted. With managed enum it’s not the case – compiler prints error about types incompatibility and
explicit type cast must be used.
unsigned u;
// OK
u = native_mood;
// error C2440: '=' : cannot convert from 'ManagedMood' to 'unsigned int'
u = managed_mood;
// OK
u = (unsigned)managed_mood;
Type Casts
Casting between data types in native C++ can be done using old C notation (Type)Value or new C++ operators:
static_cast<Type>(Value), reinterpret_cast<Type>(Value), const_cast<Type>(Value), dynamic_cast<Type>(Value).
This is all available in C++/CLI too and works as you may expect. But C++/CLI also defines a new casting operator –
safe_cast<Type>(Value). It works very intuitively, can be applied to convert between many different kinds of types
and it performs runtime checks so it guarantees safety. As we have lots of types available in C++/CLI language –
atomic types, native classes, managed classes etc. – I won’t cover all possible cases here, but here are some more
interesting examples involving managed types:
ref class BaseClass { };
ref class DerivedClass1 : public BaseClass { };
ref class DerivedClass2 : public BaseClass { };
int main(array<System::String ^> ^args)
{
DerivedClass1 ^derived_object_1 = gcnew DerivedClass1();
// Upcast does not require explicit cast.
BaseClass ^base_object = derived_object_1;
// base_object is here really of type DerivedClass1, not DerivedClass2, so...
DerivedClass2 ^derived_object_2;
20
// Invalid downcast with safe_cast throws InvalidCastException with message:
// "Unable to cast object of type 'DerivedClass1' to type 'DerivedClass2'."
derived_object_2 = safe_cast<DerivedClass2^>(base_object);
// Invalid downcast with dynamic_cast returns nullptr.
derived_object_2 = dynamic_cast<DerivedClass2^>(base_object);
Boxing
Additional subject connected with type casts is boxing. Boxing is a feature of .NET platform where a value of some
type that should be stored by value, not by managed reference (atomic types like int or C# structs, defined in
C++/CLI as value class or value struct) is saved into managed heap. It’s especially useful if we want to keep such
value or collection of such values in a place where Object^ type is expected. Object is a base class for all other types
in .NET. Example place where any user-defined object can be stored is Tag property of every Windows Forms
control. So for example if you want to associate integer number with a button on your form, you can use boxing and
unboxing in C++/CLI like this:
int number = 123;
// Boxing. Cast is not required.
button1->Tag = number;
...
// Unboxing. Use safe_cast operator.
int recovered_number = safe_cast<int>(button1->Tag);
// recovered_number == 123
Properties
Property is a class member that can be used like a field (member variable) – assigned and retrieved, while inside it
executes some specific code to write or read its value, like there were getter and setter methods. It is another
programming language feature that most modern, high-level programming languages (including .NET) have while
native C++ is lacking. Obviously it’s not necessary, although convenient.
To be compatible with .NET, C++/CLI extends the C++ syntax to support properties in classes. They are defined using
property keyword. A property must have methods inside called get and set that use appropriate types (unless you
want to create a write-only or read-only property, which is also correct).
ref class ManagedMonster {
public:
ManagedMonster(int HP) : m_HP(HP) { }
property int HP {
int get() { return m_HP; }
void set(int val) { m_HP = val; }
}
private:
int m_HP;
};
This property just writes and reads a private field, but any code can be execute in its getter and setter. For example,
setter can trigger some additional actions connected with changing the parameter, while getter can calculate final
value instead of reading it directly from a field. Regardless of this however, usage of a property looks like direct
access to a class field:
ManagedMonster ^monster = gcnew ManagedMonster(100);
// HP set is called.
monster->HP = 200;
// HP get is called. Prints 200.
Console::WriteLine(monster->HP);
21
Such properties can be defined only in managed classes, not inside natives ones.
Exceptions
Both native C++ and .NET support error handling via the concept of exceptions. Of course they differ in details. In
practice of native C++ programming exceptions are often not used at all and when they are, there is no widespread
standard of what to throw. Language allows throwing and catching values of any type, like ints, bools, objects of
user-defined classes or pointers. .NET platform, on the other hand, establishes a standard that only objects of class
Exception or derived classes can be thrown as exceptions.
Good news is that both native and managed exceptions can be handled in C++/CLI using same syntax and freely
mixed, even inside single try…catch block. Let’s see an example.
using namespace System;
#include <exception> // for native std::exception
void NativeErroneousFunction() {
throw std::exception("A native error!");
}
void ManagedErroneousFunction() {
throw gcnew System::Exception(L"A managed error!");
}
int main(array<System::String ^> ^args) {
try {
//NativeErroneousFunction();
//ManagedErroneousFunction();
}
catch (const std::exception &ex) {
Console::WriteLine(L"Native error! " + gcnew String(ex.what()));
}
catch (System::Exception ^ex) {
Console::WriteLine(L"Managed error! " + ex->Message);
}
finally {
Console::WriteLine(L"Finalization.");
}
return 0;
}
Two types of exceptions are used here. NativeErroneousFunction throws a native exception of type std::exception
– a class defined in standard C++ library, in <exception> header, intended to be used or subclassed to handle errors
in native C++ (who uses it? :) Object of this class keeps pointer to a externally-owned null-terminated string with
error message, which can be retrieved using what method. We throw this exception by value. Second type of
exception is an object of managed class System::Exception, allocated on the heap with gcnew operator. It holds
managed string with error message, accessed through Message property. By uncommenting call to first or second
“erroneous” function, you can test throwing first or second kind of exception.
Exceptions are catched in try…catch block, which has common syntax for native as well as managed code. As you can
see, both types of exceptions are caught here – native one by a reference to const object and managed one by a
managed pointer ^.
22
There is also finally section – another very useful feature that most modern programming languages have, while
native C++ does not. I assume you already know how it works. As it is also used in .NET platform, Microsoft added
support for finally to C++/CLI so we can use it in try section, no matter if we intend to handle native or managed
exceptions. A try can have only finally section, only catch sections or both.
There is one more thing you have to know about the order of handling different types of exceptions in catch blocks.
As you know from native C++, all types of exceptions can be caught using catch (...). In .NET, on the other hand, all
exceptions are subclasses of System::Exception class, so catch (Exception^) will handle all possible cases. Question
is how these general classes relate to each other? Surprisingly all native exceptions, no matter what type they have,
are also handled by catch (Exception^), because a native exception is implicitly converted to an object of type
System::Runtime::InteropServices::SEHException with message “External component has thrown an exception.”.
That’s why the catch section that handle System::Exception type, if exists, is absolutely most general so it must be
the last catch section in the block, even relative to any native exception types – like in the example above.
Arrays
Array is undoubtedly the simplest and most fundamental type of container. Both C++ and .NET support arrays, so we
also have them in C++/CLI, with clear distinction between native and managed ones – just like it was for classes.
Native Arrays
Native arrays look like in standard C++. You can define and use a static array like this:
int native_array_static[10];
for (size_t i = 0; i < _countof(native_array_static); ++i)
native_array_static[i] = i;
// native_array_static: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Or you can dynamically allocate arrays on the native heap with operator new[]. Of course you must not forget to free
them with operator delete[] when no longer needed.
size_t count = 10;
int *native_array_dynamic = new int[count];
for (size_t i = 0; i < count; ++i)
native_array_dynamic[i] = i;
// native_array_dynamic: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
delete [] native_array_dynamic;
Exceeding range of a native array causes undefined behavior, so you can end up with program crash with access
violation or an error posted by some runtime check telling about the corruption of the heap or stack (in best case),
reading some random data or overwriting some other variables.
Managed Arrays
Array in .NET is a built-in type that must be allocated on the managed heap, has some properties and methods,
including Length that returns the number of elements. In C# we use [] to define arrays, but in C++/CLI the syntax for
managed arrays is very different. It uses array keyword that behaves like a class template. Let’s look at the example:
array<int> ^managed_array = gcnew array<int>(10);
for (int i = 0; i < managed_array->Length; ++i)
managed_array[i] = i;
// managed_array: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Of course managed array don’t need to be freed manually as garbage collector will release its memory when the
array is no longer referenced from any place in your code.
23
Exceeding range of a managed array throws an exception of type System::IndexOutOfRangeException.
foreach loop – a construct present in many programming language but not in C++ - is also available in C++/CLI in
form of a for each keyword (it is so called whitespace keyword, Microsoft patented it!). With it you can iterate
through many different kinds of containers, including managed arrays. For example:
for each (int i in managed_array)
Console::Write(L"{0}, ", i);
// Output: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Another nice language feature is array initialization during construction. C++ supports this for static arrays only, not
for dynamically allocated ones. Fortunately we can do it in C++/CLI during construction of managed arrays. Here is a
bunch of examples:
// OK
int native_array_static_1[10] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
};
// Also OK
int native_array_static_2[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
};
// ERROR! There is no way to do it :(
int *native_array_dynamic = new int[10] {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
};
// OK
array<int>
0, 1, 2,
};
// Also OK
array<int>
0, 1, 2,
};
^managed_array_1 = gcnew array<int>(10) {
3, 4, 5, 6, 7, 8, 9,
^managed_array_2 = gcnew array<int> {
3, 4, 5, 6, 7, 8, 9,
Containers
Every high-level programming language, to allow coding complex programs, needs to provide some containers that
hold a collection of objects. C++ standard library – STL – has some template classes for that, like std::vector,
std::list, std::set, std::map, std::stack etc. Microsoft added in Visual C++ some additional containers that are
very useful but not defined by STL standard, like stdext::hash_map. You can use all of them in C++/CLI to hold native
objects, like this:
#include <map>
#include <string>
#include <iostream>
...
typedef std::map<std::string, NativeMonster*> MonsterMap;
MonsterMap native_map;
native_map.insert(MonsterMap::value_type("Goblin", new NativeMonster(10)));
native_map.insert(MonsterMap::value_type("Boss", new NativeMonster(1000)));
for (MonsterMap::iterator it = native_map.begin(); it != native_map.end(); ++it) {
24
std::cout << "Monster: " << it->first << std::endl;
it->second->TellHitPoints();
}
for (MonsterMap::iterator it = native_map.begin(); it != native_map.end(); ++it)
delete it->second;
This program prints following output:
NativeMonster Constructor
NativeMonster Constructor
Monster: Boss
NativeMonster has 1000 HP
Monster: Goblin
NativeMonster has 10 HP
NativeMonster Destructor
NativeMonster Destructor
.NET platform has many container classes in its standard library. Old ones, which hold objects of type Object^ so they
need casting or boxing your real data type – are in System::Collecions namespace. New ones – generic classes
parameterized with your real data type – are in System::Collections::Generic namespace. C++/CLI supports
defining as well as using generics – a .NET equivalent of C++ templates – but I won’t describe it in this tutorial.
Instead, let’s just see how we can use a managed container to hold object of a managed class.
typedef System::Collections::Generic::Dictionary<String^, ManagedMonster^> DictionaryType;
typedef System::Collections::Generic::KeyValuePair<String^, ManagedMonster^> KeyValuePairType;
DictionaryType ^managed_dictionary = gcnew DictionaryType();
managed_dictionary->Add(L"Goblin", gcnew ManagedMonster(10));
managed_dictionary->Add(L"Boss", gcnew ManagedMonster(1000));
for each (KeyValuePairType it in managed_dictionary) {
Console::WriteLine(L"Monster: {0}", it.Key);
it.Value->TellHitPoints();
}
This program prints following output:
ManagedMonster Constructor
ManagedMonster Constructor
Monster: Goblin
ManagedMonster has 10 HP
Monster: Boss
ManagedMonster has 1000 HP
Locking
Many things exist in .NET platform to support parallel (multithreaded) programming. Probably the simplest way of
synchronizing access to an object so it is mutually exclusive between threads is locking. You can just lock any object
forming a critical section around some code, so then you can be sure that no more than one thread executes this
code at time. C# has a language construct for that: lock (object) { code }. In C++/CLI you can achieve the same by
including <msclr\lock.h> header and using an msclr::lock class. It is RAII – it creates lock in constructor and frees it
in destructor, so it is best used as a local object inside some scope, created on the stack – like this:
#include <msclr\lock.h>
...
Object ^m_SyncObj;
...
25
{ // A scope for critical section
msclr::lock(m_SyncObj);
... // Critical section code
}
Using Libraries
One of the biggest strengths of C++/CLI is that you can not only freely mix managed and native code in your project,
but also directly use native C and C++ libraries next to .NET libraries. Way of referencing them is of course different,
natural to the particular type.
Using Native Libraries
To prepare for using a native C or C++ library, you have to follow same steps as if you coded a native C++ program.
First, open the Property Manager panel. Then select Microsoft.Cpp.Win32.user property sheet, right-click on it and
select Properties. In the Property Pages dialog window select VC++ Directories item in the tree on the left. Then
from the list on the right edit Include Directories and then Library Directories item, adding appropriate paths to
h and lib files of the library, respectively.
To use such library in your project, you also have to fulfill all that is required to use a library in native C++. First,
include necessary header in your cpp file, like this:
#include <fmod.h>
This allows you to compile a code that uses the library without errors. But to be also able to link your program
successfully and build the final exe file, without “unresolved external symbol” linker errors like this:
error LNK2019: unresolved external symbol "extern "C" enum FMOD_RESULT __stdcall
FMOD_System_Create(struct FMOD_SYSTEM * *)"
(?FMOD_System_Create@@$$J14YG?AW4FMOD_RESULT@@PAPAUFMOD_SYSTEM@@@Z) referenced in function "int __clrcall
main(cli::array<class System::String ^ >^)" (?main@@$$HYMHP$01AP$AAVString@System@@@Z)
You also have to tell the linker to link with a lib file corresponding to the header used. You can do it in two ways.
First is to open project properties, navigate to Configuration Properties / Linker / Input and add names of
26
required lib files to the Additional Dependencies item. Just don’t forget to do it for both Debug and Release
configurations!
Alternative way is to put a special, Microsoft-specific directive anywhere in your project code:
#pragma comment(lib, "fmodex_vc.lib")
Using Managed Libraries
To reference external managed library – a one written in C# or any other .NET language – also open project
properties, but then select Common Properties / Framework and References in the tree on the left. Then click Add
New Reference button, in the new Add Reference dialog window select Browse tab, navigate to the directory where
the library is located and select appropriate dll file.
27
Now you can start use classes from that library. Different than in native C++, there is no need for additional including
or linking.
This reference list in project properties is also useful for other purposes. From there you can click Add New Reference
button again, but in the Add Reference window go to the .NET tab this time. On the list that appears you can select
and add to your project references additional components of .NET library that are not referenced by default, like
System.Windows.Forms.DataVisualization, which contains a very powerful control for displaying charts, introduced
in .NET version 4.
Same way you can establish dependencies between projects in your Visual C++ solution. You just need to switch to
the Projects tab and select some project that the project you currently edit should depend on.
Native Types in Exported Functions
There is one issue with coding libraries in C++/CLI that appears when you export some functions that use external,
native types. I will explain in step-by-step using an example. Imagine you want to code a separate library that
implements a logger to be able to print different types of messages on the system console. The library is written in
C++/CLI and its header file looks like this:
#pragma once
#include <exception>
#include <string>
using namespace System;
28
namespace Library {
public ref class Logger {
public:
void PrintManagedString(String ^s) {
Console::WriteLine(s);
}
void PrintNativeString(const char *s) {
Console::WriteLine(gcnew String(s));
}
void PrintNativeException(const std::exception &ex) {
Console::WriteLine(L"Error: " + gcnew String(ex.what()));
}
void PrintStlString(const std::string &s) {
Console::WriteLine(gcnew String(s.c_str()));
}
};
}
As you can see there are four printing methods. Should you be able to reference such library in another C++/CLI
project, instantiate Library::Logger class and use these methods without any problems? It turns out that it is not
the case. A code that uses first two methods compile successfully and work as you could expect, because
PrintManagedString uses managed type (System::String) as parameter and PrintNativeString uses atomic type
(char).
But when you try to use third method, a compilation error appears:
error C3767: 'Library::Logger::PrintNativeException': candidate function(s) not accessible
It means that the native type used as parameter for the PrintNativeException method (std::exception) is
considered “private” in .NET world, which makes this method inaccessible from outside of your library. Luckily this
can be fixed with a special pragma directive that makes specified type public (std::exception in our case), which
also makes methods that use this type as parameter or return value type accessible for users of your library. All you
need to do is to put following line somewhere in your header file:
#pragma make_public(std::exception)
But that’s not all I can say in this topic. I have a bad news regarding fourth method from the Logger class we discuss
here. You can’t do the same #pragma make_public trick with std::string type. If you try it, compiler will tell you that
you cannot do that with template types, while std::string is really a template – a typedef to
std::basic_string<char>. There is no workaround for this unfortunately. You have to cast such objects to void* or
something…
error C2158: 'std::basic_string<_Elem,_Traits,_Ax>' : #pragma make_public directive is currently
supported for native non-template types only
Summary
That’s all I wanted to tell you about C++/CLI programming language. I hope you enjoyed the tutorial, understood
most of it and learned something that you can now use in your personal or professional projects. As you can
hopefully see now, the exotic C++/CLI language is not so weird, unpleasant or useless as some people think. It is
another language of .NET platform that has unique feature – with it you can freely mix native and managed code,
which makes it a perfect choice for some types of programming projects. If you knew C++ and some .NET language
like C# before, you only need to learn some syntax rules and your are ready to code in C++/CLI.
29
Obviously this document is not exhaustive. I tried to explain basics of this language based on my experience. I don’t
know everything about it and I didn’t even written everything I know. For example, I didn’t mention about the
<msclr/marshal.h> header containing very useful utilities. I recommend you now, instead of Googling for another
tutorial, look into official documentation for any further details. Select Help / Manage Help Settings in your Visual
C++, click Install Content from Online, select and download some of the packages. This way you will have full .NET
documentation offline, working fast and accessible whenever you need it. Just click Help / View Help or press
Ctrl+F1. I recommend you keep it open all the time as you code and use Intex tab for navigation.
30
Download