Click here to Skip to main content
Click here to Skip to main content
Go to top

A rational attempt to substantiate C++/CLI as a first class CLI language

, 27 Jul 2005
Rate this:
Please Sign up or sign in to vote.
Why the author thinks that C++/CLI has its own unique role to play as a first-class .NET programming language

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 Wink | ;-)

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;  

    //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/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;  

    //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 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(){}
    //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 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 Smile | :)

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 Wink | ;-)

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]

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
GeneralC++/CLI is the most powerful .NET language [modified] PinmemberFuzzier21-Sep-07 20:13 
GeneralThank you for writing what my heart knew [modified] PinmemberGammill12-Oct-06 15:35 
QuestionHow not to have a member disposed PinmemberArne Vogel9-Aug-06 10:59 
QuestionProblem with P/Invoke Pinmembert4urean16-Apr-06 9:05 
GeneralClass Library Project Written with C++/CLI PinmemberMajid Shahabfar12-Dec-05 21:32 
AnswerRe: Class Library Project Written with C++/CLI Pinmembersxbluebird4-Sep-07 19:53 
QuestionDeployment of a dll / C++ PinsussAnonymous5-Sep-05 3:21 
GeneralP/Invoke should specify SetLastError=false PinmemberMystic Taz3-Aug-05 22:04 
GeneralRe: P/Invoke should specify SetLastError=false PinstaffNishant Sivakumar4-Aug-05 19:35 
GeneralRe: P/Invoke should specify SetLastError=false PinsussJohn Tasler4-Aug-05 22:42 
GeneralNo need to create a new block for each using statement in C# PinmemberFrank Hileman3-Aug-05 5:38 
GeneralRe: No need to create a new block for each using statement in C# PinstaffNishant Sivakumar3-Aug-05 20:13 
GeneralRe: No need to create a new block for each using statement in C# PinmemberFrank Hileman4-Aug-05 4:36 
GeneralRe: No need to create a new block for each using statement in C# PinstaffNishant Sivakumar4-Aug-05 19:32 
GeneralI think you missed some points Pinmemberwazabbe3-Aug-05 2:51 
GeneralRe: I think you missed some points PinmemberWDen3-Aug-05 3:39 
GeneralRe: I think you missed some points Pinmemberwazabbe3-Aug-05 5:03 
GeneralRe: I think you missed some points PinmemberFrank Hileman3-Aug-05 5:51 
GeneralRe: I think you missed some points PinmemberWDen3-Aug-05 8:46 
GeneralRe: I think you missed some points PinstaffNishant Sivakumar3-Aug-05 19:59 
GeneralRe: I think you missed some points PinmemberFrank Hileman4-Aug-05 4:24 
GeneralRe: I think you missed some points PinstaffNishant Sivakumar4-Aug-05 19:36 
GeneralRe: I think you missed some points PinmemberWDen3-Aug-05 5:58 
GeneralRe: I think you missed some points PinstaffNishant Sivakumar3-Aug-05 20:11 
GeneralRe: I think you missed some points PinmemberWDen3-Aug-05 23:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 28 Jul 2005
Article Copyright 2005 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid