3

我有一个想要“冻结”的 C++ 表达式。我的意思是我的语法如下:

take x*x with x in container ...

其中...表示进一步(对此问题无用)语法。但是,如果我尝试编译它,无论我使用什么预处理器翻译来“接受”一个“运算符”(用引号括起来,因为它在技术上不是一个运算符,但翻译阶段将它变成一个类,比如说, operator* 可用),编译器仍然尝试评估/计算 x*x 来自哪里,(并且,因为它之前没有声明过(因为它在“in”阶段进一步声明),相反)找不到它并引发编译错误。

我目前的想法基本上涉及尝试将表达式放在 lambda 中(并且由于我们可以推断出容器的类型,我们可以x使用正确的类型声明为,例如,[](decltype(*begin(container)) x) { return x*x }--因此,当编译器查看此语句时,它是有效的并且没有抛出错误),但是,我在实际实现这一点时遇到了错误。

因此,我的问题是:有没有办法/什么是“冻结”x*x我的表达部分的最佳方式?

编辑:为了澄清我的问题,请采取以下措施。假设 operator- 以合理的方式定义,以便以下尝试实现上述take ...语法的功能:

MyTakeClass() - x*x - MyWithClass() - x - MyInClass() - container ...

编译这条语句时,编译器会抛出错误;x 未声明,因此 x*x 没有意义(x - MyInClass() 等也没有)。我想要实现的是找到一种方法来编译上述表达式,使用任何可用的巫术魔法,而不知道 x 的类型(或者,事实上,它将被命名为 x;它可以被命名为 '一些愚蠢的变量名')。

4

4 回答 4

2

我想出了一个基于表达式模板的几乎解决方案(注意:这些不是表达式模板,它们是基于表达式模板的)。不幸的是,我想不出一种不需要你预先声明的方法x,但我确实想出了一种延迟类型的方法,所以你只需要x全局声明一个,并且可以一遍又一遍地将它用于不同的类型在同一个程序/文件/范围内。这是发挥魔力的表达式类型,我将其设计得非常灵活,您应该能够轻松地随意添加操作和使用。它完全按照您的描述使用,除了 x 的预先声明。

我知道的缺点:它确实需要T*T,T+T并且T(long)是可编译的。

expression x(0, true); //x will be the 0th parameter.  Sorry: required :(

int main() {
    std::vector<int> container;
    container.push_back(-3);
    container.push_back(0);
    container.push_back(7);
    take x*x with x in container; //here's the magic line
    for(unsigned i=0; i<container.size(); ++i)
        std::cout << container[i] << ' ';

    std::cout << '\n';
    std::vector<float> container2;
    container2.push_back(-2.3);
    container2.push_back(0);
    container2.push_back(7.1);
    take 1+x with x in container2; //here's the magic line
    for(unsigned i=0; i<container2.size(); ++i)
        std::cout << container2[i] << ' ';

    return 0;
}

这是使一切正常的类和定义:

class expression {
    //addition and constants are unused, and merely shown for extendibility
    enum exprtype{parameter_type, constant_type, multiplication_type, addition_type} type;
    long long value; //for value types, and parameter number
    std::unique_ptr<expression> left; //for unary and binary functions
    std::unique_ptr<expression> right; //for binary functions
   
public:
    //constructors
    expression(long long val, bool is_variable=false) 
    :type(is_variable?parameter_type:constant_type), value(val)
    {}
    expression(const expression& rhs) 
    : type(rhs.type)
    , value(rhs.value)
    , left(rhs.left.get() ? std::unique_ptr<expression>(new expression(*rhs.left)) : std::unique_ptr<expression>(NULL))
    , right(rhs.right.get() ? std::unique_ptr<expression>(new expression(*rhs.right)) : std::unique_ptr<expression>(NULL))
    {}
    expression(expression&& rhs) 
    :type(rhs.type), value(rhs.value), left(std::move(rhs.left)), right(std::move(rhs.right)) 
    {}
    //assignment operator
    expression& operator=(expression rhs) {
       type = rhs.type;
       value = rhs.value;
       left = std::move(rhs.left);
       right = std::move(rhs.right);
       return *this;
    } 
 
