The cpp:type and cpp:view-type Metadata Directives with C++11

Ice for C++ provides two metadata directives that allow you to map Slice types to arbitrary C++ types: cpp:type and cpp:view-typeThese metadata directives currently apply only to the following Slice types:

  • string
  • sequence
  • dictionary

On this page:

Customizing the sequence mapping with cpp:type 

The cpp:type:c++-type metadata directive allows you to map a given Slice type, data member or parameter to the C++ type of your choice. 

For example, you can override the default mapping of a Slice sequence type:

Slice
[["cpp:include:list"]]

module Food
{
    enum Fruit { Apple, Pear, Orange };

    ["cpp:type:std::list<Food::Fruit>"]
    sequence<Fruit> FruitPlatter;
}

With this metadata directive, the Slice sequence now maps to a C++ std::list instead of the default std::vector:

C++
#include <list>

namespace Food
{

    using FruitPlatter = std::list<Food::Fruit>;

    // ...
}

The Slice to C++ compiler takes the string following the cpp:type: prefix as the name of the mapped C++ type. For example, we could use ["cpp:type:::std::list< ::Food::Fruit>"]. In that case, the compiler would use a fully-qualified name to define the type:

C++
using FruitPlatter = ::std::list< ::Food::Fruit>;

Note that the code generator inserts whatever string you specify following the cpp:type: prefix literally into the generated code. We recommend you use fully qualified names to avoid C++ compilation failures due to unknown symbols.

Also note that, to avoid compilation errors in the generated code, you must instruct the compiler to generate an appropriate include directive with the cpp:include file metadata directive. This causes the compiler to add the line

C++
#include <list>

to the generated header file.

In addition to modifying the type of a sequence itself, you can also modify the mapping for particular return values or parameters. For example:

Slice
[["cpp:include:list"]]
[["cpp:include:deque"]]

module Food
{
    enum Fruit { Apple, Pear, Orange }

    sequence<Fruit> FruitPlatter;

    interface Market
    {
        ["cpp:type:list<::Food::Fruit>"]
        FruitPlatter barter(["cpp:type:deque<::Food::Fruit>"] FruitPlatter offer);
    }
}

With this definition, the default mapping of FruitPlatter to a C++ vector still applies but the return value of barter is mapped as a list, and the offer parameter is mapped as a deque.

Instead of std::list or std::deque, you can specify a type of your own as the sequence type, for example:

Slice
[["cpp:include:FruitBowl.h"]]

module Food
{
    enum Fruit { Apple, Pear, Orange }

    ["cpp:type:FruitBowl"]
    sequence<Fruit> FruitPlatter;
}

With these metadata directives, the compiler will use a C++ type FruitBowl as the sequence type, and add an include directive for the header file FruitBowl.h to the generated code.

The class or template class you provide must meet the following requirements:

  • The class must have a default constructor.
  • The class must have a copy constructor.

If you use a class that also meets the following requirements

  • The class has a single-argument constructor that takes the size of the sequence as an argument of unsigned integral type.
  • The class has a member function size that returns the number of elements in the sequence as an unsigned integral type.
  • The class provides a member function swap that swaps the contents of the sequence with another sequence of the same type.
  • The class defines iterator and const_iterator types and provides begin and end member functions with the usual semantics; its iterators are comparable for equality and inequality.

then you do not need to provide code to marshal and unmarshal your custom sequence – Ice will do it automatically.

Less formally, this means that if the provided class looks like a vectorlist, or deque with respect to these points, you can use it as a custom sequence implementation without any additional coding.

Customizing the dictionary mapping with cpp:type

You can override the default mapping of Slice dictionaries to C++ maps with a cpp:type metadata directive, for example:

Slice
[["cpp:include:unordered_map"]]

["cpp:type:std::unordered_map<long long, Employee>"] dictionary<long, Employee> EmployeeMap;

With this metadata directive, the dictionary now maps to a C++ std::unordered_map:

C++
#include <unordered_map>

using EmployeeMap = std::unordered_map<long long, Employee>;

Like with sequences, anything following the cpp:type: prefix is taken to be the name of the type. For example, we could use ["cpp:type:::std::unordered_map<int, std::string>"]. In that case, the compiler would use a fully-qualified name to define the type:

C++
using IntStringDict = ::std::unordered_map<int, std::string>;

