Sortafe method to retrieve data of unknown type via interface

TL DR version:

I am designing a class in C ++ 14 to be generic. Below I describe the design problem, and I would appreciate a decision to implement what I am trying, or a proposal for a redesign.

Let's say that the class I'm designing is called Algo . Its constructor is passed a unique_ptr to a type, say Business , that implements the interface (i.e., inherits from a pure virtual class) and does most of the serious work.

I want an object of type Algo be able to return a pointer (or even a copy) of a data item from a Business object that it owns. But he cannot know the type that Business wants to return. I expect the owner of Algo know what will come out, based on what Business he went through.

In my C days, I blew out the type system by going around void * and typing if necessary. But now I like it.

More details:

Thus, the implementation of the pseudo-C ++ 14 situation described above may look like this:

 // perhaps a template here? class AbstractBusiness { . . . public: ?unknownType? result(); }; class Algo { //Could be public if needbe. unique_ptr<AbstractBusiness> concreteBusiness_; public: Algo(std::unique_ptr<AbstractBusiness> concreteBusiness); auto result() {return concreteBusiness_.result();} }; class Business : public AbstractBusiness { . . . public: std::valarray<float> data_; std::valarray<float> result() {return data_;} }; ::: auto b = std::unique_ptr<AbstractBusiness>{std::move(new Business())}; Algo a(std::move(b)); auto myResult = a.result(); 

In this example, myResult will be std::valarray<float> , but I do not want the Algo or AbstractBusiness interface to know this! Creator b and a must be responsible for knowing what should come from a.result() .

If I am wrong about this design, feel free to let me know. I am a little green at the moment and very open to suggestions.

I tried ... I obviously cannot use auto for the virtual method and have no template in the virtual class. These are the only things that stood out.

I play with the idea of ​​creating a container interface for any returns of Business.result() , but just for passing pointers to an abstract type to Algo.result() . But I'm starting to feel that there might be a better way, so I'm here, begging for suggestions.

+6
source share
2 answers

In fact, you did not describe the design problem. You described some of the implementation options that you encountered, and the roadblock that you have but we don’t know the reasons for choosing.

You tell us that Algo takes control of the business with a pointer to the AbstractBusiness polymorphic interface and must provide a getter for the business data, although it does not know the specific type of this data (since it does not know the specific type of business).

None of these questions have explicit answers: -

  • Why would Algo acquire a business through a polymorphic interface?
  • Why Algo provide the recipient with information about its activities?

But the solution to this should be in a way that leads to a checkpoint.

Polymorphic pothole and how to get out

Q1. makes us wonder what is the motivation for AbstractBusiness ? Along with this, it is safe to say that you want it to provide a single interface for manipulating and polling all kinds of specific types that can be determined at run time.

To be fully suitable for this purpose, AbstractBusiness will encapsulate the necessary and sufficient interface to perform all operations and requests in specific enterprises that can reasonably be expected from applications (including, but not limited to). Call this plan A. What you discovered is that it is not completely suitable for plan A. If the application occasionally needs to manipulate or request the “data” of the business presented to it through AbstractBusiness , then the AbstractBusiness interface should provide polymorphic methods for executing of all these manipulations and queries, and each particular business class should implement them accordingly for the type of data it contains.

If your AbstractBusiness has problems:

 ?unknownType? result(); 

you need to encode virtual methods that answer all convincing answers to the question: what can the application know about conditional result() or do with it?

In this light, the proposal, which was tested to introduce another polymorphic interface, AbstractData , the ancestors for all specific data types of all specific enterprises, can be considered as a proposal to compensate for the necessary methods that are absent in AbstractBusiness , separately encapsulating them in the abstraction of salvation. Better finish the unfinished AbstractBusiness .

This is all possible and possible in the Bible, but perhaps what really stopped you from completing AbstractBusiness , there is already the perception that BusinessX data can differ significantly from BusinessY data, so it is impossible to develop one set of polymorphic methods that necessary and sufficient to control both.

If so, then this suggests that business management cannot be carried out using a single abstract interface. AbstractBusiness cannot be fully suitable for this purpose and, if it has a role, its role can only be for managing polymorphic objects that represent more specialized abstractions, BusinessTypeX , BusinessTypeY , etc., in each of which there is diversity, if any specific types can be performed using a single polymorphic interface.

