问题
I have an architecture where virtually everything is a plugin. This architecture underlies a graphic user interface where each plugin is represented by a "surface" (i.e. a UI control through which the user can interact with the plugin). These surfaces are also plugins. Whenever a new plugin is added, a thin host automatically determines which of the available surfaces is the best UI match for it. How that dynamic type matching can be accomplished in C++ is the subject of this question.
At present, the architecture is implemented in C#, relying heavily on reflection, as you'll see. However, I am now in the process of redesigning the whole thing for C++, but since C++ doesn't have reflection (and since I am new to C++) I need some input on how to best replicate this functionality without it.
Here's how it's currently done in C# (simplified & pseudo):
All plugins are descendants of a Plugin class.
Each surface type is tagged with a 'target plugin type', indicating the most specific / deepest descendant of Plugin it is designed for.
The most generic of the surfaces is one that just handles the level of Plugin (just showing a name label, for example). This guarantees that all plugins will have at least one surface capable of representing them on screen, even if it doesn't provide any useful means of interaction.
Let's call this "baseline" surface type for PluginSurface. If - for the sake of brevity here (in reality it's done via Class Attributes) - we use a property on surfaces to indicate their target type, we'd have
PluginSurface.TargetType = typeof(Plugin)
Unless more specific surfaces are added, all plugins would be assigned this generic surface, no matter where they are in the Plugin inheritance hierarchy.
Now, let's say we add a new plugin type called Father, deriving from Plugin:
Father : Plugin
Then we add a few more, to create a little sample hierarchy:
Mother : Plugin
Daughter : Father
Son1 : Mother
Son2 : Mother
Grandson : Son1
At this point, all of these would be represented on screen by the generic PluginSurface. So we create additional surfaces to more specifically handle the functionality of the new plugins (all surfaces descend from PluginSurface):
MotherSurface.TargetType = typeof(Mother)
GrandsonSurface.TargetType = typeof(Grandson)
From this, the effective type matching of plugin-to-surface should be:
Father -> PluginSurface
Daughter -> PluginSurface
Mother -> MotherSurface
Son1 -> MotherSurface
Son2 -> MotherSurface
Grandson -> GrandsonSurface
Ok, so this is the mapping we expect to end up with. Essentially, the matching is implemented as a static function in the host that takes the plugin type and returns an instance of the best-fit-surface:
PluginSurface GetBestFitSurface(Type pluginType)
The function iterates through all the available surfaces and checks their TargetType property against the supplied pluginType. More specifically, it checks whether the TargetType IsAssignableFrom the pluginType. It creates a list of all the positive matches.
It then needs to narrow down this list of candidates to the best fit. For that I do a similar check of assignability, but this time between all the candidates' TargetTypes, checking each one against each of the others, and for each candidate I make a note of how many of the other candidates' TargetTypes its TargetType is assignable to. The candidate whose TargetType can be assigned (i.e. cast) to the most other TargetTypes is the best fit.
What happens later is that this surface becomes a wrapper around the plugin in question, arranging itself on screen to reflect the features of that plugin it "understands", depending of the tightness of the fit. A GrandsonSurface is specifically designed to represent GrandSon, so it will be a complete UI for that plugin. But Son1 and Son2 are "only" getting a MotherSurface, so any features they don't have in common with Mother will not be included in their UI representation (until a more specialized Son interface is made, at which point that will be the best fit etc). Note that this the intended use - normally, the Mother plugin would be abstract and the interface targeting it would really be a Son interface, intended to wrap all of Mother's children. Thus a single surface provides the UI for a whole class of similarly featured plugins.
How can this best be done in C++ without the support of reflection?
I suppose the challenge is to smoke out the C++ analog to C#'s Type.IsAssignableFrom(Type) where both Types are dynamically attributed.. I'm looking in the direction of dynamic_cast and typeid, but so far I haven't gotten a firm grasp on how to go about it. Any tips on architecture, pattern or implementation details would be greatly appreciated.
Edit 1: I think I have solved the main problem here (see my own answer below, which includes a [very] rough C++ implementation), but there might be better patterns. Feel free to rip it apart.
Edit 2: An improved model is presented in a follow-up question found here: “Poor Man's Reflection” (AKA bottom-up reflection) in C++.
回答1:
One option would be to have a function in your Plugin base class:
virtual std::string id() const { return "Plugin"; }
Which derived classes can override, for example Grandson knows its base class is Mother, so might code:
override std::string id() const { return "Grandson " + Mother::id(); }
That way, when you call id() on a plugin you get something back like "Grandson Mother Plugin". You can throw it a std::stringstream ss(id);, us a while (ss >> plugin_id), and seek using the plugin_id as a key in a container of surfaces....
I don't think you'll find a way to use typeid or dynamic_cast as flexibly, as typeid won't tell you base class (*), and dynamic_cast has to know the type it'll test for at compile time. I assume you want to be able to use new plugins, and change some file or other config of id->surface mappings, without editing your code....
(*) If you're prepared to be non-Standard and it matches you portability requirements, C++: using typeinfo to test class inheritance documents a way to get base class typeinfo at run time.
回答2:
After thinking about it, I realized that the following should work:
Since the surface types are already supposed to be tagged with their respective 'target plugin type', I realized that they might as well have that type hard coded in them (as it's not going to change once a surface plugin is compiled. This is basically how the type tagging is done in C# anyway - although there it is done using class Attributes. Duh).
This means that as long as the 'assignability' checking is done inside each surface class (that internally knows what type it is targeting) rather than in a global function (that needs to be told both the source and the target type of the check, hence the problem), it becomes a regular cast operation with a known destination type (i.e. the surface's target plugin type).
I guess the relevant function could be templated, so that the template type T would represent their 'target plugin type' and also the known destination type of the cast.
It could have a function like so:
bool TargetTypeIsAssignableFrom(Plugin* plugin)
and the function would return the result of a regular dynamic_cast (i.e. whether such a cast is successful or not):
dynamic_cast<T>(plugin)
So when the plugin -> surface matching is performed, rather than checking the pluginType against each surface's TargetType in a global function (that would need to be fed both types), each surface is simply queried about their compatibility with the plugin in question, which it tests by attempting a dynamic_cast to its known T - and the ones that return true are considered candidates.
Finally the candidates' TargetTypes are checked against each other to determine the best fit.
Here's a quick and dirty - but functional - illustration of the process (pardon my C++):
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Plugin {
protected:
string _name;
public:
Plugin() { _name = "Plugin"; };
virtual string Name() { return _name; }
};
class Plugin_A : public Plugin {
public:
Plugin_A() { _name = "Plugin_A"; };
};
class Plugin_B : public Plugin_A {
public:
Plugin_B() { _name = "Plugin_B"; };
};
class Plugin_C : public Plugin {
public:
Plugin_C() { _name = "Plugin_C"; };
};
Plugin * Global_Plugin = new Plugin;
Plugin_A * Global_Plugin_A = new Plugin_A;
Plugin_B * Global_Plugin_B = new Plugin_B;
class Surface {
protected:
string _name;
public:
Surface() { _name = "Surface"; };
string Name() { return _name; }
virtual Plugin* TargetType() { return Global_Plugin; }
virtual bool CanHost(Plugin* plugin) { return dynamic_cast<Plugin*>(plugin) != nullptr; } // TargetType: Plugin
};
class Surface_A : public Surface {
public:
Surface_A() { _name = "Surface_A"; };
Plugin* TargetType() { return Global_Plugin_A; }
bool CanHost(Plugin* plugin) { return dynamic_cast<Plugin_A*>(plugin) != nullptr; } // TargetType: Plugin_A
};
class Surface_B : public Surface_A {
public:
Surface_B() { _name = "Surface_B"; };
Plugin* TargetType() { return Global_Plugin_B; }
bool CanHost(Plugin* plugin) { return dynamic_cast<Plugin_B*>(plugin) != nullptr; } // TargetType: Plugin_B
};
vector<Surface*> surfaces;
Surface * GetSurface(Plugin* plugin) {
vector<Surface*> candidates;
cout << "Candidate surfaces for " << plugin->Name() << ":" << endl;
for (auto i = begin(surfaces); i != end(surfaces); ++i) {
if ((*i)->CanHost(plugin)) {
cout << "\t" << (*i)->Name() << endl;
candidates.push_back(*i);
}
}
int bestFit = 0, fit;
Surface * candidate = nullptr;
for (auto i = begin(candidates); i != end(candidates); ++i) {
fit = 0;
for (auto j = begin(candidates); j != end(candidates); ++j) {
if (j == i || !(*j)->CanHost((*i)->TargetType())) continue;
++fit;
}
if (candidate != nullptr && fit <= bestFit) continue;
bestFit = fit;
candidate = *i;
}
cout << "Best fit for " << plugin->Name() << ":" << endl;
cout << "\t" << candidate->Name() << endl;
return candidate;
}
int main() {
Surface * s[3];
s[0] = new Surface;
s[1] = new Surface_A;
s[2] = new Surface_B;
for (int i = 0; i < 3; ++i) {
surfaces.push_back(s[i]);
}
Plugin * p[3];
p[0] = new Plugin_A;
p[1] = new Plugin_B;
p[2] = new Plugin_C;
for (int i = 0; i < 3; ++i) {
GetSurface(p[i]);
cout << endl;
}
for (int i = 0; i < 3; ++i) {
delete p[i];
delete s[i];
}
cin.get();
delete Global_Plugin;
delete Global_Plugin_A;
delete Global_Plugin_B;
return EXIT_SUCCESS;
}
回答3:
Since you come from a language that has reflection and are asking about similar capabilities in C++ and, I suggest you should have a look into "Template Meta Programming" and "Policy based design", "Type traits" and the like and thus pushing these decisions out to compile time, compared to reflection.
In your case, I suggest having a look at tagging the classes appropriately, and making these decisions at compile time using the meta programming techniques offered by std::is_same, std::is_base_of, std::is_convertible, std::enable_if and std::conditional
As you seem to be moving to a major refactoring, more good, in-depth resources on this topic would be practically everything by Alexandrescu, Alexandrescu's book, Di Gennaro's book and David Abrahams's book on these techniques.
I especially found these posts insightful in all these matters to start with...
HTH, since TMP is much fun
来源:https://stackoverflow.com/questions/20988296/best-fit-dynamic-type-matching-for-plugins-in-c