To avoid compilation errors in the generated code, you must instruct the compiler to generate an appropriate include directive with the cpp:include file metadata directive. This causes the compiler to add the line

C++
#include <unordered_map>

to the generated header file.

Instead of std::unordered_map, you can specify a type of your own as the dictionary type, for example:

Slice
[["cpp:include:CustomMap.h"]]

["cpp:type:MyCustomMap<long long, Employee>"] dictionary<long, Employee> EmployeeMap;

With these metadata directives, the compiler will use a C++ type MyCustomMap as the dictionary type, and add an include directive for the header file CustomMap.h to the generated code.

The class or template class you provide must meet the following requirements:

  • The class must have a default constructor.
  • The class must have a copy constructor.
  • The class must provide nested types named key_typemapped_type and value_type.
  • The class must provide iterator and const_iterator types and provide begin and end member functions with the usual semantics; these iterators must be comparable for equality and inequality.
  • The class must provide a clear function.
  • The class must provide an insert function that takes an iterator (as location hint) plus a value_type parameter, and returns an iterator to the new entry or to the existing entry with the given key.

Less formally, this means you can use any class or template class that looks like a standard map or unordered_map as your custom dictionary type.

In addition to modifying the type of a dictionary itself, you can also modify the mapping for particular return values or parameters. For example:

Slice
[["cpp:include:unordered_map"]]

module HR
{
    struct Employee
    {
       long   number;
       string firstName;
       string lastName;
    }
    dictionary<long, Employee> EmployeeMap;

    interface Office
    {
        ["cpp:type:std::unordered_map<long long, Employee>"] EmployeeMap getAllEmployees();
    }
}

With this definition, getAllEmployees  returns an unordered_map, while other unqualified parameters of type EmployeeMap would use the default mapping (to a std::map).

cpp:type with custom C++ types

If your C++ type does not look like a standard C++ container, you need to tell Ice how to marshal and unmarshal this type by providing your own StreamHelper specialization for this type.

For example, you can map a Slice sequence<byte> to a Google Protocol Buffer C++ class, tutorial:Person, with the following metadata directive:

Slice
module Demo
{
   ["cpp:type:tutorial::Person"] sequence<byte> Person;
}

Since tutorial::Person does not look like a vector<Ice::Byte> or a list<Ice::Byte>, you need a StreamHelper specialization for tutorial::Person.

The simplest is to create a StreamHelper specialization that handles only this specific class:

C++
namespace Ice
{ 
    template<>
    struct StreamHelper<tutorial::Person, StreamHelperCategoryUnknown>
    {
        template<class S> static inline void 
        write(S* stream, const tutorial::Person& v)
        {
            // ... marshal v into a sequence of bytes...
        }
    
        template<class S> static inline void 
        read(S* stream, tutorial::Person& v)
        {
            //... unmarshal bytes from stream into v...
        }
    };
}

You should also provide the corresponding StreamTraits specialization:

C++
namespace Ice
{
    template<>
    struct StreamableTraits<tutorial::Person>
    {
        static const StreamHelperCategory helper = StreamHelperCategoryUnknown;
        static const int minWireSize = 1;
        static const bool fixedLength = false;
    };
}

This StreamTraits specialization is actually optional for tutorial::Person, and more generally for any mapping of sequence<byte>, since these are the values provided by the base StreamableTraits template.

Finally, remember to insert the header for these StreamHelper and StreamTraits specializations with a cpp:include file metadata directive:

Slice
[["cpp:include:Person.pb.h"]]
[["cpp:include:PersonStreaming.h"]]
module Demo {
["cpp:type:tutorial::Person"] sequence<byte> Person;

Now, if your application maps several Slice sequence<byte> to different Google Protocol Buffers, you could provide a StreamHelper specialization for each of these Google Protocol Buffer classes, but it would be more judicious to provide a single partial specialization capable of marshaling and unmarshaling any Google Protocol Buffer C++ class as a Slice sequence<byte> in a stream:

C++
namespace Ice
{
 
    // A new helper category for all Google Protocol Buffers
    const StreamHelperCategory StreamHelperCategoryProtobuf = 100;
 