    //operators
    friend expression operator*(expression lhs, expression rhs) {
        expression ret(0);
        ret.type = multiplication_type;
        ret.left = std::unique_ptr<expression>(new expression(std::move(lhs)));
        ret.right = std::unique_ptr<expression>(new expression(std::move(rhs)));
        return ret;
    }
    friend expression operator+(expression lhs, expression rhs) {
        expression ret(0);
        ret.type = addition_type;
        ret.left = std::unique_ptr<expression>(new expression(std::move(lhs)));
        ret.right = std::unique_ptr<expression>(new expression(std::move(rhs)));
        return ret;
    }
    
    //skip the parameter list, don't care.  Ignore it entirely
    expression& operator<<(const expression&) {return *this;}
    expression& operator,(const expression&) {return *this;}

    template<class container>    
    void operator>>(container& rhs) {
        for(auto it=rhs.begin(); it!=rhs.end(); ++it)
            *it = execute(*it);
    }  
private: 
    //execution
    template<class T>
    T execute(const T& p0) {
       switch(type) {
       case parameter_type :
           switch(value) {
           case 0: return p0; //only one variable
           default: throw std::runtime_error("Invalid parameter ID");
           }
       case constant_type:
           return ((T)(value));
       case multiplication_type:
           return left->execute(p0) * right->execute(p0);
       case addition_type:
           return left->execute(p0) + right->execute(p0);
       default: 
           throw std::runtime_error("Invalid expression type");
       }
    }
    //This is also unused, and merely shown as extrapolation
    template<class T>
    T execute(const T& p0, const T& p1) {
       switch(type) {
       case parameter_type :
           switch(value) {
           case 0: return p0;
           case 1: return p1; //this version has two variables
           default: throw std::runtime_error("Invalid parameter ID");
           }
       case constant_type:
           return value;
       case multiplication_type:
           return left->execute(p0, p1) * right->execute(p0, p1);
       case addition_type:
           return left->execute(p0, p1) + right->execute(p0, p1);
       default: 
           throw std::runtime_error("Invalid expression type");
       }
    }
}; 
#define take 
#define with <<
#define in >>
  

在http://ideone.com/Dnb50以正确的输出编译和运行

您可能会注意到,由于x必须预先声明,因此该with部分被完全忽略。这里几乎没有宏魔法,宏有效地将它变成“ x*x >> x << container”,其中 '>>x完全没有任何作用。所以表达式实际上是“ x*x << container”。

另请注意,此方法很慢,因为这是一个解释器,几乎所有暗示的减速。但是,它的好处是它是可序列化的,您可以将函数保存到文件中,稍后加载,然后执行

R.MartinhoFernandes观察到 的定义x可以简化为expression x;,并且可以从with截面中推断出参数的顺序,但需要对设计进行大量的重新思考,并且会更加复杂。稍后我可能会回来添加该功能,但与此同时,我知道这绝对是可能的。


如果您可以将表达式修改为 `take(x*x with x in container)`,则无需预先声明 `x`,这比表达式模板简单得多。#define with , #define in , #define take(expr, var, con) \ std::transform(con.begin(), con.end(), con.begin(), \ [](const typename con: :value_type& var) -> 类型名 con::value_type \ {return expr;});
int main() {
    std::vector<int> container;
    container.push_back(-3);
    container.push_back(0);
    container.push_back(7);
    take(x*x with x in container); //here's the magic line
    for(unsigned i=0; i<container.size(); ++i)
        std::cout << container[i] << ' ';
}
于 2012-06-13T20:08:51.620 回答
1

我做了一个与我之前的答案非常相似的答案,但是使用了实际的表达式模板,这应该会快得多。不幸的是,MSVC10 在尝试编译时会崩溃,但 MSVC11、GCC 4.7.0 和 Clang 3.2 都可以正常编译和运行。(所有其他版本未经测试)

