How can I make a class that type-erases objects until a function is called on them without specifying the list of possible functions up front?

半世苍凉 提交于 2019-12-12 16:03:32

问题


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 along with the concepts more easily. If you've seen some of Sean Parent's talks, you'll know he came up with a way to abstract polymorphism, allowing code such as this:

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

Notice that there are no pointers or anything. Calling draw on a Drawable will call the appropriate draw function on the contained object without the type of the object being easily accessible. One major downside to this is that similar classes to Drawable have to be written for each task. I'm trying to abstract this a bit so that the function does not have to be known by the class. My current solution is as follows:

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

Here, Draw is a functor with an operator()(Circle) and opeator()(Square), or a generic version. In this way, this is also sort of a visitor pattern implementation. If you wanted to also, say, print the name of each figure, you could do Applicator<Draw, PrintName>. When calling apply, the desired function is chosen.

My implementation works by passing a boost::variant of the callable types to the virtual function and having it visit that variant and call the function within. Overall, I would say this implementation is acceptable, but I haven't yet thought much about allowing any number of parameters or a return type, let alone ones that differ from function to function.

Question

I spent days trying to think of a way to have this work without making Applicator a template. Ideally, the use would be more similar to this. For the sake of simplicity, assume the functions called must have the signature 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 comes down to the fact that the type of the object can only be obtained within a function called on it. Internally, the class uses virtual functions, which means no templates. When apply is called, here's what happens (identical to Sean's talks):

  1. The internal base class's apply is called on a pointer to the base class with the runtime type of a derived class.
  2. The call is dispatched to the derived class, which knows the type of the stored object.

So by the time I have the object, the function to call must be reduced to a single type known within the class that both knows which function to call and takes the object. I cannot for the life of me come up with 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 is to have a type that holds a function call minus the unknown first argument (the stored object). This would need to at least be templated on the type of the callable object. By using Sean Parent's technique, it's easy enough to make a FunctionCall<F> class that can be stored in a GenericFunctionCall, much like a Circle in a Figure. This GenericFunctionCall can be passed into the virtual function, whereas the other cannot.

Attempt 1

  1. apply() is called with a known callable object type.
  2. The type of the callable object is used to create a FunctionCall<Type> and store it as a type-erased GenericFunctionCall.
  3. This GenericFunctionCall object is passed to the virtual apply function.
  4. The derived class gets the call object and has the object to be used as the first argument available.
  5. For the same reason of virtual functions not being allowed to be templates, the GenericFunctionCall could call the necessary function on the right FunctionCall<Type>, but not forward the first (stored object) argument.

Attempt 2

As a continuation of attempt 1:

  1. In order to pass the stored object into the function called on the GenericFunctionCall, the stored object could be type-erased into a GenericObject.
  2. One of two things would be possible:
    • A function is called and given a proper FunctionCall<Type>, but has a GenericObject to give to it, with the type unknown outside of a function called on it. Recall that the function cannot be templated on the function call type.
    • A function is called and given a proper T representing the stored object, but has a GenericFunctionCall to extract the right function call type from. We're back where we started in the derived class's apply function.

Attempt 3

  1. Take the known type of a callable object when calling apply and use it to make something that stores a function that it can call with a known stored object type (like std::function).
  2. Type-erase that into a boost::any and pass it to the virtual function.
  3. Cast it back to the appropriate type when the stored object type is known in the derived class and then pass the object in.
  4. Realize that this whole approach requires the stored object type to be known when calling apply.

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

P.S. I'm open for suggestions on better names than Applicator and apply.


回答1:


This is not possible. Consider a program composed 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);
}

It must be possible for each TU to be translated separately, indeed in causal isolation. But this means that at no point is there a compiler that simultaneously has access to Draw and to Circle, so code for Draw to call Circle::draw can never be generated.



来源:https://stackoverflow.com/questions/26303718/how-can-i-make-a-class-that-type-erases-objects-until-a-function-is-called-on-th

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!