    // All classes derived from ::google::protobuf::MessageLite will use this StreamableTraits
    template<typename T>
    struct StreamableTraits<T, typename std::enable_if<std::is_base_of< ::google::protobuf::MessageLite, T>::value >::type>
    {  
        static const StreamHelperCategory helper = StreamHelperCategoryProtobuf;
        static const int minWireSize = 1;
        static const bool fixedLength = false;
    };
    // T can be any Google Protocol Buffer C++ class
    template<typename T>
    struct StreamHelper<T, StreamHelperCategoryProtobuf>
    {
        template<class S> static inline void 
        write(S* stream, const T& v)
        {
            std::vector<Byte> data(v.ByteSize());
            // ... marshal v into a sequence of bytes...
            stream->write(&data[0], &data[0] + data.size());
        }
    
        template<class S> static inline void 
        read(S* stream, T& v)
        {
            std::pair<const Byte*, const Byte*> data;
            stream->read(data);
            //... unmarshal data into v...
        }
    };
}

If you use any of these sequence<byte> mapped to Google Protocol Buffers for optional data members or optional parameters, you also need to tell Ice which optional format to use:

C++
namespace Ice
{
    // Optional format for Slice sequence<byte> mapped to Google Protocol Buffer
    // The template parameters correspond to the data members of the corresponding StreamTraits specialization.
    template<> 
    struct GetOptionalFormat<StreamHelperCategoryProtobuf, 1, false>
    {
        static const OptionalFormat value = OptionalFormatVSize;
    };
}

The OptionalFormat provided by GetOptionalFormat for StreamHelperCategoryUnknown and its default StreamableTraits is OptionalFormatVSize, like in the example above. This way, if your application needs a single sequence<byte> mapped to a custom C++ type, you can provide just a StreamHelper specialization for this type and rely on the default StreamTraits and GetOptionalFormat templates.

cpp:type:string and cpp:type:wstring

The metadata directives cpp:type:string and cpp:type:wstring are used to map Slice strings to std::string or std::wstring, as described in Alternative String Mapping for C++. The Slice to C++ compiler recognizes string and wstring as special tokens and does not treat them like C++ types with these names.

For example, you can map a sequence<string> to a std::vector<std::wstring> with either of the following directives:

Slice
// Special cpp:type:wstring metadata directive: it maps the sequence to a std::vector<std::wstring>, not to a wstring!
["cpp:type:wstring"] sequence<string>;

or

Slice
// Maps the sequence to a std::vector<std::wstring> using a regular cpp:type metadata directive
["cpp:type:std::vector<std::wstring>"] sequence<string>;

Avoiding copies with cpp:view-type

The main drawback of using standard C++ library classes to represent strings, sequences and dictionaries (as Ice does by default) is copying. For example, if we transfer an array of characters with Ice:

Slice
void sendChars(string s);

with the default mapping, our C++ code would look like:

C++
// client side
const char* cstring = ... null terminated array of chars;
proxy->sendChars(string(cstring));
 
// server side
void sendChars(string s, ...)
{
   // use s;
}

Each invocation triggers a number of copies:

  • (client) the creation of the string on the client side makes a copy of the array of characters
  • (client) sendChars copies the characters of the string into the client-side marshaling buffer
  • (server) the unmarshaling code creates a string from the bytes in the server-side marshaling buffer

If instead of std::string, Ice could use a string type with view semantics–its instances point to memory owned by some other objects–we could avoid these copies. This is exactly what the cpp:view-type metadata directive allows us to do: select a custom mapping for Slice strings, sequences and dictionaries.

For example, we can map our Slice string parameter in the example above to a C++17 std::string_view (a class with view semantics):

Slice
void sendChars(["cpp:view-type:std::string_view"] string s);

Our C++ code is pretty much the same, but we avoid several copies:

C++
// client side
const char* cstring = ... null terminated array of chars;
proxy->sendChars(string_view(cstring)); // string_view points to the characters cstring, without copy
 
// server side
void sendChars(string_view s, ...) // string_view points to bytes in the server-side unmarshaling buffer
{
   // use s;
}

With this metadata directive, the only remaining copy during each invocation is:

  • (client) sendChars copies the characters into the client-side marshaling buffer

This cpp:view-type metadata is very much like the cpp:type metadata directive described earlier, and just like for cpp:type, the Slice to C++ compiler uses the string provided after the cpp:view-type: prefix literally in the generated code. The compiler does not (and cannot) check that this string represents a valid C++ type, or a C++ type with view semantics.

There are however two significant differences between cpp:view-type and cpp:type:

