Can I create functions and types dynamically in C ++?
I am trying to implement a filter graph type. This graphics filter is based on a mathematical algorithm I have implemented that simply defines a set of functors that take some input and produce some type of output. I have implemented each of these functors as separate C ++ classes, with a common base type that allows functors to be combined.
Below is a very simple implementation:
#include <iostream>
#include <cassert>
#include <cctype>
// Simple filter that takes a string and returns some new type
template <typename A>
struct Filter {
virtual A filter(const std::string& input) = 0;
};
// Takes a single char, returns that same char
struct SingleChar : public Filter<char> {
char filter(const std::string& input) {
assert(input.size() == 1);
return input[0];
}
};
// Takes a string, returns a pair of data types
template <typename A, typename B>
struct Sequence : public Filter<std::pair<A, B>> {
Filter<A>* left;
Filter<B>* right;
std::pair<A, B> filter(const std::string& input) {
assert(input.size() > 1);
return std::make_pair(left->filter(input.substr(0, 1)), right->filter(input.substr(1)));
}
};
template <typename B, typename A>
struct Transform : public Filter<A> {
Filter<B>* innerFilter;
std::function<A(B)> transform;
A filter(const std::string& input) {
return transform(innerFilter->filter(input));
}
};
// Simple helper function to join two strings with a space
std::string joinStringPair(std::pair<std::string, std::string> pair) {
return pair.first + ' ' + pair.second;
}
int main() {
// Takes a single char, returns that same char (i.e. "A" -> "A")
SingleChar singleLetter;
// Takes a single char, returns that same char + it lower-case version (i.e. "A" -> "Aa")
Transform<char, std::string> letterAndLower;
letterAndLower.innerFilter = &singleLetter;
letterAndLower.transform = [](char c){ return std::string(1, c) + std::string(1, std::tolower(c)); };
// Takes two chars, returns each one + its lower-case version (i.e. "AB" -> "Aa", "Bb")
Sequence<std::string, std::string> twoLetterPair;
twoLetterPair.left = &letterAndLower;
twoLetterPair.right = &letterAndLower;
// Takes two chars, returns them and their lower-case versions joined with a space (i.e. "AB" -> "Aa Bb")
Transform<std::pair<std::string, std::string>, std::string> twoLetterString;
twoLetterString.innerFilter = &twoLetterPair;
twoLetterString.transform = joinStringPair;
// Takes three chars, returns each one + its lower-case version and space-joins the last two (i.e. "ABC" -> "Aa", "Bb Cc")
Sequence<std::string, std::string> threeLetterPair;
threeLetterPair.left = &letterAndLower;
threeLetterPair.right = &twoLetterString;
// Takes three chars, returns them and their lower-case versions joined with a space (i.e. "ABC" -> "Aa Bb Cc")
Transform<std::pair<std::string, std::string>, std::string> threeLetterString;
threeLetterString.innerFilter = &threeLetterPair;
threeLetterString.transform = joinStringPair;
// Outputs "Aa Bb Cc"
std::cout << threeLetterString.filter("ABC") << std::endl;
// Outputs "Xx Yy Zz"
std::cout << threeLetterString.filter("XYZ") << std::endl;
}
The above simple example is all hardcoded. A real implementation dynamically creates this filter graph. Consider the following simple example that plots a filter at runtime based on a dynamically supplied argument. Yes, this is trivial, but it illustrates that polymorphism abstracts the exact data types (for example, the return Transform
innerFiler
can be SingleChar
or Sequence
). Hopefully this example makes it easier to represent a more complex process that creates a graph with node types that will be different from different user inputs (and includes many more types than just char
and string
).
Filter<std::string>* makeStringFilter(int stringLength) {
// (ignore the memory leaks with not explicitly deleting allocated memory;
// I'm ignoring memory leaks in this naive example for simplicity sake)
if (stringLength == 1) {
// Take a single char, transform it to a string
Transform<char, std::string>* transform = new Transform<char, std::string>;
transform->innerFilter = new SingleChar;
transform->transform = [](char c){ return std::string(1, c); };
return transform;
}
// Take a char and string pair (like a car, cdr pair in Lisp)
Sequence<char, std::string>* sequence = new Sequence<char, std::string>;
sequence->left = new SingleChar;
sequence->right = makeStringFilter(stringLength - 1);
// Turn the pair into a proper string
Transform<std::pair<char, std::string>, std::string>* transform = new Transform<std::pair<char, std::string>, std::string>;
transform->innerFilter = sequence;
transform->transform = [](std::pair<char, std::string> pair){ return pair.first + pair.second; };
return transform;
}
// Using the above function; outputs "Hello world!" (fails on strings with length != 12)
Filter<std::string>* sixLetterString = makeStringFilter(12);
std::cout << sixLetterString->filter("Hello world!") << std::endl;
As part of my program, I collect, move and manipulate these graphs a lot. In practice, this process creates many nested nodes Transform
(especially when manipulating the graph multiple times), so the graph looks like this:
... -> Transform -> Transform -> Transform -> ... -> Transform -> SingleChar
All these nested ones Transform
make the graph grow quite large, which increases the traversal and manipulation time. Ideally, I would like to concatenate all of these nested Transform
together into one transform node (so the graph is simple ... -> Transform -> SingleChar
). This can be done by creating a new Transform
node that simply concatenates all Transform
s' functions Transform::transform
and points directly to the latter SingleChar
.
However, I ran into problems with static typing and C ++ compression of these Transform
s. In a dynamically typed language, compression is easy because I can just compose Transform
and all types will work at runtime. But getting to print in C ++ is a headache.
The reason is that innerFilter
for Transform
is just a polymorphic pointer. If I have a Transform<B, A>
c innerFilter
that points to Transform<C, B>
, it innerFilter
just has a polymorphic type Filter<B>
. To compress those two Transform
s, I need to create a new type conversion Transform<C, A>
. The problem is that the type has C
been "erased" by polymorphism; I only have types A
and B
.
Can these transforms be compressed? I've looked into type erasure but it doesn't seem like a solution. Template polymorphic functions are (understandably) not legal in C ++. Static polymorphism (à la CRTP ) is not useful here because I build, move and process these filter plots at runtime depending on user input.
I'm willing to completely rethink the implementation to get this to work. The exact implementation is not fixed; however, it must have the same general functionality and type safety as this implementation. My guess is that a new implementation is necessary (if at all possible) since you cannot dynamically create new types at runtime in C ++, which would presumably require nesting conversions in that implementation to compact.
source to share
I think it works. First, create a class that can combine any 2 Transforms:
// First transform takes and A and returns a B, second takes a B and returns a C
template<A, B, C>
struct TwoTransform {
std::function<B(A)> t1;
std::function<C(B)> t2;
TwoTransform(std::function<B(A)> t1, std::function<C(B)> t2) {
this.t1 = t1;
this.t2 = t2;
}
C filter(A input) {
return t2(t1(input));
}
}
Then you can create a method for your TwoTransform that takes another Transform and returns another TwoTransform:
template<A, B, C>
struct TwoTransform {
// Same code as above
TwoTransform<A, C, D> addAnother(std::function<D(C)> nextOne) {
return new TwoTransform(this, nextOne);
}
}
So, you can use it like:
Transform<A, B> t1;
Transofrm<B, C> t2;
Transform<C, D> t3;
Transform<D, E> t4;
Transform<A, E> final = new TwoTransform(t1, t2).addAnother(t3).addAnother(t4);
Please note: I've been writing Java lately and am not trying above; I have a suspicion that I have used Java syntax instead of C ++ in a few places, but hopefully you get the general idea.
source to share
You are probably using expression templates, people like Todd Veldhuizen invented it to dump vector and matrix instructions.
Read his article http://ubietylab.net/ubigraph/content/Papers/pdf/CppTechniques.pdf , especially chapters 1.9 and 1.10
source to share