📜 ⬆️ ⬇️

Using boost :: variant to describe model states

In data models, it is often necessary to store certain switchable states. The classic way in C ++ for this is to use enum enum types.

For example, if your user can switch between two screens in your program, you start the enum screen {screen_one, screen_two}; and the screen cur_screen_ variable. The renderer should obtain the “current selected screen” from the model, and then draw it, requesting additional data from the model relating to this particular screen. Sort of:

switch (model.cur_screen())
{
case screen_one:
model.get_screen_one_elements();
...
case screen_two:
model.get_screen_two_elements();
...
}

')
When using such a model, the programmer may request data that is completely irrelevant for the current state. For example, call the get_screen_two_elements () method to get a list of second screen items when the current screen is first. It is good practice to use ASSERT (cur_screen_ == screen_one) asserts in methods that are dependent on a particular screen. This provides some control over the execution time.

But there is a way to provide control over compile time and more explicit separation of states with the help of boost :: variant.


With this approach, screen_one and screen_two are not enum elements, but full-fledged classes. And all the data and methods dependent on this state go into the class of the state.

There is no more get_screen_one_elements () method in the main model, now there is a get_elements () method in the class screen_one. The currently selected screen is stored in a variable of type boost :: variant <screen_one, screen_two>.

class screen_one
{
public :
const std :: vector < screen_one_elements >& get_elements() const
{
return ...;
}
};

class screen_two
{
public :
const std :: vector < screen_two_elements >& get_elements() const
{
return ...;
}
};

class cool_data_model
{
public :
typedef boost :: variant < screen_one, screen_two > screen;

template < typename NewScreenType >
void change_screen( const NewScreenType & new_val)
{
cur_screen_ = new_val;
}

template < typename VisitorType >
VisitorType :: result_type apply_visitor( const VisitorType & visitor)
{
return boost :: apply_visitor(visitor, cur_screen_);
}
private :
screen cur_screen_;
};


For perception (drawing) of such a model, you need to use the visitor mechanism. This is a special functor that defines the bracket operators for each element of a variant. In our case, for each state.

class painter : public boost :: static_visitor <>
{
public :
void operator ()( const screen_one & val_screen)
{
// ,
val_screen.get_elements();
...
}
void operator ()( const screen_two & val_screen)
{
// ,
val_screen.get_elements();
...
}
};

model.apply_visitor(painter());


The class-state stores in itself the data necessary for the time the screen is in the “selected state” and provides all the specific functionality. You do not refer to the functionality of the second screen, if the current is selected first.

In general, the concept of visitors is very similar in spirit to the operation of "measurements" in physics. There is a certain system, we apply a measuring instrument to it. We get the result, and / or change the state of the system.

In addition, it is very convenient to use visitors for language compilers. By parsing we get the AST (tree of elements), then we apply to it various tools for analysis, optimization, and the resulting output.

Source: https://habr.com/ru/post/101114/


All Articles