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.
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:-
Algo
acquire a business via a polymorphic interface?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 BusinessX
s data and BusinessY
s 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, Algo
s 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 Algo
s 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With