Apart from the built-in types, Variants are capable of handling instances of arbitrary types as long as they inherit from VariantDataClass. To create a custom Variant type, four steps need to be taken:
- Write an Ice interface with the basic type description
- Add a Variant type id for the new type
- Write a C++ implementation of the type
- Add the created type to the object factory registry
Ice Interface
The first part of a custom Variant type is an Ice interface that allows using the new type in Ice communication. The interface should inherit from VariantDataClass and define the intended data structure.
In this example, we implement a custom Variant type for complex numbers, defined by a real and an imaginary part, both stored as floats.
{
class ComplexFloatBase extends VariantDataClass
{
float real;
float imag;
};
};
Type Id
Add a VariantTypeId declaration somewhere in the variant's header.
Note: armarx::ComplexFloatBase must be unique in all ArmarX projects.
C++ Implementation
The second part of a custom Variant data type is the C++ implementation of the defined Ice interface ComplexFloatBase. There are three types of methods that need to be present in the implementation:
- Custom data type interface
- Parameterized constructors
- getReal, getImag for accessing the data value
- Standard data type interface
- ice_clone
- clone
- output for printing the data value
- getType for retrieving the Id of the new type
- validate for checking the data's sanity
- Serialization -
- serialize, deserialize for serializing to JSON for MemoryX
class TestComplexFloat : virtual public ComplexFloatBase
{
template <class BaseClass, class VariantClass>
friend class GenericFactory;
public:
TestComplexFloat()
{
real = 0;
imag = 0;
}
TestComplexFloat(float real, float imag)
{
this->real = real;
this->imag = imag;
}
float getReal()
{
return real;
}
float getImag()
{
return imag;
}
{
return this->clone();
}
VariantDataClassPtr clone(
const Ice::Current&
c = Ice::emptyCurrent)
const override
{
return new TestComplexFloat(*this);
}
std::string output(
const Ice::Current&
c = Ice::emptyCurrent)
const override
{
s <<
"(" << real <<
" + " << imag <<
"i)";
}
VariantTypeId getType(
const Ice::Current&
c = Ice::emptyCurrent)
const override
{
return armarx::VariantType::TestComplexFloat;
}
bool validate(
const Ice::Current&
c = Ice::emptyCurrent)
override
{
return true;
}
public:
void serialize(const armarx::ObjectSerializerBasePtr& serializer, const ::Ice::Current& = Ice::emptyCurrent) const override
{
obj->setFloat("real", real);
obj->setFloat("imag", imag);
}
void deserialize(const armarx::ObjectSerializerBasePtr& serializer, const ::Ice::Current& = Ice::emptyCurrent) override
{
real = obj->getFloat("real");
imag = obj->getFloat("imag");
}
};
The next step is to define a pointer type for the new data type:
Object Factory
Finally, the new type needs to be registered to allow it to be used in Ice communication. The factory tells ArmarX, which exact type to instantiate when the respective interface is used in communication. In this case, whenever a ComplexFloatBase is transferred via Ice, it is translated into a TestComplexFloat on reception.
{
class TestFactories : public FactoryCollectionBase
{
public:
{
add<ComplexFloatBase, TestComplexFloat>(map);
return map;
}
static const FactoryCollectionBaseCleanUp TestFactoriesVar;
};
}
The variable needs to be initalized in the the cpp-file of the Object Factory like this:
Usage
The new data type can be used as if it was one of the types already included in ArmarX:
Variant varComplex(new TestComplexFloat(1, 2));
If you want to receive Variants via Ice (e.g. implement an interface that uses variants), the ObjectFactory-header of the library, where the variant is implemented, needs to be included and the library needs to be linked.
@note If you want to use the variant in the statechart editor, see How to add your own data type to a statechart