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-type
. These 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:
[["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
:
#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:
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
#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:
[["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:
[["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
andconst_iterator
types and providesbegin
andend
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 vector
, list
, 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:
[["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
:
#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:
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
#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:
[["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_type
,mapped_type
andvalue_type
. - The class must provide
iterator
andconst_iterator
types and providebegin
andend
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 aniterator
(as location hint) plus avalue_type
parameter, and returns aniterator
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:
[["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:
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:
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:
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:
[["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:
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:
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 string
s 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:
// 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
// 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:
void sendChars(string s);
with the default mapping, our C++ code would look like:
// 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):
void sendChars(["cpp:view-type:std::string_view"] string s);
Our C++ code is pretty much the same, but we avoid several copies:
// 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, whilecpp:type
can be applied to the definition of sequence and dictionary types, to operation parameters and to data memberscpp: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), whilecpp: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):
string getChars();
With the default mapping, we get a std::string
back, and each invocation makes a few copies:
// 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:
["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!
// 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:
["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:
[["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:
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 string
s mapped to string_view
(when it's safe to do so), such as:
["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:
["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>
.