Introduction
This article is not an attempted glamorization of the C++/CLI language
semantics and there will be no stabs at other languages like C# or VB.NET,
rather, this is just an unofficial endeavor, by a non-Microsoft-employee who
loves the language, to substantiate the fact that C++/CLI has its own unique
role to play as a first-class .NET programming language. A question that's being
increasingly asked on newsgroups and technical forums, is why anyone should use
C++ to code .NET applications when languages like C# and VB.NET are more suited
for the purpose, considering that those languages can only be used to write CLI
applications. Usually such posts are also followed by comments stating how the
C++ syntax is complicated and convoluted, how C++ is now an obsolete language
and how the VS.NET designers do not support C++ as well as they do support C#
and VB.NET. Some of these suspicions are totally fallacious while some are
partly correct (specially the ones that talk about lack of
designer/intellisense/clickonce support), but nearly all of these reservations
are cast without making an honest attempt to judge C++/CLI's objectives as a CLI
language. Hopefully, this article should serve to clear all the confusion,
mystery and distrust surrounding the C++/CLI language specification and its role
in the VS.NET language hierarchy. And remember, the author does not work for nor
is paid by Microsoft, so technically speaking, any bias detected would be a mere
figment of your hyper-active imagination ;-)
Fastest and simplest native-interop
C++ offers a unique interop mechanism, rather unimaginatively called C++
interop, in addition to the P/Invoke mechanism available in other languages like
C# or VB.NET. C++ interop is far more intuitive than P/Invoke, since you simply
#include
the required headers, link with the required libs
and call any function just as you would in native C++. It's also a *lot* faster
than P/Invoke - which is very easily verifiable. Now, it's arguable that in
real-life applications, the performance benefits obtained via C++ interop may be
negligible compared to the time taken for user-GUI-interactions, database
access, network data transfer, complex arithmetic algorithms etc., but the fact
remains that scenarios, where even a few nano-seconds gained per interop-call
can make a huge impact on the overall performance/responsiveness of an
application, cannot be totally ruled out. I have two code snippets below (one
written in C# using P/Invoke and the other written in C++ using C++ Interop) and
I also include the times taken (in ms) for various iteration counts. How you
interpret those times and what impact you think it may have on your application
is left to you. I merely intend to point out that, factually, C++ code executes
faster than C# code where heavy use of native-interop calls is involved.
The C# program (uses P/Invoke)
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll")]
static extern uint GetTickCount();
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint GetWindowsDirectory(
[Out] StringBuilder lpBuffer, uint uSize);
static void Test(int x)
{
StringBuilder sb = new StringBuilder(512);
for (int i = 0; i < x; i++)
GetWindowsDirectory(sb, 511);
}
static void DoTest(int x)
{
uint init = GetTickCount();
Test(x);
uint tot = GetTickCount() - init;
Console.WriteLine(
"Took {0} milli-seconds for {1} iterations",
tot, x);
}
static void Main(string[] args)
{
DoTest(50000);
DoTest(500000);
DoTest(1000000);
DoTest(5000000);
Console.ReadKey(true);
}
The C++ program (uses C++ Interop)
void Test(int x)
{
TCHAR buff[512];
for(int i=0; i<x; i++)
GetWindowsDirectory(buff, 511);
}
void DoTest(int x)
{
DWORD init = GetTickCount();
Test(x);
DWORD tot = GetTickCount() - init;
Console::WriteLine(
"Took {0} milli-seconds for {1} iterations",
tot, x);
}
int main(array<System::String ^> ^args)
{
DoTest(50000);
DoTest(500000);
DoTest(1000000);
DoTest(5000000);
Console::ReadKey(true);
return 0;
}
Speed comparisons
Iterations |
C# app |
C++ app |
50,000 |
61 |
10 |
500,000 |
600 |
70 |
1,000,000 |
1162 |
140 |
5,000,000 |
6369 |
721 |
The performance difference is truly staggering! So, here's one really good
reason why you would want to use C++/CLI if you are doing native interop on a
serious level - performance! With all due respect to the .NET Framework's Base
Class Library, I've been forced to resort to native-interop for any non-trivial
non-web-based .NET application I've worked on. Of course, why I'd want to use
.NET for an application that requires so much native-interop is a totally
different question and the answers/causes for it may not be within my control
(nor your control).
If you are still skeptical about the performance benefits, there's another
very good reason why you'd want to use C++/CLI over C# or VB.NET - source code
bloat! As an example, I show below a C++ function that uses the IP helper API to
enumerate network adapters on a machine and list out the IP addresses associated
with each adapter.
C++ code enumerating n/w adapters
void ShowAdapInfo()
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
ULONG OutBufLen = 0;
if( GetAdaptersInfo(NULL, &OutBufLen) == ERROR_BUFFER_OVERFLOW )
{
int divisor = sizeof IP_ADAPTER_INFO;
#if _MSC_VER >= 1400
if( sizeof time_t == 8 )
divisor -= 8;
#endif
pAdapterInfo = new IP_ADAPTER_INFO[OutBufLen/divisor];
if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS )
{
}
else
{
int index = 0;
while(pAdapterInfo)
{
Console::WriteLine(gcnew String(pAdapterInfo->Description));
Console::WriteLine("IP Address list : ");
PIP_ADDR_STRING pIpStr = &pAdapterInfo->IpAddressList;
while(pIpStr)
{
Console::WriteLine(gcnew String(pIpStr->IpAddress.String));
pIpStr = pIpStr->Next;
}
pAdapterInfo = pAdapterInfo->Next;
Console::WriteLine();
}
}
delete[] pAdapterInfo;
}
}
Now let's see a C# version that uses P/Invoke - and I am not exaggerating in
the slightest when I tell you that it took me nearly half an hour and a dozen
visits to www.pinvoke.net
before I copy/pasted all the required declarations!
The C# port using P/Invoke
const int MAX_ADAPTER_NAME_LENGTH = 256;
const int MAX_ADAPTER_DESCRIPTION_LENGTH = 128;
const int MAX_ADAPTER_ADDRESS_LENGTH = 8;
const int ERROR_BUFFER_OVERFLOW = 111;
const int ERROR_SUCCESS = 0;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct IP_ADDRESS_STRING
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
public string Address;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct IP_ADDR_STRING
{
public IntPtr Next;
public IP_ADDRESS_STRING IpAddress;
public IP_ADDRESS_STRING Mask;
public Int32 Context;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct IP_ADAPTER_INFO
{
public IntPtr Next;
public Int32 ComboIndex;
[MarshalAs(UnmanagedType.ByValTStr,
SizeConst = MAX_ADAPTER_NAME_LENGTH + 4)]
public string AdapterName;
[MarshalAs(UnmanagedType.ByValTStr,
SizeConst = MAX_ADAPTER_DESCRIPTION_LENGTH + 4)]
public string AdapterDescription;
public UInt32 AddressLength;
[MarshalAs(UnmanagedType.ByValArray,
SizeConst = MAX_ADAPTER_ADDRESS_LENGTH)]
public byte[] Address;
public Int32 Index;
public UInt32 Type;
public UInt32 DhcpEnabled;
public IntPtr CurrentIpAddress;
public IP_ADDR_STRING IpAddressList;
public IP_ADDR_STRING GatewayList;
public IP_ADDR_STRING DhcpServer;
public bool HaveWins;
public IP_ADDR_STRING PrimaryWinsServer;
public IP_ADDR_STRING SecondaryWinsServer;
public Int32 LeaseObtained;
public Int32 LeaseExpires;
}
[DllImport("iphlpapi.dll", CharSet = CharSet.Ansi)]
public static extern int GetAdaptersInfo(
IntPtr pAdapterInfo, ref int pBufOutLen);
static void ShowAdapInfo()
{
int OutBufLen = 0;
if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) ==
ERROR_BUFFER_OVERFLOW )
{
IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen);
if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS )
{
}
else
{
while(pAdapterInfo != IntPtr.Zero)
{
IP_ADAPTER_INFO adapinfo =
(IP_ADAPTER_INFO)Marshal.PtrToStructure(
pAdapterInfo, typeof(IP_ADAPTER_INFO));
Console.WriteLine(adapinfo.AdapterDescription);
Console.WriteLine("IP Address list : ");
IP_ADDR_STRING pIpStr = adapinfo.IpAddressList;
while (true)
{
Console.WriteLine(pIpStr.IpAddress.Address);
IntPtr pNext = pIpStr.Next;
if (pNext == IntPtr.Zero)
break;
pIpStr = (IP_ADDR_STRING)Marshal.PtrToStructure(
pNext, typeof(IP_ADDR_STRING));
}
pAdapterInfo = adapinfo.Next;
Console.WriteLine();
}
}
Marshal.FreeHGlobal(pAdapterInfo);
}
}
Good heavens! If anyone tells me that copy/pasting half a dozen constant
declarations, three structures and an API method in addition to resorting to the
Marshal
class's AllocHGlobal
, FreeHGlobal
and PtrToStructure
functions is not a bother, I flatly refuse to
believe that you are telling the truth.
Stack semantics and deterministic destruction
C++ gives us deterministic destruction via simulated stack semantics and if
you haven't read about it yet, you might want to take a look at my article on
the topic :-
Deterministic Destruction in C++/CLI. Simply put, stack semantics is
syntactic sugar for the Dispose-pattern. But it's a far more intuitive
semantically than the C# using
block syntax. Take a look at the
following C# and C++ code snippets (both do the same thing - concatenate the
contents of two files and write them to a third file).
C# code - using block semantics
public static void ConcatFilestoFile(
String file1, String file2, String outfile)
{
String str;
try
{
using (StreamReader tr1 = new StreamReader(file1))
{
using (StreamReader tr2 = new StreamReader(file2))
{
using (StreamWriter sw = new StreamWriter(outfile))
{
while ((str = tr1.ReadLine()) != null)
sw.WriteLine(str);
while ((str = tr2.ReadLine()) != null)
sw.WriteLine(str);
}
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
C++ code - stack semantics
static void ConcatFilestoFile(
String^ file1, String^ file2, String^ outfile)
{
String^ str;
try
{
StreamReader tr1(file1);
StreamReader tr2(file2);
StreamWriter sw(outfile);
while(str = tr1.ReadLine())
sw.WriteLine(str);
while(str = tr2.ReadLine())
sw.WriteLine(str);
}
catch(Exception^ e)
{
Console::WriteLine(e->Message);
}
}
Not only is the C# code unwarrantedly verbose compared to equivalent C++
code, but the using
-block syntax leaves it to the programmer to explicitly
specify where he wants Dispose
to be invoked [the end of the using
block] whereas with C++/CLI's stack semantics, the compiler handles it using
regular scoping rules. In fact, this makes modifying the code a little more tedious
in C# than in C++ - as an example, lets modify the code so that the output file is created
even if only one of the input files exist. See the modified C# and C++ code
snippets below.
Modified C# code
public static void ConcatFilestoFile(
String file1, String file2, String outfile)
{
String str;
try
{
using (StreamWriter sw = new StreamWriter(outfile))
{
try
{
using (StreamReader tr1 = new StreamReader(file1))
{
while ((str = tr1.ReadLine()) != null)
sw.WriteLine(str);
}
}
catch (Exception) { }
using (StreamReader tr2 = new StreamReader(file2))
{
while ((str = tr2.ReadLine()) != null)
sw.WriteLine(str);
}
}
}
catch (Exception e){ }
}
Taking the using
block for the StreamWriter
to the
top involved realignment of the using
block braces correspondingly
- not a big deal in the above case obviously, but for non-trivial modifications,
this can be quite confusing and a source of bother and possible logical errors.
[BTW, I am aware that you don't need braces for single statement blocks, so the
above blocks can be imagined to be multi-line blocks to comprehend my point - I
wanted to minimize the sample code snippets as much as possible].
Modified C++ code
static void ConcatFilestoFile(
String^ file1, String^ file2, String^ outfile)
{
String^ str;
try
{
StreamWriter sw(outfile);
try
{
StreamReader tr1(file1);
while(str = tr1.ReadLine())
sw.WriteLine(str);
}
catch(Exception^){}
StreamReader tr2(file2);
while(str = tr2.ReadLine())
sw.WriteLine(str);
}
catch(Exception^){}
}
Woah, wasn't that a lot easier to do than what we did for C#? I just moved
the StreamWriter
declaration to the top and added an extra
try
-block - that's all. Even for trivial cases as in my sample code
snippets, if the complexity involved is so enormously reduced in C++, you can
visualize the impact of using stack semantics on your coding efficiency when you
work on larger projects.
Still not convinced? Okay, lets take a look at member
objects and their destruction. Imagine CLI GC classes R1
and
R2
both of which implement IDisposable
and have functions
called F()
, and a CLI GC class R
that has an R1
and R2
member each and a function F()
which internally
calls F()
on the R1
and R2
members. Let's
see the C# implementation first.
C# implementation of a disposable class hierarchy
class R1 : IDisposable
{
public void Dispose() { }
public void F() { }
}
class R2 : IDisposable
{
public void Dispose() { }
public void F() { }
}
class R : IDisposable
{
R1 m_r1 = new R1();
R2 m_r2 = new R2();
public void Dispose()
{
m_r1.Dispose();
m_r2.Dispose();
}
public void F()
{
m_r1.F();
m_r2.F();
}
public static void CallR()
{
using(R r = new R())
{
r.F();
}
}
}
Okay - straightaway we notice a few things - the IDisposable
interface has to be manually implemented for each disposable class, and for
class R
which has R1
and R2
members, the
Dispose
method needs to invoke Dispose
on the member
classes too. Now let's see the C++ implementation of the above set of classes.
The equivalent C++ implementation
ref class R1
{
public:
~R1(){}
void F(){}
};
ref class R2
{
public:
~R2(){}
void F(){}
};
ref class R
{
R1 m_r1;
R2 m_r2;
public:
~R(){}
void F()
{
m_r1.F();
m_r2.F();
}
static void CallR()
{
R r;
r.F();
}
};
*chortle* I can just see the expression on your faces now! No more manual
IDisposable
implementations (we just put destructors into our
classes) and the best part - class R
's destructor (the
Dispose
method) does not bother calling Dispose
on any disposable members
it may have - it doesn't have to, the compiler generates all the code for that.
Mixed types
Okay, so C++ supports native types - it's always done that; and C++ supports
CLI types - yeah, isn't that why we even have this article here; well, it also
supports mixed types - native types with CLI members and CLI types with native
members! Think of the possibilities that offers you.
Note that, as of Whidbey, the mixed types implementation is not
complete; and as far as I could make out from Brandon's, Herb's and Ronald's
postings, there's this really cool type unified model that will be realized
in Orcas - you can new /delete CLI types on the
native C++ heap and you can gcnew /delete native
types on the CLI heap. But since this is all post-Whidbey stuff, I won't
discuss the unified model in this article and this is just for your
information. |
Before I talk about where you can apply mixed types, I'd like to show you
what mixed types are. If you understand mixed types, please skip the next few
paragraphs. I am going to quote Brandon Bray verbatim here : "A mixed type is
either a native class or ref class that requires object members, either by
declaration or by inheritance, to be allocated on both the garbage collected
heap and the native heap". So if you have a managed type with a native
member or a native type with a managed member, you've got a mixed type. VC++
Whidbey does not support mixed types directly (the unified type model is a
post-Whidbey scenario), but it gives us library-provided workarounds to
implement mixed types. Let's start off with a native type that contains a
managed member.
ref class R
{
public:
void F(){}
R(){}
~R(){}
};
Imagine that this managed type R has a non-trivial constructor and a
non-trivial destructor for the sake of my example.
class Native
{
private:
gcroot<R^> m_ref;
public:
Native():
m_ref(gcnew R())
{
}
~Native()
{
delete m_ref;
}
void DoF()
{
m_ref->F();
}
};
Since, I cannot have an R
member in my class, I've used the
gcroot
template class (declared in gcroot.h, you can
#include
vcclr.h though) which wraps the
System::Runtime::InteropServices::GCHandle
structure. It's a smart
pointer like class which overloads operator->
to return the managed type used as
the template parameter. So in the above class, I can use m_ref
just
as if I had declared it as a R^
and you can see this in action in
the DoF
function. You can actually save on the manual delete
call
by using auto_gcroot
(analogous to std::auto_ptr
and
declared in msclr\auto_gcroot.h) instead of gcroot
. Here's a
slightly better implementation that uses auto_gcroot
.
class NativeEx
{
private:
msclr::auto_gcroot<R^> m_ref;
public:
NativeEx() : m_ref(gcnew R())
{
}
void DoF()
{
m_ref->F();
}
};
Let's take a look at the reverse now - a native member in a CLI class.
ref class Managed
{
private:
Native* m_nat;
public:
Managed():m_nat(new Native())
{
}
~Managed()
{
delete m_nat;
}
!Managed()
{
delete m_nat;
#ifdef _DEBUG
throw gcnew Exception("Uh oh, finalizer got called!");
#endif
}
void DoF()
{
m_nat->DoF();
}
};
I cannot have a Native
object as a ref
class
member, so I need to use a Native*
object instead. I new
the Native
object in the constructor and delete
it in both the
destructor and the finalizer (just in case). If it's a debug build, reaching the
finalizer will also throw an exception - so the developer can promptly add a
call to delete
or use stack semantics for his CLI type. Curiously,
the libraries team didn't write an anti-class for gcroot
- but it's
not a biggie to write your own.
template<typename T> ref class nativeroot
{
T* m_t;
public:
nativeroot():m_t(new T){}
nativeroot(T* t):m_t(t){}
T* operator->()
{
return m_t;
}
protected:
~nativeroot()
{
delete m_t;
}
!nativeroot()
{
delete m_t;
#ifdef _DEBUG
throw gcnew Exception("Uh oh, finalizer got called!");
#endif
}
};
It's a rather simplistic implementation of a smart-pointer like ref
class that handles allocation/de-allocation of the native object. For a
more complete implementation, you might want to take a look at Kenny Kerr's
AutoPtr
template struct
here. Anyway, using the nativeroot
template class, we can
revise our Managed
class as follows :-
ref class ManagedEx
{
private:
nativeroot<Native> m_nat;
public:
void DoF()
{
m_nat->DoF();
}
};
Okay, so what's the big deal about mixed types, you might ask! The deal is
that, now you can mix your MFC, ATL, WTL, STL based code repositories with the
.NET Framework in the most straightforward manner possible - just write your
mixed-mode code and compile! It's one thing having an MFC class in a DLL and a
.NET application calling into this DLL, it's quite another being able to add
.NET class members to your MFC class and vice-versa. [I've
co-authored a book with Tom Archer on mixing MFC with .NET - though we targeted
the earlier Managed C++ extensions -
Extending MFC Applications with the .NET Framework, so kindly humor me by
pretending to acknowledge my claims that I know how useful this can be].
As an example, imagine that you have an MFC dialog that accepts data from the
user through a multi-line edit box - now, you have a new requirement to show a
read-only edit box that'll show a running md5 hash of the text in the multi-line
edit box. Your team mates are bemoaning how they'd have to spend hours delving
into the crypto API and your manager is worried that you may have to buy a 3rd
party encryption library; that's when you impress them all by announcing in your
Anakin voice that you'll do the task in under 15 minutes. Here's how :-
Add the new edit box to your dialog resource and add corresponding DDX
variables. Enable the /clr compilation mode and add the following lines to your dialog's
header file :-
#include <msclr\auto_gcroot.h>
using namespace System::Security::Cryptography;
Now use the auto_gcroot
template to declare an
MD5CryptoServiceProvider
member :-
protected:
msclr::auto_gcroot<MD5CryptoServiceProvider^> md5;
In the OnInitDialog
handler, gcnew
the
MD5CryptoServiceProvider
member.
md5 = gcnew MD5CryptoServiceProvider();
And add an EN_CHANGE
handler for the multi-line edit box :-
void CXxxxxxDlg::OnEnChangeEdit1()
{
using namespace System;
CString str;
m_mesgedit.GetWindowText(str);
array<Byte>^ data = gcnew array<Byte>(str.GetLength());
for(int i=0; i<str.GetLength(); i++)
data[i] = static_cast<Byte>(str[i]);
array<Byte>^ hash = md5->ComputeHash(data);
CString strhash;
for each(Byte b in hash)
{
str.Format(_T("%2X "),b);
strhash += str;
}
m_md5edit.SetWindowText(strhash);
}
Mixed type - that's what we have there - a CDialog
derived class
(which is native) containing an MD5CryptoServiceProvider
member (CLI
type). The reverse can be done just as effortlessly (as demonstrated in the
earlier code snippets) - you may have a Windows Forms application and might want
to take advantage of a native class library - no problem, use the
nativeroot
template defined above.
Managed templates
If you've been to at least one tech-session on .NET 2.0 (or C# 2.0), you must
have been peppered strongly with the concept of generics, how it avoids the
evils of templates in C++, how it's the right way to do templates etc. Well,
assuming that is all correct, C++/CLI supports generics just as well as any
other CLI language - but what it does that no other CLI language does is that it
also supports managed templates - means templated ref
and
value
classes. If you've never used templates before, you may not
appreciate this all that much, but if you come from a template background and
you've found that generics, for all its supposed OOPishness, limits the way you
code, managed templates should be a huge relief for you. You can use generics
and templates together - in fact it's possible to instantiate a generic type
with a managed type's template parameter (though the reverse is not possible due
to the run-time instantiation used by generics). STL.NET (or STL/CLR) discussed
later, makes very good use of mixing generics with managed templates.
The sub-type constraint mechanism used by generics prevents you from doing
stuff like this :-
generic<typename T> T Add(T t1, T t2)
{
return t1 + t2;
}
error C2676: binary '+' : 'T' does not define this operator or a
conversion to a type acceptable to the predefined operator
Now see the corresponding template version :-
template<typename T> T Add(T t1, T t2)
{
return t1 + t2;
}
You can do this :-
int x1 = 10, x2 = 20;
int xsum = Add<int>(x1, x2);
You can also do this :-
ref class R
{
int x;
public:
R(int n):x(n){}
R^ operator+(R^ r)
{
return gcnew R(x + r->x);
}
};
R^ r1 = gcnew R(10);
R^ r2 = gcnew R(20);
R^ rsum = Add<R^>(r1, r2);
That works with a native type like int
as well as with a
ref
type (as long as the ref
type has a + operator
).
This shortcoming with generics is not a bug or a disability as such - it is by
design. Generics are instantiated at run time by any calling assembly, so the
compiler cannot know for sure that a specific operation can be performed on a
generic parameter, unless it matches a subtype constraint, so the compiler does
this resolution at the generic definition. Another handicap when you use
generics is that it won't allow you to use non-type parameters. The following
generic class definition won't compile :-
generic<typename T, int x> ref class G
{
};
error C2978: syntax error : expected 'typename' or 'class'; found type
'int'; non-type parameters are not supported in generics
Compare with a managed template :-
template<typename T, int x = 0> ref class R
{
};
If you are beginning to feel grateful that C++ provided you with both
generics and managed templates, take a look at this one :-
template<typename T> ref class R
{
public:
void F()
{
Console::WriteLine("hey");
}
};
template<> ref class R<int>
{
public:
void F()
{
Console::WriteLine("int");
}
};
Well, you cannot do that with generics. If you do try, you'll see this :-
error C2979: explicit specializations are not supported in
generics
You can mix templates and generics in inheritance chains too :-
generic<typename T> ref class Base
{
public:
void F1(T){}
};
template<typename T> ref class Derived : Base<T>
{
public:
void F2(T){}
};
Derived<int> d;
d.F1(10);
d.F2(10);
Oh, and finally, you cannot derive a
generic
class from a
generic
parameter
type.
The following code won't compile :-
generic<typename T> ref class R : T
{
};
error C3234: a generic class may not derive from a generic type
parameter
Templates let you do so (as if you didn't know that already).
ref class Base
{
public:
void F(){}
};
generic<typename T> ref class R : T
{
};
R<Base> r1;
r1.F();
So, next time you attend some local UG session, and your local C# evangelist
starts bad-mouthing templates, you know what to do :-)
STL/CLR
C++ developers who heavily used STL must have felt thoroughly handicapped
when they moved to .NET 1/1.1 and many of them probably gave up and went back to
native coding. Technically, you can use native STL with .NET types (using
gcroot
), but the resultant code would be pretty inefficient not to
mention ugly :-
std::vector< gcroot<IntPtr> >* m_vec_hglobal;
for each(gcroot<IntPtr> ptr in *m_vec_hglobal)
{
Marshal::FreeHGlobal(ptr);
}
Presumably the VC++ team took this into consideration, and post-Whidbey, they
will provide STL.NET (or STL/CLR) as a separate web-download.
Why, you may ask? Stan Lippman, gives 3 reasons in his MSDN article :-
STL.NET Primer (MSDN)
- Extensibility - The STL design separates algorithms and containers into
separate domain spaces - means you have a bunch of containers and a bunch of
algorithms, and you can use the algorithms on any of the containers and you
can use the containers with any of the algorithms. So, if you add a new
algorithm, you can use it with any of the containers and similarly, a new
container can be used with any of the existing algorithms.
- Unification - All those hardcore C++ developers out there, with their
preciously accumulated STL expertise can reuse their expertise without a
learning curve. Getting good at using STL takes time - and once you've got
there, it'd be a definite advantage to be able to use your skills in the
.NET world, wouldn't it?
- Performance - STL.NET is implemented using managed templates that
implement generic interfaces. And since its core is coded using C++ and
managed templates, it's expected to have a significant performance advantage
over the generic containers available in the BCL.
Those who've used STL before won't need any demonstration, so the below code
snippets are for the benefit of those who haven't used STL previously.
vector<String^> vecstr;
vecstr.push_back("wally");
vecstr.push_back("nish");
vecstr.push_back("smitha");
vecstr.push_back("nivi");
deque<String^> deqstr;
deqstr.push_back("wally");
deqstr.push_back("nish");
deqstr.push_back("smitha");
deqstr.push_back("nivi");
I've used two STL.NET containers, vector
and deque
and the code for populating both containers looks identical (push_back
is used for both). Now, I'll use the replace
algorithm on both
containers - again, the code is pretty much identical.
replace(vecstr.begin(), vecstr.end(),
gcnew String("nish"), gcnew String("jambo"));
replace(deqstr.begin(), deqstr.end(),
gcnew String("nish"), gcnew String("chris"));
The important thing to notice is that I've used the "same" algorithm -
replace
- on two different STL containers, using identical function
calls. That's what Stan meant when he talked about "Extensibility". I'll prove
it by writing one of the most pointless functions anyone ever coded :-
template<typename ForwardIterator> void Capitalize(
ForwardIterator first, ForwardIterator end)
{
for(ForwardIterator it = first; it < end; it++)
*it = (*it)->ToUpper();
}
It goes through a System::String^
container and
capitalizes each and every string in it - definitely not the sort of algorithm
that's going to convince the standard C++ committee into adopting it for the
next version of STL ;-)
Capitalize(vecstr.begin(), vecstr.end());
Capitalize(deqstr.begin(), deqstr.end());
for(vector<String^>::iterator it = vecstr.begin();
it < vecstr.end(); it++)
Console::WriteLine(*it);
Console::WriteLine();
for(deque<String^>::iterator it = deqstr.begin();
it < deqstr.end(); it++)
Console::WriteLine(*it);
My algorithm - however dumb it was - worked with both the vector
and the deque
containers! Okay, I am not going further on this
because if I do, the STL gurus are going to get mad at me for my silly code
snippets while the non-STLers may simply get bored. If you haven't used STL yet,
go read Stan Lippman's article and/or get a good book on STL.
Familiar syntax
Developers often fall in love with their programming language - and rarely
with functional or practical motives. Remember the revolt of the VB6ers, when
Microsoft announced that VB6 will no longer be officially supported? Non-VBers
were totally astonished by this behavior, which to them seemed to be the height
of imbecility, but core VBers were prepared to die for their language. In fact,
if VB.NET was never invented, a majority of the VBers would have stayed away
from .NET, for C# would have been utterly alien to them with its C++ ancestry.
Many VB.NETers may have moved to C#, but they wouldn't have done so directly
from VB6; VB.NET served as a channel to get them away from their
VB-mindsets. Correspondingly, had Microsoft released only VB.NET (and no C#),
.NET might have become the new OOPish VB with a bigger class library - the C++
community would have looked at it with scorn - they wouldn't even have bothered
to check out the .NET base class library. Why, any set of developers who use a
particular language should be scornful at another set of developers using a
different language, is not a question I attempt to answer here - the answer to
that would also need to answer why some people love whisky, others love coke and
still others love milk, or why some people think Aishwarya Rai is beautiful
while many others think she looks like something the cat brought in. All I am
going to say here is that syntax familiarity is a big thing as far as a
developer is concerned.
How intuitive do you think is something like this to someone coming from a
C++ background?
char[] arr = new char[128];
The first thing he/she would think would be that someone got their brackets
in the wrong place. How about this?
int y = arr.Length;
"Yikes" would be a highly probable reaction to that. Now compare that to :-
char natarr[128];
array<char>^ refarr = gcnew array<char>(128);
int y = refarr->Length;
Note the distinction in syntax between declaring a native array and a managed
array. The distinctive template-like syntax visually alerts the developer to the
fact that refarr
is not the typical C++ array and that it's
probably some sort of CLI class descendent (which it is as a matter of fact) and
thus there's every chance that methods and properties can be applied to it.
The finalizer syntax chosen for C# is probably the single biggest source of
confusion for C++ programmers who've moved to C#. See the following C# code :-
class R
{
~R()
{
}
}
Okay, so ~R
looks like a destructor but is actually the
finalizer. Why, I ask you, why? Compare that to the following C++ code :-
ref class R
{
~R()
{
}
!R()
{
}
};
The ~R
here is the destructor (actually the Dispose-pattern
equivalent of a destructor - but it behaves like a destructor as far as the C++
coder is concerned) and the new !R
syntax is for the finalizer - so
no confusion there and the syntax is visually compatible with native C++.
Take a look at the generics syntax in C# :-
class R<T>
{
};
And now look at the syntax in C++ :-
generic<typename T> ref class R
{
};
Anyone who's ever used templates would only need a fraction of a second to
figure out the C++ syntax, while the C# syntax is unwarrantedly confusing and
non-intuitive. I could probably go on and on like this, but it'd be
ideologically repetitive! My point is that, if you've come from a C++
background, C++/CLI syntax would be the closest to what you've been using thus
far. C# (and J# for that matter) look like C++, but there are quite a few odd semantic
differences that can be extremely frustrating and annoying, and unless you give
up C++ totally (banish the very thought!!!), the syntactic differences would
never cease to cause confusion and frustration. In that sense, I think VB.NET is
better, at least it's got its own unique syntax, so someone doing both C++ and
VB.NET is not going to mix up the syntax.
Conclusion
Eventually, what language you code in may depend on multiple factors - what
languages are your colleagues using, what languages were used to develop the
existing code repository, will your client have any language-specifications for
you etc. The purpose of this article was to establish a few solid scenarios
where C++/CLI has a definite edge over any other CLI language. If you have an
application that's going to do native-interop 90% of the time, why would you
even think of using anything other than C++? If you are attempting to develop a
generic collection, why restrict yourself to just generics when you can get the
best of both generics and templates? If you already work with C++, why learn a
new language? Often, I have felt that languages like C# and VB.NET attempt to
hide the CLR from you, the developer, whereas C++ not only lets you smell the
CLR but if you try hard enough you can kiss it!
History
- July 28 2005
- Based on feedback I got from Jochen Kalmbach, a Visual C++ MVP, I've
added the
SuppressUnmanagedCodeSecurity
attribute to the C#
P/Invoke declarations and as Jochen right stated, it does improve C#'s
P/Invoke performance, though it's still no where near C++ Interop
performance.
- I've also added a few paragraphs/code-snippets that show how P/Invoke
nearly always results in source code bloat compared to C++ Interop.
- Other minor changes were made to various parts of the article.
- July 26 2005 - Article first published [Parts of this article were
written much earlier, but it was on this date that I put everything together
into a single (hopefully) meaningful commentary]