Archive Specialization

cereal supports having specific serialization behaviors for different archive types.


TLDR Version

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>.


Specializing Built-in Types

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).

Specializing the archive

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.

Specializing the type

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):

The serialization code:

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.

Using the serialization code:

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

Output using cereal built in support:

{
  "filter": [
    { "key": "type", "value": "sensor" },
    { "key": "status", "value": "critical" }
  ]
}

Output using the above specialized serializaton code:

{
  "filter": {
    "status": "critical",
    "type": "sensor"
  }
}

Specializing User Defined Types

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

Useful Type Traits

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:

EnableIf and DisableIf

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

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

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

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.