One of the fundamental data structures in information processing is the tree, to model hierarchical data. Although we have many standardized containers in C++, a tree structure is still missing. Of course, there are several different libraries which provide this feature in different facets. For my current work I am using Boost PropertyTree and in this article I want to describe a small enhancement to it.

Boost PropertyTree delivers some quite unique features compared to other tree libraries. For instance it has a flexible leaf type built in, i.e. the data stored in different leaves may have different types. Though one could add this feature to other trees with a variant type in the leaves, but this adds another level of indirection. Another very nice feature is the parsing and generation of XML, JSON, INI and Info files. So, for simple data storing and reading tasks Boost PropertyTree provides everything out of the box.

Once you like to work with this tree structure, you might use it in other contexts as simple data handling. That leaves may hold different types comes in very handy, but one realizes quite soon, they may only hold types, which stream to and from string (at least if you use the default ptree with standard data type string). So, we could provide the two stream operators for our data type, if we were free to add them (they could already be in use for user output etc.).

Luckily the authors help us in this respect and provide a customization point for PropertyTree, namely the struct translator_between. Next we need to get a fast and reliable string storage for custom types like:

struct person_t {
  std::string prename;
  std::string name;
};

struct entry_t {
  std::string key;
  std::size_t value;
};
struct data_t {
  std::string name;
  std::vector<entry_t> entries;
};

Manually writing and parsing might still be ok for person_t but it gets quite cumbersome for data_t, even when using Boost Spirit. Again we rely on a solid solution by using Boost Serialization for this task. Since you might not be able to alter the type definitions, Boost Serialization allows to add the serialization feature externally to the types:

namespace boost {
  namespace serialization {

    template<class Archive>
    void serialize(Archive & ar, person_t & obj, const unsigned int) {
      ar & obj.name;
      ar & obj.prename;
    }

    template<class Archive>
    void serialize(Archive & ar, entry_t & obj, const unsigned int) {
      ar & obj.key;
      ar & obj.value;
    }

    template<class Archive>
    void serialize(Archive & ar, data_t & obj, const unsigned int) {
      ar & obj.name;
      ar & obj.entries;
    }

  } // namespace serialization
} // namespace boost

However, we need to tell our PropertyTree that it should use the serialization if available. As said we specialize the struct translator_between template.

    typedef std::string string_type;

    template<typename T>
    struct translator_between<string_type, T> {
      typedef SerializationTranslator<string_type, T> type;
    };

    template<>
    struct translator_between<string_type, string_type> {
      typedef id_translator<string_type> type;
    };

This tells Boost PropertyTree, that it finds a translator type SerializationTranslator somewhere. We get some compiling problems because there are already some translator_between specializations available, and the compiler cannot figure out which to use for string. To get around that, we specialize again for our chosen string_type. You have to change the string_type accordingly if you're using a different string data type. To proceed we need to provide the translator:

    template<typename String, typename T>
    struct SerializationTranslator {
      typedef String internal_type;
      typedef T      external_type;

      boost::optional<external_type> get_value(const internal_type& str);

      boost::optional<internal_type> put_value(const external_type& obj);
    };

We are providing a set and a get method, but the implementation is a little involved. There are a view points we need to handle:

  • Boost PropertyTree stores fundamental types by using their stream operators. This gives a short (no boiler plate) and human readable string representation.
  • Boost Serialization stores fundamental types automatically also by using the stream operators. However, the serialization is longer then the string and not human readable.
  • Boost PropertyTree might be used to read or write files to disk; it would be nice to have those as clean as possible. So, we should not use Boost Serialization for fundamental types.
  • For custom types, we might already have stream operators defined. It is quite likely that those are not build to store and restore values, but to communicate with the user. So, we won't use those.
  • Finally, to store a custom type in the tree we want to have a serialization to have it as type safe as possible.
      boost::optional<external_type> get_value(const internal_type& str) {
        std::istringstream stream(str);
        external_type result;
        istream(stream, result);
        if (stream.rdstate() == std::ios::failbit) {
          return boost::none;
        } else {
          return result;
        }
      }

      boost::optional<internal_type> put_value(const external_type& obj) {
        std::ostringstream result;
        ostream(result, obj);
        return result.str();
      }

    private:
      //--------------------------------------
      template <typename Object, typename = typename std::enable_if<
          std::is_assignable<internal_type, Object>::value>::type>
      static auto ostream(std::ostream& o, Object const& obj)
          -> decltype(o << obj, void()) {
        o << obj;
      }

      template <typename Object, typename = typename std::enable_if<
          !std::is_assignable<internal_type, Object>::value>::type>
      static auto ostream(std::ostream& o, Object const& obj)
          -> decltype(boost::serialization::serialize<boost::archive::binary_oarchive>(
              std::declval<boost::archive::binary_oarchive&>(), obj, 0), void()) {
        boost::archive::binary_oarchive oa(o);
        oa << obj;
      }

      //--------------------------------------
      template <typename Object, typename = typename std::enable_if<
          std::is_assignable<internal_type, Object>::value>::type>
      static auto istream(std::istream& i, Object& obj)
          -> decltype(i >> obj, void()) {
        i >> obj;
      }

      template <typename Object, typename = typename std::enable_if<
          !std::is_assignable<internal_type, Object>::value>::type>
      static auto istream(std::istream& i, Object& obj)
          -> decltype(boost::serialization::serialize<boost::archive::binary_iarchive>(
              std::declval<boost::archive::binary_iarchive&>(), obj, 0), void()) {
        try {
          boost::archive::binary_iarchive ia(i);
          ia >> obj;
        } catch (boost::archive::archive_exception) {
          i.setstate(std::ios::failbit);
        }
      }
      //--------------------------------------
    };