AbstractBusiness will only present an interface that is shared by all enterprises. It will not have result() and the caller who receives a pointer to AbstractBusiness with the intention of doing something with the thing returned by BusinessTypeX::result() will continue by dynamically translating the source pointer to BusinessTypeX * and calling result() through target pointer only if its value is not null.

We still do not know what is the motivation of AbstractBusiness . We simply pursued a rather plausible idea that you have the ambitions of a “textbook” for her - plan A - and either did not realize that you simply did not finish, or you realized that the variety of data that you work with does not allow you complete it on one plan A and not have plan B. Plan B: Extend the polymorphic hierarchy and use dynamic_cast<LowerType *>(HigherType *) to provide secure access to the LowerType interface when it is ahead of HigherType alone. [1]

Turn Q2. Now. Most likely, the reason for Algo::result() is simple: because it is a done thing for the class to provide recipients who directly respond to the customer’s natural requests, in which case the natural request is for data belonging to the Algo owned business. But if Algo knows his business only as AbstractBusiness , then he simply cannot return the data belonging to his business, because already visible reasons mean that AbstractBusiness cannot return the "data" to Algo or anything else.

Algo::result() is incorrectly understood as AbstractBusiness::result() is incorrectly understood. Given that BusinessX and BusinessY data can be requested either through some repertoire of virtual methods that are still TODO in AbstractBusiness (plan A), or perhaps using BusinessX and BusinessY methods that are not inherited from AbstractBusiness at all (Plan B), the only query that Algo , of course, can and should be maintained in respect of his company - is to return a pointer AbstractBusiness through which he owns his business, giving the caller a request through a pointer or lowering it, if they can, to the interface of a lower class, which needed. Even if you can finish AbstractBusiness with Plan A, the idea is that the missing method report should be duplicated in the Algo interface just so that the caller never has to get and drop the AbstractBusiness uncompelling pointer. Will every type that controls the AbstractBusiness pointer follow?

To summarize so far, if AbstractBusiness has good reason to exist, you need to either finish it on one plan A, or work as a result of such actions, or limit it to insufficient to be a sufficient interface for managing all enterprises and strengthening it with an enriched polymorphic The hierarchy maintained by customers through dynamic casting for plan B and in any case, you should be satisfied with Algo and similar jobs in the AbstractBusiness trade in order to return their AbstractBusiness index to customers who specialize in it.

Better not there

But the question of whether AbstractBusiness compelling reason to exist is still dangling, and if you find yourself involved in Plan B, which in itself will make the question more acute: when it turns out that the abstract interface, the root class of a single inheritance hierarchy, cannot deliver plan A, then there is doubt about the wisdom of the architecture that he depicts. Dynamic casting for detecting and receiving interfaces is a clumsy and expensive flow control mode, especially unpleasant when, as you say, this is your situation - the area that will need to perform downcasting rigmarole already knows what type it should "go out" type, which he "invested". Should all types that are of imperfect origin from root abstraction have a single ancestor for a reason different from the uniformity of the interface (since this does not give them)? Saving common interfaces is an ongoing goal, but is run-time polymorphism right or even one of the right ways to implement them in the context of your project?

Your AbstractBusiness code sketch does not use an end goal, but for a type that can evenly fill certain slots in the Algo class, so that Algo can work correctly on any type that has certain traits and behaviors. As stated, Algo only requires a qualification type requirement that it must have a result() method that returns something: it doesn't care. But the fact that you express Algo requirements for a qualification type, indicating that it must be AbstractBusiness , prevents it from caring that result() returned: AbstractBusiness cannot make this result() method, although any of its descendants.

Suppose in this case you put AbstractBusiness out of the job of providing common type attributes that Algo can run on, and let Algo do this instead, creating a template? - since it looks like what AbstractBusiness does for Algo , serves for the template parameter, but sabotages this goal:

 #include <memory> template<class T> class Algo { std::unique_ptr<T> concreteBusiness_; public: explicit Algo(T * concreteBusiness) : concreteBusiness_{concreteBusiness}{}; auto result() { return concreteBusiness_->result(); } }; #include <valarray> #include <algorithm> struct MathBusiness { std::valarray<float> data_{1.1,2.2,3.3}; float result() const { return std::accumulate(std::begin(data_),std::end(data_),0.0); } }; #include <string> struct StringBusiness { std::string data_{"Hello World"}; std::string result() const { return data_; } }; #include <iostream> int main() { Algo<MathBusiness> am{new MathBusiness}; auto ram = am.result(); Algo<StringBusiness> as{new StringBusiness}; auto ras = as.result(); std::cout << ram << '\n' << ras << '\n'; return 0; } 

