带有用户定义函数的 C++ Math Parser

Yij*_*uan 2 c++ parsing function-pointers

我想用用户定义的函数实现一个数学解析器。有几个问题需要解决。例如,int eg(int a,int b){return a+b;}是我想添加到解析器的函数。第一:如何将所有的函数存储到一个容器中? std::map<std::string,boost::any> func_map可能是一个选择(通过func_map["eg"]=eg“。但是,在这种地图中调用函数非常困难,因为我必须使用any_cast<T>boost::any.

第二:如何处理重载的函数?确实可以通过 的方法来区分重载的函数typeid,但是离真正的实现还差得很远。

解析表达式并不是一项难的技能,最难的部分已经在上面描述过了。

muparserx 为这个问题提供了一个有趣的解决方案,但我正在寻找另一种方法。

我不熟悉 lambda 表达式,但它可能是一种可以接受的方式。

更新:我需要这样的东西:

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;
}
Run Code Online (Sandbox Code Playgroud)

Dan*_*our 5

如何将所有功能存储到容器中?

要存储在某个容器中,它们必须是相同的类型。该std::function包装是一个不错的选择,因为这可以让你使用,即使有状态的函数对象。由于您可能不希望所有函数都采用相同数量的参数,因此您需要从静态主机类型系统中“提取”函数的数量。一个简单的解决方案是使用接受 a 的函数std::vector

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

但是您不希望您的用户编写必须手动解包其参数的函数,因此将其自动化是明智的。

// 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);
  };
}
Run Code Online (Sandbox Code Playgroud)

上面的代码处理简单的情况:一个已经接受向量的函数。对于所有其他函数,它们需要被包装并打包到一个新创建的函数中。一次做这个论点使这相对容易:

// 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...);
  });
}
Run Code Online (Sandbox Code Playgroud)

以上仅适用于已经采用向量的(特殊)函数。对于普通函数,我们需要一个函数将它们变成这样的特殊函数:

// 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...);
  });
}
Run Code Online (Sandbox Code Playgroud)

使用它,您可以将任何函数打包为相同类型:

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

然后,您可以将它们放在地图中,并使用从某个输入解析的某个向量来调用它们:

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;
}
Run Code Online (Sandbox Code Playgroud)

一个上面的代码的现场演示是在这里

如何处理重载的函数?确实可以通过typeid的方法来区分重载的函数,但是离真正的实现还差得很远。

如您所见,如果将相关信息打包到函数中,则不需要。顺便说一句,typeid除了诊断之外,不应该用于任何其他用途,因为它不能保证返回不同类型的不同字符串。

现在,最后,要处理不仅采用不同数量的参数,而且参数类型也不同的函数,您需要将这些类型统一为一个类型。这通常称为“总和类型”,并且在 Haskell 等语言中很容易实现:

data Sum = IVal Int | SVal String
-- A value of type Sum is either an Int or a String
Run Code Online (Sandbox Code Playgroud)

在 C++ 中,这很难实现,但一个简单的草图可能看起来像这样:

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)};
}
Run Code Online (Sandbox Code Playgroud)

但这只是一个粗略的草图,因为boost::any应该非常适合这种情况。请注意,上述内容和 boost::any 不太像真正的总和类型(它们可以是任何类型,而不仅仅是来自给定选择的类型),但这对您来说无关紧要。

我希望这能让你开始:)


由于您在添加多类型支持时遇到问题,因此我对上面的草图进行了一些扩展并使其正常工作。不过,代码还远未准备好生产:我在四处乱扔字符串,不要跟我谈论完美转发:D

上述Any类的主要变化是使用共享指针而不是唯一指针。这只是因为它使我免于编写复制和移动构造函数以及赋值运算符。

除此之外,我添加了一个成员函数以便能够将Any值打印到流并添加相应的运算符:

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))};
}
Run Code Online (Sandbox Code Playgroud)

我还编写了一个小的“解析”函数,它展示了如何将原始文字转换为Any包含(在本例中)整数、双精度值或字符串值的值:

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;
}
Run Code Online (Sandbox Code Playgroud)

通过还提供operator>>可以继续使用istream_iterators 进行输入。

打包函数(或更准确地说是它们返回的函数)也被修改:当将一个元素从参数向量传递到下一个函数时,Any会执行从到相应参数类型的转换。这也可能失败,在这种情况下 astd::bad_cast被捕获并重新抛出信息性消息。最里面的函数(在 内部创建的 lambda pack_function)将其结果包装到一个make_any调用中。

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?
Run Code Online (Sandbox Code Playgroud)

可以在此处找到与上一个类似的示例。我需要补充一点,这种Any类型不支持隐式类型转换,所以当你有Any一个int存储时,你不能将它传递给一个需要double. 虽然这可以实现(通过手动提供大量转换规则)。

但是我也看到了您的更新,因此我使用了该代码并应用了必要的修改来运行我提出的解决方案

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;
}
Run Code Online (Sandbox Code Playgroud)

它不使用元组,但您可以编写一个(模板递归)函数来访问元组的每个元素,将其包装成 anAny并将其打包到一个向量中。

另外我不确定为什么Any在初始化结果变量时隐式转换 from不起作用。


嗯,转换为使用boost::any应该不是那么困难。首先,make_any将只使用boost::any的构造函数:

template<typename T>
boost::any make_any(T&& value) {
  return boost::any{std::forward<T>(value)};
}
Run Code Online (Sandbox Code Playgroud)

在 pack 函数中,我认为唯一需要更改的是从参数向量中的当前元素“提取”正确类型。目前这很简单arguments.at(N),依赖于到所需类型的隐式转换。由于boost::any不支持隐式转换,您需要使用boost::any_cast来获取底层值:

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 "};
   }
  });
}
Run Code Online (Sandbox Code Playgroud)

当然,如果您像在您提供的示例中那样使用它,您还需要使用它boost::any_cast来访问结果值。

这应该(理论上)这样做,最终您需要向调用std::remove_reference的模板参数添加一些“魔法” boost::any_cast,但我怀疑这是必要的。(typename std::remove_reference<T>::type而不仅仅是T)

虽然我目前无法测试上述任何一项。