We provide in each stream direction two functions. At most one of them is available for a given type, since the std::enable_if is complementary. In the enable if we check if a std::string could be constructed from the given type. This is true for the fundamental types and handles the stream operators for custom types. Additionally we enforce the existence of boost::serialization::serialize for custom types, which gives nicer compiler errors, if we try to store a custom type without serialization.

That is basically it. For each new type just add the corresponding serialize method and you are good to go.

Complete example

Here is the full example to play:

#include <boost/property_tree/ptree.hpp>
namespace pt = boost::property_tree;

#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/serialization/vector.hpp>

#include <iostream>
#include <string>
#include <vector>

//==============================================================================
struct person_t {
  std::string prename;
  std::string name;
};

struct entry_t {
  std::string key;
  std::size_t value;
};
struct data_t {
  std::string name;
  std::vector<entry_t> entries;
};

//--------------------------------------
inline std::ostream& operator<<(std::ostream &o, person_t const& obj) {
  return o << obj.prename << " " << obj.name;
} 

//--------------------------------------
namespace boost {
  namespace serialization {

    template<class Archive>
    void serialize(Archive & ar, person_t & obj, const unsigned int) {
      ar & obj.name;
      ar & obj.prename;
    }

    template<class Archive>
    void serialize(Archive & ar, entry_t & obj, const unsigned int) {
      ar & obj.key;
      ar & obj.value;
    }

    template<class Archive>
    void serialize(Archive & ar, data_t & obj, const unsigned int) {
      ar & obj.name;
      ar & obj.entries;
    }

  } // namespace serialization
} // namespace boost

//==============================================================================
namespace boost {
  namespace property_tree {

    template<typename String, typename T>
    struct SerializationTranslator {
      typedef String internal_type;
      typedef T      external_type;

      boost::optional<external_type> get_value(const internal_type& str) {
        std::istringstream stream(str);
        external_type result;
        istream(stream, result);
        if (stream.rdstate() == std::ios::failbit) {
          return boost::none;
        } else {
          return result;
        }
      }

      boost::optional<internal_type> put_value(const external_type& obj) {
        std::ostringstream result;
        ostream(result, obj);
        return result.str();
      }

    private:
      //--------------------------------------
      template <typename Object, typename = typename std::enable_if<
          std::is_assignable<internal_type, Object>::value>::type>
      static auto ostream(std::ostream& o, Object const& obj)
          -> decltype(o << obj, void()) {
        o << obj;
      }

      template <typename Object, typename = typename std::enable_if<
          !std::is_assignable<internal_type, Object>::value>::type>
      static auto ostream(std::ostream& o, Object const& obj)
          -> decltype(boost::serialization::serialize<boost::archive::binary_oarchive>(
              std::declval<boost::archive::binary_oarchive&>(), obj, 0), void()) {
        boost::archive::binary_oarchive oa(o);
        oa << obj;
      }

      //--------------------------------------
      template <typename Object, typename = typename std::enable_if<
          std::is_assignable<internal_type, Object>::value>::type>
      static auto istream(std::istream& i, Object& obj)
          -> decltype(i >> obj, void()) {
        i >> obj;
      }

      template <typename Object, typename = typename std::enable_if<
          !std::is_assignable<internal_type, Object>::value>::type>
      static auto istream(std::istream& i, Object& obj)
          -> decltype(boost::serialization::serialize<boost::archive::binary_iarchive>(
              std::declval<boost::archive::binary_iarchive&>(), obj, 0), void()) {
        try {
          boost::archive::binary_iarchive ia(i);
          ia >> obj;
        } catch (boost::archive::archive_exception) {
          i.setstate(std::ios::failbit);
        }
      }
      //--------------------------------------
    };

    typedef std::string string_type;

    template<typename T>
    struct translator_between<string_type, T> {
      typedef SerializationTranslator<string_type, T> type;
    };

    template<>
    struct translator_between<string_type, string_type> {
      typedef id_translator<string_type> type;
    };

  } // namespace property_tree
} // namespace boost

//==============================================================================
template <typename T>
void test_ptree(T const& obj) {
  pt::ptree tree;

  tree.add("data", obj);
  std::cout << tree.get<T>("data") << std::endl;
}

//==============================================================================
int main() {
  test_ptree(42);
  test_ptree(4.7f);
  test_ptree(std::string{"Hello world"}); 
  test_ptree(person_t{"Homer", "Simpson"});

  entry_t e1 {"One", 1};
  entry_t e2 {"Two", 2};

  data_t data {"Data", {e1, e2}};

  pt::ptree tree;
  tree.add("data", data); 
  auto ret = tree.get<data_t>("data");

  std::cout << ret.name << std::endl;
  for (const auto &e : ret.entries) {
    std::cout << e.key << ":" << e.value << std::endl;
  }
}

Output:

42
4.7
Hello world
Homer Simpson
Data
One:1
Two:2