|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis 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-interopC++ 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
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
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 adaptersvoid ShowAdapInfo()
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
ULONG OutBufLen = 0;
//Get the required size of the buffer
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];
//Get info for the adapters
if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS )
{
//Call failed
}
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/Invokeconst 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;
//Get the required size of the buffer
if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) ==
ERROR_BUFFER_OVERFLOW )
{
IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen);
//Get info for the adapters
if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS )
{
//Call failed
}
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
Stack semantics and deterministic destructionC++ 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# C# code - using block semanticspublic 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 semanticsstatic 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 Modified C# codepublic 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 Modified C++ codestatic 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 Still not convinced? Okay, lets take a look at member
objects and their destruction. Imagine CLI GC classes C# implementation of a disposable class hierarchyclass 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 The equivalent C++ implementationref 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
Mixed typesOkay, 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.
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(){}
//Assume non-trivial ctor/dtor
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 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 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 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 protected:
msclr::auto_gcroot<MD5CryptoServiceProvider^> md5;
In the md5 = gcnew MD5CryptoServiceProvider();
And add an 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 Managed templatesIf 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 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;
}
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 generic<typename T, int x> ref class G
{
};
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 :-
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 The following code won't compile :- generic<typename T> ref class R : T
{
};
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/CLRC++ 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 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)
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, 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 - template<typename ForwardIterator> void Capitalize(
ForwardIterator first, ForwardIterator end)
{
for(ForwardIterator it = first; it < end; it++)
*it = (*it)->ToUpper();
}
It goes through a 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 Familiar syntaxDevelopers 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 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 ref class R
{
~R()
{
}
!R()
{
}
};
The 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. ConclusionEventually, 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
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||