C++ Math Parser with user-defined function

2019-01-19 09:29发布

I want to implement a math parser with user-defined function. There are several problems to be solved. For example, int eg(int a,int b){return a+b;} is the function I want to add to the parser. First: How to store all the functions into a container? std::map<std::string,boost::any> func_map may be a choose (by func_map["eg"]=eg". However, It's very hard to call the function in this kind of map, for I have to use any_cast<T> to get the real function from the wrapper of boost::any.

Second: How to handle the overloaded function? It's true that I can distinguish the overloaded functions by the method of typeid, but it's far from a real implementation.

Parsering expressions is not a difficult skill and the hardest part is described above.

muparserx provides an interesting solution for this problem, but I'm finding another method.

I'm not familiar with lambda expressions but may be it's an acceptable way.

Update: I need something like this:

int eg(int a,int b){ return a+b;}
int eg(int a,int b, string c){return a+b+c.length();}
double eh(string a){return length.size()/double(2);}
int main(){
    multimap<string,PACKED_FUNC> func_map;
    func_map.insert(make_pair("eg",pack_function<int,int>(eg));
    func_map.insert(make_pair("eg",pack_function<int,int,string>(eg));
    func_map.insert(make_pair("eh",pack_function<string>(eh));
    auto p1=make_tuple(1,2);
    int result1=apply("eg",PACK_TUPLE(p1));//result1=3
    auto p2=tuple_cat(p1,make_tuple("test"));
    int result2=apply("eg",PACK_TUPLE(p2));//result2=7
    auto p3=make_tuple("testagain");
    double result3=apply("eh",PACK_TUPLE(p3));//result3=4.5
    return 0;
}

1条回答
Fickle 薄情
2楼-- · 2019-01-19 09:41

How to store all the functions into a container?

To store then inside some container, they must be of the same type. The std::function wrapper is a good choice, since this allows you to use even stateful function objects. Since you probably don't want all functions to take the same number of arguments, you need to "extract" the arity of the functions from the static host type system. An easy solution is to use functions that accept a std::vector:

// Arguments type to the function "interface"
using Arguments = std::vector<int> const &;
// the interface
using Function = std::function<int (Arguments)>;

But you don't want your users to write functions that have to unpack their arguments manually, so it's sensible to automate that.

// Base case of packing a function.
// If it's taking a vector and no more
// arguments, then there's nothing left to
// pack.
template<
  std::size_t N,
  typename Fn>
Function pack(Fn && fn) {
 return
  [fn = std::forward<decltype(fn)>(fn)]
  (Arguments arguments)
  {
   if (N != arguments.size()) {
    throw
      std::string{"wrong number of arguments, expected "} +
      std::to_string(N) +
      std::string{" but got "} +
      std::to_string(arguments.size());
   }
   return fn(arguments);
  };
}

The above code handles the easy case: A function that already accepts a vector. For all other functions they need to be wrapped and packed into a newly created function. Doing this one argument a time makes this relatively easy:

// pack a function to a function that takes
// it's arguments from a vector, one argument after
// the other.
template<
  std::size_t N,
  typename Arg,
  typename... Args,
  typename Fn>
Function pack(Fn && fn) {
 return pack<N+1, Args...>(
  [fn = std::forward<decltype(fn)>(fn)]
  (Arguments arguments, Args const &... args)
  {
   return fn(
     arguments,
     arguments[N],
     args...);
  });
}

The above only works with (special) functions that already take a vector. For normal functions we need an function to turn them into such special functions:

// transform a function into one that takes its
// arguments from a vector
template<
  typename... Args,
  typename Fn>
Function pack_function(Fn && fn) {
 return pack<0, Args...>(
  [fn = std::forward<decltype(fn)>(fn)]
  (Arguments arguments, Args const &... args)
  {
   return fn(args...);
  });
}

Using this, you can pack any function up to be the same type:

Function fn =
  pack_function<int, int>([] (auto lhs, auto rhs) {return lhs - rhs;});

You can then have them in a map, and call them using some vector, parsed from some input:

int main(int, char**) {
 std::map<std::string, Function> operations;
 operations ["add"] = pack_function<int, int>(add);
 operations ["sub"] = pack_function<int, int>(
   [](auto lhs, auto rhs) { return lhs - rhs;});
 operations ["sum"] = [] (auto summands) {
   int result = 0;
   for (auto e : summands) {
    result += e;
   }
   return result;
  };
 std::string line;
 while (std::getline(std::cin, line)) {
  std::istringstream command{line};
  std::string operation;
  command >> operation;
  std::vector<int> arguments {
    std::istream_iterator<int>{command},
    std::istream_iterator<int>{} };
  auto function = operations.find(operation);
  if (function != operations.end ()) {
   std::cout << line << " = ";
   try {
    std::cout << function->second(arguments);
   } catch (std::string const & error) {
    std::cout << error;
   }
   std::cout << std::endl;
  }
 }
 return 0;
}

A live demo of the above code is here.

How to handle the overloaded function? It's true that I can distinguish the overloaded functions by the method of typeid, but it's far from a real implementation.

As you see, you don't need to, if you pack the relevant information into the function. Btw, typeid shouldn't be used for anything but diagnostics, as it's not guaranteed to return different strings with different types.

Now, finally, to handle functions that don't only take a different number of arguments, but also differ in the types of their arguments, you need to unify those types into a single one. That's normally called a "sum type", and very easy to achieve in languages like Haskell:

data Sum = IVal Int | SVal String
-- A value of type Sum is either an Int or a String

In C++ this is a lot harder to achieve, but a simple sketch could look such:

struct Base {
  virtual ~Base() = 0;
};
inline Base::~Base() {}

template<typename Target>
struct Storage : public Base {
  Target value;
};

struct Any {
  std::unique_ptr<Base const> value;
  template<typename Target>
  Target const & as(void) const {
    return
      dynamic_cast<Storage<Target> const &>(*value).value;
   }
};

template<typename Target>
auto make_any(Target && value) {
  return Any{std::make_unique<Storage<Target>>(value)};
}

But this is only a rough sketch, since there's boost::any which should work perfectly for this case. Note that the above and also boost::any are not quite like a real sum type (they can be any type, not just one from a given selection), but that shouldn't matter in your case.

I hope this gets you started :)


Since you had problems adding multi type support I expanded a bit on the above sketch and got it working. The code is far from being production ready, though: I'm throwing strings around and don't talk to me about perfect forwarding :D

The main change to the above Any class is the use of a shared pointer instead of a unique one. This is only because it saved me from writing copy and move constructors and assignment operators.

Apart from that I added a member function to be able to print an Any value to a stream and added the respective operator:

struct Base {
  virtual ~Base() = 0;
  virtual void print_to(std::ostream &) const = 0;
};
inline Base::~Base() {}

template<typename Target>
struct Storage : public Base {
  Target value;
  Storage (Target t) // screw perfect forwarding :D
   : value(std::forward<Target>(t)) {}

  void print_to(std::ostream & stream) const {
    stream << value;
  }
};

struct Any {
  std::shared_ptr<Base const> value;

  template<typename Target>
  Target const & as(void) const {
    return
      dynamic_cast<Storage<Target> const &>(*value).value;
   }
   template<typename T>
   operator T const &(void) const {
     return as<T>();
  }
   friend std::ostream & operator<<(std::ostream& stream, Any const & thing) {
     thing.value->print_to(stream);
     return stream;
   }
};

template<typename Target>
Any make_any(Target && value) {
  return Any{std::make_shared<Storage<typename std::remove_reference<Target>::type> const>(std::forward<Target>(value))};
}

I also wrote a small "parsing" function which shows how to turn a raw literal into an Any value containing (in this case) either an integer, a double or a string value:

Any parse_literal(std::string const & literal) {
  try {
    std::size_t next;
    auto integer = std::stoi(literal, & next);
    if (next == literal.size()) {
      return make_any (integer);
    }
    auto floating = std::stod(literal, & next);
    if (next == literal. size()) {
      return make_any (floating);
    }
  } catch (std::invalid_argument const &) {}
  // not very sensible, string literals should better be
  // enclosed in some form of quotes, but that's the
  // job of the parser
  return make_any<std:: string> (std::string{literal});
}

std::istream & operator>>(std::istream & stream, Any & thing) {
  std::string raw;
  if (stream >> raw) {
    thing = parse_literal (raw);
  }
  return stream;
}

By also providing operator>> it's possible to keep using istream_iterators for input.

The packing functions (or more precisely the functions returned by them) are also modified: When passing an element from the arguments vector to the next function, an conversion from Any to the respective argument type is performed. This may also fail, in which case a std::bad_cast is caught and an informative message rethrown. The innermost function (the lambda created inside pack_function) wraps its result into an make_any call.

add 5 4 = 9
sub 3 2 = 1
add 1 2 3 = wrong number of arguments, expected 2 but got 3
add 4 = wrong number of arguments, expected 2 but got 1
sum 1 2 3 4 = 10
sum = 0
sub 3 1.5 = argument 1 has wrong type 
addf 3 3.4 = argument 0 has wrong type 
addf 3.0 3.4 = 6.4
hi Pete = Hello Pete, how are you?

An example similar to the previous one can be found here. I need to add that this Any type doesn't support implicit type conversions, so when you have an Any with an int stored, you cannot pass that to an function expecting a double. Though this can be implemented (by manually providing a lot of conversion rules).

But I also saw your update, so I took that code and applied the necessary modifications to run with my presented solution:

Any apply (multimap<string, Function> const & map, string const & name, Arguments arguments) {
 auto range = map.equal_range(name);
 for (auto function = range.first;
      function != range.second;
      ++function) {
  try {
   return (function->second)(arguments);
  } catch (string const &) {}
 }
 throw string {" no such function "};
}


int eg(int a,int b){ return a+b;}
int eg(int a,int b, string c){return a+b+c.length();}
double eh(string a){return a.size()/double(2);}
int main(){
 multimap<string, Function> func_map;
 func_map.insert(make_pair(
   "eg",pack_function<int,int>(
     static_cast<int(*)(int, int)>(&eg))));
 func_map.insert(make_pair(
   "eg",pack_function<int,int,string>(
     static_cast<int (*)(int, int, string)>(&eg))));
 func_map.insert(make_pair(
   "eh",pack_function<string>(eh)));

 // auto p1=make_tuple(1,2);
 // if you want tuples, just write a
 // function to covert them to a vector
 // of Any.
 Arguments p1 =
   {make_any (1), make_any (2)};
 int result1 =
   apply(func_map, "eg", p1).as<int>();

 vector<Any> p2{p1};
 p2.push_back(make_any<string> ("test"));
 int result2 =
   apply(func_map, "eg", p2).as<int>();


 Arguments p3 = {make_any<string>("testagain")};
 double result3 =
   apply(func_map, "eh", p3).as<double>();


 cout << result1 << endl;
 cout << result2 << endl;
 cout << result3 << endl;
 return 0;
}

It doesn't use tuples, but you could write a (template recursive) function to access each element of a tuple, wrap it into an Any and pack it inside a vector.

Also I'm not sure why the implicit conversion from Any doesn't work when initialising the result variables.


Hm, converting it to use boost::any shouldn't be that difficult. First, the make_any would just use boost::any's constructor:

template<typename T>
boost::any make_any(T&& value) {
  return boost::any{std::forward<T>(value)};
}

In the pack function, the only thing that I'd guess needs to be changed is the "extraction" of the correct type from the current element in the arguments vector. Currently this is as simple as arguments.at(N), relying on implicit conversion to the required type. Since boost::any doesn't support implicit conversion, you need to use boost::any_cast to get to the underlying value:

template<
  std::size_t N,
  typename Arg,
  typename... Args,
  typename Fn>
Function pack(Fn && fn) {
 return pack<N+1, Args...>(
  [fn = std::forward<decltype(fn)>(fn)]
  (Arguments arguments, Args const &... args)
  {
   try {
    return fn(
      arguments,
      boost::any_cast<Arg>(arguments.at(N)),
      args...);
   } catch (boost::bad_any_cast const &) { // throws different type of exception
    throw std::string{"argument "} + std::to_string (N) +
      std::string{" has wrong type "};
   }
  });
}

And of course, if you use it like in the example you provided you also need to use boost::any_cast to access the result value.

This should (in theory) do it, eventually you need to add some std::remove_reference "magic" to the template parameter of the boost::any_cast calls, but I doubt that this is neccessary. (typename std::remove_reference<T>::type instead of just T)

Though I currently cannot test any of the above.

查看更多
登录 后发表回答