template<class Param, class ... Tail>
class jevois::Parameter< Param, Tail ... >
A set of Parameters attached to a component.
This variadic template class is just for the convenience of adding several parameters to a Component in one statement.
The way in which we have implemented Parameter in the JeVois framework may seem unorthodox at first, but is the best way we have found so far in terms of minimizing burden when writing new components with lots of parameters. In our earlier framework, the iLab Neuromorphic Vision Toolkit (iNVT) started in 1995, parameters were included in components as member variables. The burden to programmers was so high that often they just did not include parameters and hardwired values instead, just to avoid that burden. The burden comes from the requirements:
- we want to be able to support parameters of any type
- we want each parameter to have a name, description, default value, specification of valid values
- we want parameters to appear in related groups in the help message
- we want to support callbacks, i.e., functions that are called when one tries to change the parameter value
- we typically want the callback to be a member function of the Component that owns a given parameter, since changing that parameter value will typically trigger some re-organization in that Component (otherwise the callback might not be needed).
Possible implementation using class data members for parameters (similar to what we used in iNVT), here shown for a sample int parameter to specify the size of a queue held in a class MyComp that derives from Component:
ParamDef<int> sizeparamdef("size", "Queue size", 5, Range<int>(1, 100), categ);
{
public:
Param<int> sizeparam;
void sizeParamCallback(int newval) { myqueue.resize(newval); }
MyComp(std::string const & instance) :
sizeparam(sizeparamdef)
{
sizeparam.setCallback(&MyComp::sizeParamCallback);
}
};
So we basically end up with 3 names that people have no idea what to do with and will just use confusing names for (sizeparamdef, sizeparam, sizeParamCallback), and we have to 1) specify the definition of name, description, etc somewhere using some arbitrary name (here sizeparamdef), then add the member variable for the param to the component using some other name (here sizeparam), then construct the param which would typically require linking it to its definition so we can get the default value and such, and finally hook the callback up (note how MyComp is not fully constructed yet when we construct sizeparam hence referencing sizeParamCallback() at that time is dubious at best). In reality, things are even worse since typically the paramdef, component class declaration, and component implementation, should be in 3 different files.
The approach we developed for the Neuromorphic Robotics Toolkit (NRT) and refined for JeVois works as follows:
- each parameter is a unique new class type. We create that type once with one name, and it holds the parameter value and the definition data. This is further facilitated by the JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) variadic macro.
- for parameters with callbacks, their class type includes a pure virtual onParamChange(param, value) function that will need to be implemented by the host component. This is facilitated by the JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) variadic macro. The first argument of onParamChange() is the parameter class type, so that a host component with many parameters will have many different onParamChange() functions, one per parameter that has a callback.
- components inherit from their parameters using variadic templates to make inheriting from multiple parameters short and easy.
- each parameter exposes simple functions get(), set(), etc (see ParameterCore and ParameterBase). In a component that has many parameters, accessing parameters is achieved by disambiguating on which base class (i.e., which parameter) one wants to access the get(), set(), etc function, which is achieved by calling paramx::get() vs paramy::get(), etc
- No need to declare parameter member variables (we inherit from them instead).
- No need to do anything at construction of the component.
- No need to manually hook the callback function in the component host class to the parameter.
- Strong compile-time checking that the programmer did not forget to write the callback function for each parameter that was declared as having a callback.
- Only one name used throughout for that parameter and all its associated machinery (definition, callback).
- It is easy to write scripts that search the source tree for information about all the parameters of a component, since those are always all specified in the Parameter< ... > inheritance statement.
Remaining caveat: it is often desirable to use short names for parameters, such as "size", "length", "dims", etc and those names may clash between several components as the .H files for these components are included when building a more complex component that uses those. This is not an issue for Module, which is a terminal entity and is typically written as a single .C file with no .H file. For components intended for broad use, we currently recommend putting all the parameters in a namespace that is the lowercase version of the component class name.
Below is the resulting implementation in Manager.H. We start with declaring the parameters, and we inherit from them when declaring the Manager class:
{
namespace manager
{
static ParameterCategory const ParamCateg("General Options");
#ifdef JEVOIS_LDEBUG_ENABLE
#else
#endif
LogLevel::info, LogLevel_Values, ParamCateg);
0, ParamCateg);
"JeVois cameras are connected to a same USB bus", "jevois", ParamCateg);
}
class Manager : public Component,
public Parameter<manager::help, manager::loglevel, manager::tracelevel, manager::nickname>
{
public:
For the parameters that we declared as having a callback, we further include in our definition of the Manager class overrides for the pure virtual onParamChange() functions that they added to our manager class. Note the signatures of these functions: The first argument is a const reference to the parameter for which this callback is, and its main role is to disambiguate between the different onParamChange() functions a component may have. The second argument is the proposed new parameter value. The onParamChange() function should examine the candidate new value and (1) if it does not like it, throw and std::range_error with a descriptive message of why the value is rejected, (2) otherwise, it is assumed that the value is accepted and the callback can then allocate resources or do other work with that value (the actual modification of the Parameter object is handled upstream and the callback does not need to worry about it: if it returns without throwing, the proposed value will become the new value of the Parameter). The Parameter is locked-up for writing as long as the callback is running, to avoid destruction of the parameter and/or concurrent parameter value changes by several different threads. Thus, callbacks should try to execute quickly, and should not call set(), etc on the parameter as this will always deadlock (get() is allowed if your callback needs to know the current value of the parameter).
void onParamChange(manager::loglevel
const & param, manager::LogLevel
const & newval)
override;
void onParamChange(manager::tracelevel
const & param,
unsigned int const & newval)
override;
There is nothing to do in the constructor, destructor, etc of Manager. The only thing that remains to be done is to implement the onParamChange() functions in Manager.C. Note how we use the JEVOIS_UNUSED_PARAM(x) macro to avoid compiler warnings about our callbacks not using the "param" function parameter:
jevois::manager::LogLevel const & newval)
{
switch(newval)
{
#ifdef JEVOIS_LDEBUG_ENABLE
#endif
}
}
unsigned int const & newval)
{
if (newval)
{
#if !defined(JEVOIS_TRACE_ENABLE) || !defined(JEVOIS_LDEBUG_ENABLE)
LERROR(
"Debug trace has been disabled at compile-time, re-compile with -DJEVOIS_LDEBUG_ENABLE=ON and "
"-DJEVOIS_TRACE_ENABLE=ON to see trace info");
#endif
}
}
For completeness, if you wonder what JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) and JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) exactly do, here they are in ParameterHelpers.H and reproduced here:
#ifndef JEVOIS_DOXYGEN
#define JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) \
struct ParamName : public virtual jevois::ParameterRegistry, public jevois::ParameterCore<ParamType> \
{ \
typedef ParamType type; \
\
inline ParamName() : jevois::ParameterCore<ParamType>(jevois::ParameterDef<ParamType>(#ParamName, __VA_ARGS__)) \
{ jevois::ParameterRegistry::addParameter(this); } \
\
inline virtual ~ParamName() \
{ jevois::ParameterRegistry::removeParameter(this); } \
\
inline virtual jevois::Component const * owner() const override \
{ return dynamic_cast<jevois::Component const *>(static_cast<jevois::ParameterRegistry const *>(this)); } \
};
#define JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) \
struct ParamName : public virtual jevois::ParameterRegistry, public jevois::ParameterCore<ParamType> \
{ \
typedef ParamType type; \
\
virtual void onParamChange(ParamName const & param, ParamType const & newval) = 0; \
\
inline ParamName() : jevois::ParameterCore<ParamType>(jevois::ParameterDef<ParamType>(#ParamName, __VA_ARGS__)) \
{ setCallback([this](ParamType const & newval) { this->onParamChange(*this, newval); }); \
jevois::ParameterRegistry::addParameter(this); } \
\
inline virtual ~ParamName() \
{ jevois::ParameterRegistry::removeParameter(this); } \
\
inline virtual jevois::Component const * owner() const override \
{ return dynamic_cast<jevois::Component const *>(static_cast<jevois::ParameterRegistry const *>(this)); } \
};
#endif
Also see Engine.H or the many components in the jevoisbase library.
Definition at line 382 of file Parameter.H.