Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typesafe method for retrieving data of unknown type through interface

TL;DR version:

I am designing a class in C++14 to be generic. Below I describe a design problem, and I would be grateful for a solution to implement what I'm trying, or a suggestion for a redesign.

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

I want an object of type Algo to be able to return a pointer (or even a copy) of a data member from the Business object that it owns. But it can't know the type that Business will want to return. I expect the owner of Algo to know what will come out based on what Business he passed in.

In my C days, I would blow off the type system by passing around void* and casting as needed. But that sort of thing now wreaks to me.

More detail:

So, a sort of pseudo-C++14 implementation of the above situation might look like:

// 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 a std::valarray<float>, but I don't want Algo or the AbstractBusiness interface to have to know that! The creator of b and a should be in charge of knowing what should come out of a.result().

If I am taking a wrong turn in this design, don't hesitate to let me know. I'm a bit green at this point and very open to suggestions.

I've tried... I obviously can't use auto for a virtual method, nor have a template in a virtual class. These are the only things that stood out.

I'm playing with the idea of making a container interface for whatever Business.result() returns, and just passing pointers to abstract type up to Algo.result(). But I'm starting to feel like there may be a better way, so I'm on here begging for suggestions.

like image 607
Timtro Avatar asked Oct 09 '15 20:10

Timtro


2 Answers

You haven't actually described a design problem. You've described some implementation choices that you've gone with and a roadblock you've run into, but we don't know the reasons for the choices.