这是模板的用法。实现代码在这里

#define take 
#define with ,
#define in >>= 

//function call for containers 
template<class lhsexpr, class container>
lhsexpr operator>>=(lhsexpr lhs, container& rhs)
{
    for(auto it=rhs.begin(); it!=rhs.end(); ++it)
        *it = lhs(*it);
    return lhs;
}

int main() {
    std::vector<int> container0;
    container0.push_back(-4);
    container0.push_back(0);
    container0.push_back(3);
    take x*x with x in container0; //here's the magic line
    for(auto it=container0.begin(); it!=container0.end(); ++it)
        std::cout << *it << ' ';
    std::cout << '\n';

    auto a = x+x*x+'a'*x;
    auto b = a; //make sure copies work
    b in container0;
    b in container1;
    std::cout << sizeof(b);

    return 0;
}

正如你所看到的,这与我之前的代码完全一样,除了现在所有的函数都是在编译时决定的,这意味着这将与 lambda 具有完全相同的速度。事实上,C++11 的 lambdas 早于boost::lambda它在非常相似的概念上工作。

这是一个单独的答案,因为代码大不相同,而且更加复杂/令人生畏。这也是为什么实施不在答案本身中的原因。

于 2012-06-15T22:51:23.403 回答
0

我认为使用预处理器不可能获得这种“列表理解”(不完全是,但它正在做同样的事情)ala haskell。预处理器只是简单的搜索和替换参数的可能性,所以它不能执行任意替换。尤其是不能改变表达部分的顺序。

如果不更改顺序,我看不到这样做的方法,因为您总是需要x以某种方式出现在x*x定义此变量之前。使用 lambda 将无济于事,因为您仍然需要xx*x部件前面,即使它只是作为参数。这使得这种语法是不可能的。

有一些方法可以解决这个问题:

  • 使用不同的预处理器。有一些基于 Lisp-macros 思想的预处理器,可以使它们具有语法意识,因此可以将一种语法树任意转换为另一种语法树。一个例子是为 OCaml 语言开发的 Camlp4/Camlp5。关于如何使用它进行任意语法转换有一些非常好的教程。我曾经有一个关于如何使用 Camlp4 将 makefile 转换为 C 代码的解释,但我再也找不到了。还有其他一些关于如何做这些事情的教程。

  • 稍微改变语法。这种列表理解本质上只是对Monad用法的语法简化。随着 C++11 的到来,C++ 中的 Monads 已经成为可能。然而,语法糖可能不是。如果你决定将你想要做的事情包装在一个 Monad 中,很多事情仍然是可能的,你只需要稍微改变语法。不过,在 C++ 中实现 Monad 一点也不有趣(尽管我最初期望不是这样)。在这里查看一些如何在 C++ 中获取 Monads 的示例。

于 2012-06-13T08:34:42.910 回答
0

最好的方法是使用预处理器解析它。我确实相信预处理器可以成为构建 EDSL(嵌入式领域特定语言)的非常强大的工具,但您必须首先了解预处理器解析事物的局限性。预处理器只能解析出预定义的标记。所以必须通过在表达式周围放置括号来稍微改变语法,并且FREEZE还必须围绕它一个宏(我刚刚选择了 FREEZE,它可以被称为任何东西):

FREEZE(take(x*x) with(x, container))

使用此语法,您可以将其转换为预处理器序列(当然,使用Boost.Preprocessor库)。一旦你把它作为一个预处理器序列,你就可以对它应用很多算法来把它转换成你喜欢的样子。类似的方法是使用 C++ 的Linq库完成的,您可以在其中编写:

LINQ(from(x, numbers) where(x > 2) select(x * x))

现在,要首先转换为 pp 序列,您需要定义要解析的关键字,如下所示:

#define KEYWORD(x) BOOST_PP_CAT(KEYWORD_, x)
#define KEYWORD_take (take)
#define KEYWORD_with (with)

