wiki:Tutorials/Advanced/Suitable Port Data Types

This article explains which C++ types can be used in Finroc ports and parameters.

Basically, any C++ class (or struct) that can be instantiated and serialized may be used as port type.

There are three types of serialization:

  • Binary (most efficient; data exchange between parts)
  • String (for config files and unknown types in finstruct/fingui)
  • XML (for config files and unknown types in finstruct/fingui)

Types used in ports need to be binary serializable. String and XML serialization can be implemented for convenience in tooling.

Types used in (static) parameters need to be serializable to String or to XML.

If a type T is serializable, then std::vector<T> is serializable, too.

Furthermore, all fundamental numeric types, all enums, bool, std::string, std::pair and std::tuple are serializable.

In the following, we will use the type tAnnotatedPose2D as an example:

struct tAnnotatedPose2D
{
  rrlib::math::tPose2D pose;
  std::string name;
  rrlib::time::tTime last_visit;
}

Note: All deserialization operations ('>>' operators) may throw an exception if deserialization fails.

Binary Serialization

To make a type binary serializable, two stream operators need to be implemented:

rrlib::serialization::tOutputStream& operator << (rrlib::serialization::tOutputStream& stream, const tAnnotatedPose2D& pose)
rrlib::serialization::tInputStream& operator >> (rrlib::serialization::tInputStream& stream, tAnnotatedPose2D& pose)

Public Member Variables

If a type's member variables are public (as in the tAnnotatedPose2D example), serialization is straightforward:

(This is also possible if member variables are private - with the stream operators declared as friends.)

rrlib::serialization::tOutputStream& operator << (rrlib::serialization::tOutputStream& stream, const tAnnotatedPose2D& pose)
{
  stream << pose.pose << pose.name << pose.last_visit;
  return stream;
}

rrlib::serialization::tInputStream& operator >> (rrlib::serialization::tInputStream& stream, tAnnotatedPose2D& pose)
{
  stream >> pose.pose >> pose.name >> pose.last_visit;
  return stream;
}

Here is a little example that covers the case that you want to declare the stream-operator as friend and also use templates, it's a little bit tricky:

template <typename T>
class A {
 private:
  T private_field;

  // declare operator<< as friend. important: do not use T here, but rename it!
  template <typename T_>
  friend rrlib::serialization::tOutputStream& operator << (rrlib::serialization::tOutputStream& stream, const A<T_>& o);
 
};

template <typename T>
rrlib::serialization::tOutputStream& operator << (rrlib::serialization::tOutputStream& stream, const A<T>& o)
{
 stream << o.private_field;
 return stream;
}

NOTE: The stream operators need to be declared as non-member functions, see this Discussion

Furthermore, you might need to add an include:

#include "rrlib/serialization/serialization.h"

Using Getters/Setters

If the member variables of a class are not public, getters and setters of the class need to be used instead. Example:

rrlib::serialization::tOutputStream& operator << (rrlib::serialization::tOutputStream& stream, const tAnnotatedPose2D& pose)
{
  stream << pose.GetPose() << pose.GetName() << post.GetLastVisit();
  return stream;
}

rrlib::serialization::tInputStream& operator >> (rrlib::serialization::tInputStream& stream, tAnnotatedPose2D& pose)
{
  tPose2D p;
  std::string n;
  rrlib::time::tTime lv;
  stream >> p >> n >> lv;  
  pose.SetPose(p);
  pose.SetName(n);
  pose.SetLastVisit(lv);
  return stream;
}

String Serialization

For string serialization, these two operators need to be implemented:

rrlib::serialization::tStringOutputStream& operator << (rrlib::serialization::tStringOutputStream& stream, const tAnnotatedPose2D& pose)
rrlib::serialization::tStringInputStream& operator >> (rrlib::serialization::tStringInputStream& stream, tAnnotatedPose2D& pose)

The second operator may throw an exception, if parsing fails. Or it may print a warning message and use some default value.

String serialization is often inconvenient and error-prone for complex types - as parsing can go wrong in many ways. For the example above (tAnnotatedPose2D), XML serialization is the more convenient option.

XML Serialization

For XML serialization, these two operators need to be implemented:

rrlib::xml::tNode& operator << (rrlib::xml::tNode& node, const tAnnotatedPose2D& pose)
const rrlib::xml::tNode& operator >> (const rrlib::xml::tNode& node, tAnnotatedPose2D& pose)

e.g.:

rrlib::xml::tNode& operator << (rrlib::xml::tNode& node, const tAnnotatedPose2D& pose)
{
  node.SetAttribute("name", pose.name);
  node.SetAttribute("lastvisit", rrlib::serialization::Serialize(pose.last_visit));
  node.SetContent(rrlib::serialization::Serialize(pose.pose));
  return node;
}

const rrlib::xml::tNode& operator >> (const rrlib::xml::tNode& node, tAnnotatedPose2D& pose)
{
  pose.name = node.GetStringAttribute("name");
  pose.last_visit = rrlib::serializaiton::Deserialize<rrlib::time::tTime>(node.GetStringAttribute("lastvisit"));
  pose.pose = rrlib::serializaiton::Deserialize<rrlib::math::tPose2D>(node.GetTextContent());
  return node;
}