You tell us that Algo takes ownership of a business via a pointer to a polymorphic interface AbstractBusiness and must provide a getter for that business's data, though it doesn't know the concrete type of that data (because it doesn't know the concrete type of the business).

Neither of these questions have evident answers:-

  1. Why should Algo acquire a business via a polymorphic interface?
  2. Why should Algo provide a getter for the data of its business?

But deciding it must be so leads to the roadblock.

The polymorphic pothole and how to get out it

Q1. leads us to wonder what is the motivation for AbstractBusiness? Platitudinously, it's safe to say you want it to provide a uniform interface for manipulating and querying all sorts of businesses of concrete types that may be determined at runtime.

To be fully fit for that purpose, AbstractBusiness will encapsulate a necessary and sufficient interface for discharging all of the operations and queries on concrete businesses that applications (including but not limited to your own) can reasonably be expected to need. Call that Plan A. What you have discovered is that it isn't fully fit for Plan A. If the application needs at times to manipulate or query "the data" of a business that is represented to it via an AbstractBusiness, then the AbstractBusiness interface needs to provide polymorphic methods to discharge all of those manipulations and queries, and each concrete business class needs to implement them appropriately for the type of data it contains.

Where your AbstractBusiness has the problematic:

?unknownType? result();

you need to code virtual methods that address all of the convincing answers to the question: What might an application want to know about the notional result() , or do to it?

In this light, the suggestion that has been canvassed to introduce another polymorphic interface, AbstractData, ancestral to all of the concrete data types of all the concrete businesses, may be viewed as a suggestion to compensate for the necessary methods that are missing from AbstractBusiness by separately encapsulating them in a rescue abstraction. Better to finish the unfinished AbstractBusiness.

This is all good and scriptural perhaps, but maybe what has actually stopped you from finishing AbstractBusiness already is the perception that the data of BusinessX can be essentially different from that of BusinessY, so that it is impossible to devise a single set of polymorphic methods that is necessary and sufficient to manage both.

If this is the case, it tells you that businesses cannot all be managed through a single abstract interface. AbstractBusiness can't be fully fit for that purpose and, if it has a role, its role can only be to manage polymorphic objects that represent more specialized abstractions, BusinessTypeX, BusinessTypeY, etc., within each of which the variety, if any, of concrete types can be accommodated by a single polymorphic interface.

AbstractBusiness will then present only the interface that is shared by all businesses. It will have no result() at all and a caller who obtains a pointer to AbstractBusiness with the intention of doing something with with the thing returned by BusinessTypeX::result() will proceed by dynamically casting the source pointer to BusinessTypeX *, and calling result() through the target pointer only if its not null.

We still do not know what is the motivation of AbstractBusiness. We've just pursued the fairly plausible thought that you have "textbook" ambitions for it - Plan A - and have either failed to realize that you just haven't finished it, or you have grasped that the diversity of the data you're dealing with prevents you from finishing it per Plan A, and don't have a Plan B. Plan B is: Deepen the polymorphic hierarchy and use dynamic_cast<LowerType *>(HigherType *) to secure safe access to the LowerType interface when it outruns the HigherType one. [1]

The turn of Q2. now. Most likely, the reason for Algo::result() is simply: Because it's the done thing for a class to provide getters that directly answer the client's natural queries, and in this case a natural query is for the data owned by the business that is owned by the Algo. But if the Algo knows its business only as an AbstractBusiness, then it just can't return the data owned by its business, because the reasons already seen mean that AbstractBusiness can't return "the data" to the Algo, or to anything else.

Algo::result() is misconceived identically as AbstractBusiness::result() is misconceived. Given that BusinessXs data and BusinessYs data might need to be queried either through some repertoire of virtual methods that are still TODO in AbstractBusiness (Plan A), or perhaps through methods of BusinessX and BusinessY that are not inherited from AbstractBusiness at all (Plan B), the only query that Algo certainly can and should support with respect to its business is to return the AbstractBusiness pointer through which it owns its business, leaving it to the caller to query through the pointer or downcast it, if they can, to a lower-type interface they want query. Even if it is possible to finish AbstractBusiness per Plan A, the idea that the missing reportoire of methods should all be duplicated in the interface of Algo just so that a caller never has to receive and downcast an AbstractBusiness pointer is uncompelling. Would every type that manages an AbstractBusiness pointer follow suit?

Summarizing thus far, if AbstractBusiness has a good reason to exist, then you need either to finish it per Plan A, and work through the repercussions of so doing, or else curtail it short of attempting to be a sufficient interface for managing all businesses and bolster it with an enriched polymorphic hierarchy that clients negotiate by dynamic casting, per Plan B; and in either case you should be content for Algo and similar jobsworths in the AbstractBusiness trade just to return their AbstractBusiness pointer to clients who have specialized uses for it.

Better than that, don't go there

But the question of whether AbstractBusiness has a good reason to exist is still dangling, and should you find yourself driven to Plan B that in itself will make the question more pointed: when it transpires that an abstract interface, cast as the root class of a single inheritance hierarchy, cannot deliver Plan A then a doubt arises about the wisdom of the architecture it figures it. Dynamic casting to detect and acquire interfaces is a clunky and expensive mode of flow control and especially vexatious when - as you tell us is your situation - a scope that will have to perform the downcasting rigmarole already knows the type it should "get out" is the type it "put in". Do all the types that are imperfectly descended from the root abstraction need to have a single ancestor, for a reason other than uniformity of interface (since it doesn't give them that)? The economies of generic interfaces are an ever-present goal, but is runtime polymorphism the right means, or even one of the right means, to realize them in the context of your project?

In your code-sketch, AbstractBusiness serves no end purpose but to furnish a type that can uniformly fill certain slots in class Algo, with the effect that Algo can operate correctly on any type that exhibits certain traits and behaviours. As sketched, Algos only requirement of a qualifying type is that it shall have a result() method that returns something: it doesn't care what. But the fact that you express Algos requirements upon a qualifying type by specifying that it shall be an AbstractBusiness prohibits it from not caring what is returned by result(): AbstractBusiness cannot do that result() method, although any of its descendants might do.

Suppose in that case that you sack AbstractBusiness from the job of enforcing the generic attributes of types on which Algo can operate and let Algo itself do that instead, by making it a template? - since it looks as if what AbstractBusiness is doing for Algo is serving the purpose of a template parameter but sabotaging that very purpose:

#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 this way of transferring the genericity from AbstractBusiness to Algo, the former is left completely redundant, and so removed. This is a nutshell illustration of how the introduction of templates changed the game of C++ design root and branch, making polymporhic designs obselete for most of their prior applications to the crafting of generic interfaces.

We are working from a sketch of your problem context: perhaps there remain good reasons not in sight for AbstractBusiness to exist. But even if there are, they don't per se constitute reasons for Algo not to be a template or to have any dependency on AbstractBusiness. And perhaps they can one by one be eliminated by similar treatments.

Making Algo into a template still might not be a viable solution for you, but if it isn't then there is essentially more to the problem than we have seen. And anyhow take away this rule of thumb: Templates for generic interfaces; polymorphism for runtime adaptation of an interface's behaviour.


[1] What might look like another plan is to encapulate "the data" of each concrete business in a boost::any or std::experimental::any. But you can probably see straightaway that this is essentially the same as the idea of encapsulating the data in a rescue abstraction, using an off-the shelf Swiss Army abstraction, rather than crafting your own. In either guise, the idea still leaves callers to downcast the abstraction to the type of real interest to find out if that's what they've got, and in that sense is a variant of Plan B.

like image 141
Mike Kinghan Avatar answered Nov 16 '22 07:11

Mike Kinghan


There are several ways to go at this. The easiest way is to not pass the ownership but call Algo by reference:

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

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

If you know all the types that derive from AbstractBusiness you could use the visitor pattern:

First we declare an abstract method accept in AbstractBusiness that takes a BusinessVisitor. This visitor will be responsible to handle the different types and perform an action based on which type it is visiting:

class BusinessVisitor;

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

The BusinessVisitor looks like this:

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 visitor visit and let overload resolution 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 an accept method to Algo as well. This one will simply dispatch 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 specific business type we need to define a visitor that handles this type:

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

  /* ... */ get_result() const;
};

Now we can run Algo and retrieve 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 don't want extract a specific value from Algo but if you want to trigger an action. In that case the action is usually different depending on the business type, thus the whole action can be specified in the visitor.

A different and quite elegant way would be to use a 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 would be to wrap the type in a class derived from a tag class (no methods or data members) and dynamic_cast it to the type you know it contains. Using dynamic_cast it's usually frowned upon but it has it's uses.

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

Note: I dropped the std::move for the argument of the std::unique_ptr constructor, it doesn't do a thing there: The result of the new operation is already an rvalue and moving a pointer is as efficient as copying it.

like image 1
Florian Avatar answered Nov 16 '22 05:11

Florian