所以它的工作方式是当你调用KEYWORD(take(x*x) with(x, container))它时它会扩展为(take)(x*x) with(x, container),这是将它转换为 pp 序列的第一步。现在继续我们需要使用 Boost.Preprocessor 库中的 while 构造,但首先我们需要定义一些小宏来帮助我们:

// Detects if the first token is parenthesis
#define IS_PAREN(x) IS_PAREN_CHECK(IS_PAREN_PROBE x)
#define IS_PAREN_CHECK(...) IS_PAREN_CHECK_N(__VA_ARGS__,0)
#define IS_PAREN_PROBE(...) ~, 1,
#define IS_PAREN_CHECK_N(x, n, ...) n
// Detect if the parameter is empty, works even if parenthesis are given
#define IS_EMPTY(x) BOOST_PP_CAT(IS_EMPTY_, IS_PAREN(x))(x)
#define IS_EMPTY_0(x) BOOST_PP_IS_EMPTY(x)
#define IS_EMPTY_1(x) 0 

// Retrieves the first element of the sequence
// Example:
// HEAD((1)(2)(3)) // Expands to (1)
#define HEAD(x) PICK_HEAD(MARK x)
#define MARK(...) (__VA_ARGS__),
#define PICK_HEAD(...) PICK_HEAD_I(__VA_ARGS__,)
#define PICK_HEAD_I(x, ...) x

// Retrieves the tail of the sequence
// Example:
// TAIL((1)(2)(3)) // Expands to (2)(3)
#define TAIL(x) EAT x
#define EAT(...)

这提供了对括号和空的更好的检测。它提供了一个HEADTAIL宏,其工作方式与BOOST_PP_SEQ_HEAD. (Boost.Preprocessor 无法处理具有 vardiac 参数的序列)。现在我们如何定义一个TO_SEQ使用 while 构造的宏:

#define TO_SEQ(x) TO_SEQ_WHILE_M \
( \
BOOST_PP_WHILE(TO_SEQ_WHILE_P, TO_SEQ_WHILE_O, (,x)) \
)

#define TO_SEQ_WHILE_P(r, state) TO_SEQ_P state
#define TO_SEQ_WHILE_O(r, state) TO_SEQ_O state
#define TO_SEQ_WHILE_M(state) TO_SEQ_M state

#define TO_SEQ_P(prev, tail) BOOST_PP_NOT(IS_EMPTY(tail))
#define TO_SEQ_O(prev, tail) \
BOOST_PP_IF(IS_PAREN(tail), \
TO_SEQ_PAREN, \
TO_SEQ_KEYWORD \
)(prev, tail)
#define TO_SEQ_PAREN(prev, tail) \
(prev (HEAD(tail)), TAIL(tail))

#define TO_SEQ_KEYWORD(prev, tail) \
TO_SEQ_REPLACE(prev, KEYWORD(tail))

#define TO_SEQ_REPLACE(prev, tail) \
(prev HEAD(tail), TAIL(tail))

#define TO_SEQ_M(prev, tail) prev

现在,当你打电话时,TO_SEQ(take(x*x) with(x, container))你应该得到一个序列(take)((x*x))(with)((x, container))

现在,这个序列更容易使用(因为 Boost.Preprocessor 库)。您现在可以对其进行反转、转换、过滤、折叠等。这非常强大,并且比将它们定义为宏要灵活得多。例如,在 Linq 库中,查询from(x, numbers) where(x > 2) select(x * x)被转换为这些宏:

LINQ_WHERE(x, numbers)(x > 2) LINQ_SELECT(x, numbers)(x * x)

对于这些宏,它将生成用于列表理解的 lambda,但是在生成 lambda 时它们还有更多工作要做。在你的库中也可以这样做,take(x*x) with(x, container)可以转换成这样的东西:

FREEZE_TAKE(x, container, x*x)

另外,您没有定义take侵入全局空间的宏。

注意:这里的这些宏需要 C99 预处理器,因此在 MSVC 中不起作用。(虽然有解决方法)

于 2012-06-14T07:25:34.430 回答