How can I create a class that erases objects until a function is called on them without giving the list of possible functions in front?

Background

The title probably sounds confusing, so let me explain. First of all, here is a minimal version of my implementation so you can follow the concepts more easily. If you've seen some of Sean Rodin's conversations, you'll realize that he came up with a way to abstract polymorphism by allowing code like this:

std::vector<Drawable> figures{Circle{}, Square{}};
for (auto &&figure : figures) {draw(figure);}

      

Note that there are no pointers or anything else. Calling draw

to Drawable

will call the appropriate function draw

on the contained object without the object's type being readily available. One of the major disadvantages of this is that similar classes must be written for each task Drawable

. I'm trying to distract this a bit so that the function doesn't need to be known to the class. My current solution looks like this:

std::vector<Applicator<Draw>> figures{Circle{}, Square{}};
for (auto &&figure : figures) {figure.apply(Draw{});}

      

Here draw

is a functor with operator()(Circle)

and opeator()(Square)

, or a generic version. So it is also a kind of implementation of the visitor pattern. If you want to print the name of each shape, for example, you can do Applicator<Draw, PrintName>

. When called apply

, the desired function is selected.

My implementation works by passing boost::variant

callable types to a virtual function and forcing it to visit that variant and call the function inside. Overall, I would say that this implementation is acceptable, but I haven't thought much about allowing any number of parameters or return type yet, let alone those that differ from function to function.

Question

I spent several days trying to figure out a way to get this working without creating a Applicator

template. Ideally, the usage would be more like this. For the sake of simplicity, let's say the functions called must be signed void(ObjectType)

.

//For added type strictness, I could make this Applicator<Figure> and have 
//using Figure<struct Circle> = Circle; etc
std::vector<Applicator> figures{Circle{}, Square{}}; 
for (auto &&figure : figures) {figure.apply(Draw{});} //or .apply(draw); if I can

      

The problem usually boils down to the fact that the type of an object can only be obtained within the limits of the function called on it. Internally, the class uses virtual functions, which means no templates. When called apply

, this is what happens (identical to Sean's conversations):

  • The application of an inner base class is invoked by a pointer to a base class with the runtime type of the derived class.
  • The call is sent to a derived class that knows the type of the stored object.

So, by the time I have an object, the calling function should be reduced to a single type known in the class that knows which function the object is calling and accepting. I can't for the life of me think of a way to do this.

Attempts

Here are a couple of failed attempts, so you can see why I find this difficult:

The premise for both of the first two must be of a type that contains the function call minus the unknown first argument (the stored object). This is required at least for the type template of the called object. Using Sean's Parent technique, it is easy enough to make a class FunctionCall<F>

that can be saved in GenericFunctionCall

as well as Circle

in Figure

. This one GenericFunctionCall

can be passed to a virtual function whereas the other cannot.

Attempt 1

  • apply()

    called with a known callable object type.
  • The type of the called object is used for creation FunctionCall<Type>

    and stores it as an erasable type GenericFunctionCall

    .
  • This object GenericFunctionCall

    is passed to the virtual function apply

    .
  • The resulting class receives a call object and has an object to be used as the first available argument.
  • For the same reason that virtual functions cannot be templates, it GenericFunctionCall

    can call the desired function on the right FunctionCall<Type>

    , but not pass the first (stored object) argument.

Attempt 2

As a follow-up to attempt 1:

  • To pass a stored object to a function called in GenericFunctionCall

    , the stored object can be erased using erase in GenericObject

    .
  • One of two things is possible:
    • A function is called and its own is set FunctionCall<Type>

      , but it is given GenericObject

      with a type that is unknown outside the function called on it. Recall that a function cannot be programmed for the type of function call.
    • The function is called and assigned the correct one T

      that represents the stored object, but has the GenericFunctionCall

      correct type of function call to retrieve. We're back to where the derived class started to function apply

      .

Attempt 3

  • Take a known type of callable object when invoked apply

    and use it to create something that stores a function that it can call using a known type of stored object (for example std::function

    ).
  • Enter erase in boost::any

    and pass it to the virtual function.
  • Revert it to the appropriate type when the type of the stored object is known in the derived class, and then pass the object.
  • Understand that this entire approach requires the type of the stored object to be known when called apply

    .

Any bright ideas on how to turn this class into one that doesn't need template arguments, but rather can take any callable object and call it with the stored object?

PS I'm open to suggestions for better names than Applicator

and apply

.

+3


source to share


1 answer


It's impossible. Consider a program consisting of three translation units:

// tu1.cpp
void populate(std::vector<Applicator>& figures) {
  figures.push_back(Circle{});
  figures.push_back(Square{});
}

// tu2.cpp
void draw(std::vector<Applicator>& figures) {
  for (auto &&figure : figures) { figure.apply(Draw{}); }
}

// tu3.cpp
void combine() {
  std::vector<Applicator>& figures;
  populate(figures);
  draw(figures);
}

      



For each technical specification it should be possible to separately translate, indeed, into causal isolation. But this means that there is by no means a compiler that has access to Draw

and at the same time Circle

, so the code Draw

to call can Circle::draw

never be generated.

+4


source







All Articles