Types without no-argument Constructor

Finroc needs to know how to instantiate a data type in order to create multiple empty buffers in a buffer pool. Types without a no-argument constructor require an template specialization of rrlib::serialization::DefaultInstantiation (rrlib::rtti::sStaticTypeInfo in Finroc 13.10).

Suppose, tAnnotatedPose2D only has a constructor accepting a std::string and is in namespace rrlib::example:

  tAnnotatedPose2D(const std::string& initial_name);

We would need to do the following (suppose tAnnotatedPose2D is in namespace rrlib::example):

Finroc 13.10

namespace rrlib
{
namespace rtti
{

// Customize sStaticTypeInfo, because empty constructor is protected
template <>
struct sStaticTypeInfo<example::tAnnotatedPose2D> : public detail::sStaticTypeInfoDefaultImpl<example::tAnnotatedPose2D>
{
  static example::tAnnotatedPose2D* Create(void* placement)
  {
    return new(placement) example::tAnnotatedPose2D("Unknown Pose");
  }

  static example::tAnnotatedPose2D CreateByValue()
  {
    return example::tAnnotatedPose2D("Unknown Pose");
  }
};

} // namespace rtti
} // namespace rrlib

Any newer version of Finroc

namespace rrlib
{
namespace serialization
{

// Customize DefaultInstantation, because empty constructor is protected
template <>
struct DefaultInstantiation<example::tAnnotatedPose2D>
{
  static example::tAnnotatedPose2D Create()
  {
    return example::tAnnotatedPose2D("Unknown Pose");
  }
};

} // namespace serialization
} // namespace rrlib

Specialize Type Traits (Finroc versions newer than 13.10)

There are various type traits in rrlib::serialization and rrlib::rtti that can be specialized - to allow Finroc to handle special types (at all) and also to optimize how Finroc handles certain types.

  • rrlib::rtti::SupportsBitwiseCopy defines whether an object of type T can be safely deep-copied using memcpy and whether equality can be tested using memcmp.
  • rrlib::rtti::GenericOperations defines generic operations for objects of a type T: DeepCopy and Equals. If available, operators = and == are used by default (or bitwise operations if this is valid according to SupportsBitwiseCopy). Otherwise, these operations are performed using serialization, which is not ideal with respect to computational overhead (note: if binary serialization is not implemented correctly, this may have unexpected side effects e.g. in blackboards).
  • rrlib::rtti::AutoRegisterRelatedTypes defines which other types should be registered (if they have not been already) when a tDataType<T> object is created. By default, std::vector<T> is registered as well.
  • rrlib::rtti::TypeName defines the rrlib_rtti name of a type. It can be specialized for types in order to give them other names (possibly because they are more readable - or to retain backward-compatibility). Notably, a name can also be specified in the tDataType constructor. This type trait, however, is useful for defining default names for templates.
  • rrlib::serialization::ContainerSerialization defines how standard containers such as std::vector of a type T are (de)serialized.
  • rrlib::serialization::ContainerResize defines how standard containers such as std::vector of a type T are resized. For all types T with default instantiation, the method resize(size_t) is used.

Make data types known to Finroc

Up to now, Finroc (rrlib_rtti to be more precise) does not know about the data type. To change this, an instance of rrlib::rtti::tDataType<T> must be created for this type (if you create a port or parameter of type T, this is done automatically).

We typically do this in an rtti.cpp file in our rrlibs for all types that are relevant in this respect (see rrlib/math/rtti.cpp for a good example). An rtti.cpp for our tAnnotatedPose2D would look like this:

#ifdef _LIB_RRLIB_RTTI_PRESENT_

//----------------------------------------------------------------------
// External includes (system with <>, local with "")
//----------------------------------------------------------------------
#include "rrlib/rtti/rtti.h"

//----------------------------------------------------------------------
// Internal includes with ""
//----------------------------------------------------------------------
#include "rrlib/xyz/tAnnotatedPose2D.h"

//----------------------------------------------------------------------
// Namespace usage
//----------------------------------------------------------------------
using namespace rrlib::xyz;
using namespace rrlib::rtti;

//----------------------------------------------------------------------
// Type initializers
//----------------------------------------------------------------------
static tDataType<tAnnotatedPose2D> init_type_annotated_pose_2d;

#endif

Type names are automatically extracted from the demangled C++ rtti name. It is, however, also possible to assign a custom name (as first constructor parameter).

Common problems

Stream operator issues

The stream operators should be implemented in the same namespace as type T. Otherwise, the compiler might not find them.

If the stream operator's implementation is in the C++ header file (instead of the .cpp file), the operator needs to be declared inline (for non-template types at least). Otherwise, the linker will complain.

If the stream operator is implemented in the .cpp file in the global namespace, the namespace of the class needs to prepended to operator - e.g.:

rrlib::serialization::tOutputStream& rrlib::example::operator << (rrlib::serialization::tOutputStream& stream, const rrlib::example::tAnnotatedPose2D& pose)

"Must take exactly one argument" Errors

Go and declare the operators outside of your class!

Last modified 4 years ago Last modified on 10.10.2014 13:40:06