On declarative programming in C++
Often I face the problem of mapping the parameter space of one API onto the parameter space of another one. Often I see this solved by nested nested nested ... switch statements.
And I was wondering if there would happen to be a library or a technique that allows you to 'declare' the mapping instead of 'program' it.
A trivial example would consist of merging the values of two enumerates into one:
namespace sourceAPI {
struct A { typedef e { A1, A2, A3 } };
struct B { typedef e { B1, B2 } };
}
namespace targetAPI {
struct AB { typedef e { A1B1, A1B2, A2B1, A2B2, A3B1, A3B2 } };
}
In which the mapping is often done like
switch( a ){
case( A::A1 ): switch( b ) {
case( B::B1 ): return A1B1;
case( B::B2 ): return A1B2;
....
}
And this mapping still needs a 'reverse' switch, too.
But I would rather like something 'dense' like
declare( source( A::A1, B::B1 ), target( AB::A1B1 ) );
declare( source( A::A1, B::B2 ), target( AB::A1B2 ) );
....
开发者_JAVA百科
Has anyone seen such a technique or framework or library?
You can use Boost.Bimap, which provides a bidirectional mapping between two types.
It has a bit of runtime overhead (generally, roughly the same amount of overhead you would get by using a pair of std::map
s for this purpose, which isn't a whole lot).
It does allow you to define mappings about as densely as your example, though; generally you just add pairs to the map, one pair at a time.
Using a table driven approach is fine - it's equivalent.
There are two issues that you need to worry about: enum layout changes (which change ordinality) or enum content changes (addition/removal).
Whatever solution you choose, you want to mitigate the problems caused by these two issues.
In my own work, I prefer to use a pattern like this:
TargetAPI ToTargetAPI(SourceAPI source)
{
// ...
}
Where things might nest, I call out to another ToXXXX method.
For enums, my ToXXXX methods are either a single switch or a table lookup (or in some cases an expression transform), and I check input ranges with code that throws (whether it's bounds checking or a default statement in a switch).
To me, the mechanism to translate from one type to another is less important than the engineering that prevents bugs from occurring when API's change by failing hard and failing fast. Think about it this way: would you lose more time typing in a complete switch statement (nested or not) with error checking or tracking down a bug from an enum that is out of range and wasn't checked?
In many cases you can accomplish this with simple lookup tables. Since enumerated types can be cast to integer values, you can use them as the index into an array of enumerated types of a different kind, hence doing quick and easy conversion. It has the pleasant side effect of being about as fast as it's humanly possible to make this sort of thing. Depending upon your use, lookup tables can get rather large (but then, if you're making a switch statement with one case for each enumeration, that would be even larger). Also, if you need bi-directional conversion then you'd have to make 2 lookup tables, one for each direction.
Also, be aware that many compilers can optimize enumerated types down to the minimum data type needed to store every value. There are ways around this (often a compiler flag, or you can just declare a "dummy" value of something like 0xffffffff to force a 32-bit enumeration), but it's worth noting.
If you have non-integer values, you can use maps. The STL includes several varieties, and as somebody else mentioned, boost has a nice one that's bidirectional.
For this particular type of task, you can often cheat a bit, and simply use non-overlapping bit patterns for the two enumerations. For example:
enum A::e a;
enum B::e b;
// ... set values of a and b.
AB::e result = (b << 2) | a;
In this particular case, since A has exactly three members, the result is even a contiguous range of values. When the number of members (of all but one enumeration) is anything but one fewer than a power of two, the result will be non-contiguous though.
I'm pretty sure your question is really intended to be more general than just finding a way to deal with this particular problem though. In fact, I suspect the example is purely hypothetical. Unfortunately, it's hard to guess at what other types of problems you might care about. There are certainly quite a few examples of declarative programming in C++. For a couple of obvious examples, virtually everything that uses Boost Spirit or Boost Xpressive ends up doing at least some declarative programming. For better or worse, however, both of these are devoted to similar problems that happen to be quite a bit different from the ones you seem to care about.
A std::map with a boost::tuple as key.
If you don't mind using "make_tuple" in your declaration, then you already have variable key elements in your declaration for free. You need "make_tuple" to do the actual conversion.
Edit:
Things do become more complicated when ranges or wildcards are needed
精彩评论