You see, that in such a way as to transfer general information from AbstractBusiness to Algo , the first one remains completely redundant and therefore deleted. This is a brief illustration of how the introduction of patterns has changed the game of the root and branches of the C ++ design, which makes polymorphic designs inspectors for most of their previous applications for creating common interfaces.

We work from the sketch of your problematic context: there may be good reasons why AbstractBusiness cannot exist. But even if they exist, they alone are not grounds for Algo not to be a template or to have any dependence on AbstractBusiness . And, perhaps, they can be individually eliminated by similar treatment methods.

Creating Algo in a template may still be an impractical solution for you, but if it is not, then the problem is much larger than we saw. And somehow cancel this rule: Templates for common interfaces; polymorphism for adapting interface behavior in real time.


[1] What might look like another plan is to encapsulate the "data" of each specific business in boost::any or std::experimental::any . But you can probably see right away that this is essentially the same as the idea of ​​encapsulating data in an abstraction for salvation using the abbreviation of the Swiss army, rather than creating your own. In any guise, the idea still leaves the defiant to reduce the abstraction to a type of real interest, to find out if this is what they have, and in this sense is a variant of plan B.

+1
source

There are several ways to do this. The easiest way is not to transfer ownership, but to call Algo via the link:

 Business b; Algo(b); auto result = b.get_result(); 

However, sometimes this is not possible. In this case, various options open up that can become quite complex. Let me start with the most versatile and complex:

If you know all the types derived from AbstractBusiness , you can use the visitor template:

First, we declare an abstract accept method in AbstractBusiness , which accepts BusinessVisitor . This visitor will be responsible for handling the various types and perform an action based on what type it is visiting:

 class BusinessVisitor; struct AbstractBusiness { virtual ~AbstractBusiness() = default; virtual void accept(BusinessVisitor&) const = 0; }; 

BusinessVisitor as follows:

 class BusinessOne; class BusinessTwo; struct BusinessVisitor { virtual ~BusinessVisitor() = default; virtual void on_business_one(const BusinessOne&) {}; virtual void on_business_two(const BusinessTwo&) {}; }; 

Some people prefer to call all methods in the visit visitor and allow you to overload the permission to do the rest, but I prefer more explicit names.

 struct BusinessOne { void accept(BusinessVisitor& v) const { v.on_business_one(*this); } }; struct BusinessTwo { void accept(BusinessVisitor& v) const override { v.on_business_two(*this); } }; 

Now we can add the accept method to Algo . This one will just be sent to the contained AbstractBusiness object.

 class Algo { std::unique_ptr<AbstractBusiness> b_; public: Algo(std::unique_ptr<AbstractBusiness> b); void accept(BusinessVisitor& visitor) const override { return b_->accept(visitor); } }; 

To get the result for a certain type of business, we need to determine the visitor who processes this type:

 struct BusinessOneResult : public BusinessVisitor { void on_business_one(const BusinessOne& b) { // save result; } /* ... */ get_result() const; }; 

Now we can run Algo and get the result:

 auto b = std::unique_ptr<AbstractBusiness>(new BusinessOne()); Algo a(std::move(b)); BusinessOneResult visitor; a.accept(visitor); auto result = visitor.get_result(); 

The real power of this approach unfolds if you do not want to extract a specific value from Algo , but if you want to trigger an action. In this case, the action usually differs depending on the type of business, so the whole action can be indicated in the visitor.

Another and pretty elegant way is to use std::future :

 struct Business { std::future</*...*/> get_future_result() { return promise_.get_future(); } void some_method() { // ... promise_.set_value(...); } private: std::promise</*...*/> promise_; }; // Must use Business here (AbstractBusiness doesn't know about the // type of the future). auto b = std::unique_ptr<Business>(new Business()); auto future = b.get_future_result(); Algo a(std::move(b)); auto result = future.get(); 

Another way is to wrap the type in the class derived from the tag class (without methods or data elements), and dynamic_cast to the type that you know contains. Using dynamic_cast, he usually frowned, but he uses it.

std :: any or boost :: any would be another way.

Note. I rejected std::move for the constructor argument std::unique_ptr , it does nothing: the result of the new operation is already rvalue and moving the pointer is as efficient as copying it.

+1
source

All Articles