  • cpp:view-type can be applied only to operation parameters, while cpp:type can be applied to the definition of sequence and dictionary types, to operation parameters and to data members
  • cpp:view-type changes the mapping of a parameter to the specified C++ type only when it is safe to use a view object (an object that does not own memory), while cpp:type changes the mapping all the time.

For example, if instead of sending an array of characters from a client to a server, we return an array of characters from the server to the client, without using AMI or AMD (the default):

Slice
string getChars();

 With the default mapping, we get a std::string back, and each invocation makes a few copies:

C++
// client-side
string s = prx->getChars();
 
// server side
string 
getChars(...)
{
   const char* cstring = ... null terminated array of chars;
   return cstring; // the conversion to string makes a copy
}

Now, if we map the returned string to a string_view with the cpp:type metadata directive:

Slice
["cpp:type:std::string_view"] string getChars(); // don't do this

 we avoid some copies but our string_view now points to deallocated memory and our program will crash!

C++
// client-side
string_view s = prx->getChars(); // string_view points to the client-side marshaling buffer, which is deallocated as soon as getChars() returns
 
// server side
string_view 
getChars(...)
{
   const char* cstring = ... null terminated array of chars;
   return cstring; // string_view points to (or may point to) stack-allocated memory, reclaimed when getChars() returns 
}

Never use the cpp:type metadata directive with a view type (a type that does not manage its memory); you should only use cpp:view-type with view types.

With cpp:view-type, the compiler changes the mapping only when it's safe to use a view-type, namely:

  • Input parameters, on the client-side and on the server-side
  • Out and return parameters provided by the Ice run-time to AMI callbacks
  • Out and return parameters provided to marshaled results or AMD callbacks 

The cpp:array metadata directive for sequences follows the same rules.

With our getChars example:

Slice
["cpp:view-type:std::string_view"] string getChars();

it is not safe the change the client-side mapping or the server-side mapping (unless we use a marshaled result or AMD), so the returned C++ type remains a std::string.

Like with the cpp:type metadata directive, if the C++ type specified with cpp:view-type is a standard library type (such as std::list) or looks like one, Ice will automatically marshal and unmarshal it for you. You just need to include the corresponding header with the cpp:include file metadata directive:

Slice
[["cpp:include:MyContainer.h"]]
module Sample 
{
    ...
}

For other C++ types, you need to tell Ice how to marshal and unmarshal these types, as described above in cpp:type with custom C++ types

In particular, Ice does not know how to marshal and unmarshal the string_view type. If you want to map some string parameters to string_view, you need to provide a StreamHelper for string_view, such as:

C++
namespace Ice
{ 
    template<>
    struct StreamableTraits<std::string_view>
    {
        static const StreamHelperCategory helper = StreamHelperCategoryBuiltin;
        static const int minWireSize = 1;
        static const bool fixedLength = false;
    };

    template<>
    struct StreamHelper<std::string_view, StreamHelperCategoryBuiltin>
    {
        template<class S> static inline void
        write(S* stream, const std::string_view& v)
        {
            stream->write(v.data(), v.size());
        }

        template<class S> static inline void 
        read(S* stream, std::string_view& v)
        {
            const char* vdata = 0;
            size_t vsize = 0;
            stream->read(vdata, vsize);
            v = std::string_view(vadata, vsize);
        }
    };
}

This StreamHelper specialization will take care of plain string parameters, and also sequence or dictionary parameters that contain strings mapped to string_view (when it's safe to do so), such as:

Slice
 ["marshaled-result", "cpp:view-type:std::vector<std::string_view>"] StringSeq 
 echoStringSeq(["cpp:view-type:std::vector<std::string_view>"] StringSeq seq);

The Ice/throughput demo illustrates the use of cpp:view-type with sequences of strings.

Using both cpp:type and cpp:view-type

If you specify both cpp:type and cpp:view-type for the same parameter, cpp:view-type applies to mapped parameters safe for view types, and cpp:type applies to all other mapped parameters.

With the following somewhat contrived example:

Slice
["marshaled-result", "cpp:view-type:std::vector<std::string_view>", "cpp:type:std::list<std::wstring>"] StringSeq getStringSeq(); 

calling getStringSeq() on a proxy returns a std::list<std::wstring>, while the StringSeq is passed to the marshaled result struct as a std::vector<string_view>.

See Also