C++/CLI: System::String^, std::string, and "a string literal"

Without thinking too hard I had held the following precept: std::string is to ISO C++ as System::String^ is to C++/CLI. Or, to give the idea a political tint, I thought of the two string abstractions as separate but equal library class types. Therefore, I presumed the following pair of overloaded methods was inherently ambiguous,

 

void display( String^ );

void display( const string& );

 

and a call such as the following

 

display( "which one?" );

 

would be flagged as ambiguous and I would be required to indicate which instance I intended through an explicit cast. But this turns out to be way off the mark, and misses a key implication of the unified type system of the CLI.

 

The type of a string literal within C++/CLI, by default, is the same as in ISO-C++; that is, it is a const char*.  However, if a string literal occurs in a context in which a String^ Unicode string literal is required, it is treated as such implicitly (otherwise, an explicit conversion is necessary). For example,

string str = "a native string literal";

String ^ps = "a managed string literal";

 

void print( String^ text );

print( "ok, recognized as a managed string literal" );

However, if we use the string literal in a context-free setting, the literal must be explicitly cast to a String^, as in the following instances,

( "hi" )-> PadLeft( 8 ); // error …

(( String^ )"Index" )->PadLeft( 8 ); // correct …

So, getting back to our original question, how is the string literal handled when we invoked our overloaded display() method?

 

display( "which one?" );

 

Under C++/CLI, a string literal goes through a hierarchical sequence of standard conversions, as follows (slightly simplified),

1. if there is no const char* parameter to match exactly, the const is discarded, and it best matches a char* parameter.

2. if there is no char* parameter, it next best matches a String^ parameter. This is what happens in our case.

3. if there is no String^ parameter, it next best matches one of the String^ base classes, either an interface, or Object^.

4. if there is no parameter of one of the String^ base classes, it next best matches a bool parameter!

Only if there is no match of a string literal through a standard conversion are user-defined conversions considered, such as is required to turn a string literal into a std::string. In this sense, a string literal is more nearly a String^ than a string.

 

This reflects a fundamental difference between ISO-C++ and C++/CLI. In ISO-C++, types are independent except when explicitly part of the same class inheritance hierarchy. Thus, there is no implicit type relationship between a string literal and the std::string class type, even though they share a common abstraction domain.

 

C++/CLI, on the other hand, supports a unified type system. Every type, including a literal value, is implicitly a kind of Object^. This is why we can call methods through a literal value or an object of the built-in types. The value 5 is of type Int32. It is derived from System::ValueType, and ValueType is derived from Object. A string literal bridges both the native and managed worlds. Its initial conversions are those of the native world. If the native promotions are not appropriate, it traverses the managed set of standard conversions. The string literal is represented as a String^ within the managed world. A String^ is derived from Object^. In addition, it implements a number of System interfaces. Its declaration looks as follows,

public ref class String sealed :

       IComparable, ICloneable, IConvertible, IEnumerable {}

So, much to my initial surprise, the invocation of display() is not ambiguous – not in the least. Rather, the display(String^) is the best match. Again, this is because the creation of a std::string object requires a user-defined conversion of the string literal through the one-argument string constructor taking a const char* parameter.

 

Here are two further pairs of overloaded methods. If you do not understand why the particular instance is invoked, re-examine the list of four standard conversions presented above.

 

void display( const char *text );

void display( String^ text );

 

// best match: display( const char* ) …

display( "ok, invoke const char* not String^" );

 

void process( Object^ o );

void process( const string& s );

 

// best match: process( Object^ ) …

process( "ok, invoke Object^ not string" );

If you are an experienced native programmer, this probably feels somewhat unbalanced. Within C++/CLI, the traversal distance between “a string literal“ and the std::string class is a considerably wider swatch than within the native language..