cereal supports having specific serialization behaviors for different archive types.
You can specialize your own types or types that cereal comes with support for to exhibit specific behaviors with specific archives. This is typically done by replacing at least one generic templated parameter with the type you wish to specialize for. Specializations will be given precedence over any templated serialization function.
If you want to specialize for several types of archives, you will need to use templates and type traits to restrict
template instantiation. cereal provides several traits to help with this, defined in <cereal/details/traits.hpp>
.
Although cereal comes with a support for nearly every type in the standard library, there can be times when it is desirable to have custom functionality for a type that cereal provides the serialization for. Function overloading can be used to override the cereal implementation with a custom one. This will work even if you include the cereal support for a type - assuming that the compiler doesn’t find any ambiguity in your overload (more on that later).
If you want to make an archive behave differently for some type, you can do this by creating overloads where the archive is explicitly defined (and not a template parameter):
namespace cereal
{
// Overload the std::complex serialization code for a specific archive
template <class T>
void save( cereal::XMLOutputArchive & ar, std::complex<T> const & comp )
{ /* your code here */ }
template <class T>
void load( cereal::XMLInputArchive & ar, std::complex<T> & comp )
{ /* your code here */ }
// Note that it doesn't make much sense to overload a plain serialize function
// (i.e. void serialize()) because it expects to be called
// for loading (an input archive) and saving (an output archive).
// We would end up needing to implement two versions of it, so using a load/save
// pair makes more sense.
}
Important!
When overloading a cereal provided serialization function, you should place it in the cereal
namespace.
It is also easy to specialize the behavior of a specific type for all archives or a group of archives. This is done by restricting the serialized type instead of restricting the archive (though you can definitely specialize on both the archive and the type).
The following example shows how to specialize serialization for std::map<std::string, std::string>
for text archives such that it
roughly matches the output of an SQL `WHERE` clause in which all the expressions (from a name-value-pair perspective)
are ANDed together (inspired by this stackoverflow post):
namespace cereal
{
//! Saving for std::map<std::string, std::string> for text based archives
// Note that this shows off some internal cereal traits such as EnableIf,
// which will only allow this template to be instantiated if its predicates
// are true
template <class Archive, class C, class A,
traits::EnableIf<traits::is_text_archive<Archive>::value> = traits::sfinae> inline
void save( Archive & ar, std::map<std::string, std::string, C, A> const & map )
{
for( const auto & i : map )
ar( cereal::make_nvp( i.first, i.second ) );
}
//! Loading for std::map<std::string, std::string> for text based archives
template <class Archive, class C, class A,
traits::EnableIf<traits::is_text_archive<Archive>::value> = traits::sfinae> inline
void load( Archive & ar, std::map<std::string, std::string, C, A> & map )
{
map.clear();
auto hint = map.begin();
while( true )
{
const auto namePtr = ar.getNodeName();
if( !namePtr )
break;
std::string key = namePtr;
std::string value; ar( value );
hint = map.emplace_hint( hint, std::move( key ), std::move( value ) );
}
}
} // namespace cereal
Important!
Please note that for all specializations, you must have at least one parameter that has higher precedence than the
default templated version that cereal provides. This usually means you need to explicitly specialize at least one
template parameter. In the above examples we specialized two of the template parameters for std::map
.
int main()
{
std::stringstream ss;
{
cereal::JSONOutputArchive ar(ss);
std::map<std::string, std::string> filter = {{"type", "sensor"}, {"status", "critical"}};
ar( CEREAL_NVP(filter) );
}
std::cout << ss.str() << std::endl;
{
cereal::JSONInputArchive ar(ss);
cereal::JSONOutputArchive ar2(std::cout);
std::map<std::string, std::string> filter;
ar( CEREAL_NVP(filter) );
ar2( CEREAL_NVP(filter) );
}
std::cout << std::endl;
return 0;
}
{
"filter": [
{ "key": "type", "value": "sensor" },
{ "key": "status", "value": "critical" }
]
}
{
"filter": {
"status": "critical",
"type": "sensor"
}
}
Specializing a user defined type works in almost exactly the same fashion to types in the standard library that cereal provides
the implementation for. The only real difference is that there is no longer a requirement that the serialization code
reside in the cereal
namespace - it now goes wherever you would normally put your serialization code.
Since there is no provided implementation to conflict with, you have more control over the generality of your implementations. It is much easier to use templates to restrict how the serialization functions are instantiated, as shown in the following example, which uses different serialization depending on whether a binary archive is used versus some other type:
struct Hello
{
int x;
// Enabled for text archives (e.g. XML, JSON)
template <class Archive,
cereal::traits::EnableIf<cereal::traits::is_text_archive<Archive>::value>
= cereal::traits::sfinae>
std::string save_minimal( Archive & ) const
{
return std::to_string( x ) + "hello";
}
// Enabled for text archives (e.g. XML, JSON)
template <class Archive,
cereal::traits::EnableIf<cereal::traits::is_text_archive<Archive>::value>
= cereal::traits::sfinae>
void load_minimal( Archive const &, std::string const & str )
{
x = std::stoi( str.substr(0, 1) );
}
// Enabled for binary archives (e.g. binary, portable binary)
template <class Archive,
cereal::traits::DisableIf<cereal::traits::is_text_archive<Archive>::value>
= cereal::traits::sfinae>
int save_minimal( Archive & ) const
{
return x;
}
// Enabled for binary archives (e.g. binary, portable binary)
template <class Archive,
cereal::traits::DisableIf<cereal::traits::is_text_archive<Archive>::value>
= cereal::traits::sfinae>
void load_minimal( Archive const &, int const & xx )
{
x = xx;
}
};
There are several useful type traits defined in <cereal/details/traits.hpp>
which are useful when specializing serialization functions. The doxygen documentation for this entire file can be found here. The following are a few especially helpful traits:
If you are used to template metaprogramming in C++, you may already be familiar with techniques such as
std::enable_if
, which can be used to selectively disable function
overloads.
cereal provides some similar functionality in a slightly more aesthetically pleasing manner with EnableIf
(documentation) and
DisableIf
(documentation). These will enable (with EnableIf
) or disable (with DisableIf
) a function overload if all of their variadic bool parameters, when ANDed together, are true. Since they perform an AND operation of their arguments, OR operations or other more complicated boolean expressions should be done manually beforehand.
Instead of using the clumsy syntax of std::enable_if
, which requires replacing the function return type, EnableIf
and DisableIf
are added as an extra template parameter to the function, with the default value of
cereal::traits::sfinae
. See any of the above examples or the
linked documentation for useage details.
is_same_archive
operates in a similar manner to std::is_same
except that it will automatically strip various wrappers and CV (const or volatile) qualifiers that may be applied. Its
use is recommended over std::is_same
because cereal will often slightly adjust archive parameters so that they become
const or are wrapped in some internal traits class. See the documentation for more information and an example.
is_text_archive
checks to see if an archive has been tagged with the cereal::traits::TextArchive
tag. JSON and XML
archives that ship with cereal both have this tag. See here for more information.
strip_minimal
is especially useful when working with custom save_minimal
or load_minimal
functions. cereal may
wrap types when performing compile time checks to ensure the validity of such functions, which may cause some custom
metaprogramming to fail unexpectedly. Using the type provided by strip_minimal
, instead of the raw template
parameter, will help prevent this. Check out the documentation as well as its use for serializing enums in the
common.hpp
type support here.