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.

+3


source to share


2 answers


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.

0


source


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

0


source







All Articles