'冻结'一个表达

时间:2012-06-13 06:19:34

标签: c++ templates c++11 c-preprocessor

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

take x*x with x in container ...

其中...表示进一步(对此问题无用)语法。但是,如果我尝试编译它,无论我曾经使用什么预处理器翻译'采取''运算符'(在引号中,因为它在技术上不是运算符,但是翻译阶段将它变成了一个类,比如说,运算符*可用,)编译器仍然试图评估/计算x * x的来源,(并且,因为它之前没有被声明(因为它在'in'阶段进一步声明),它代替)找不到它并抛出编译错误。

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

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

修改: 为了澄清我的问题,请采取以下措施。假设运算符以一种理智的方式定义,以便以下尝试实现上述take ...语法的功能:

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

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

4 个答案:

答案 0 :(得分:2)

我想出了基于表达式模板的几乎解决方案(注意:这些不是表达式模板,它们是基于表达式模板)。遗憾的是,我无法想出一种不需要您预先声明x的方法,但我确实想出了一种延迟类型的方法,因此您只需声明{ {1}}一个全局,并且可以在同一个程序/文件/范围中反复使用它用于不同的类型。这是魔术的表达类型,我设计的非常灵活,您应该能够轻松地随意添加操作和使用。除了x的预先声明之外,它与你描述的完全一致。

缺点我知道:它需要xT*TT+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;
}

http://ideone.com/Dnb50

处编译并运行正确的输出

您可能会注意到,由于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 >> 必须预先声明,因此x部分将被完全忽略。这里几乎没有宏观魔法,宏有效地将其转换为“with”,其中x*x >> x << container完全没有任何作用。所以表达式有效地>>x”。

另请注意,此方法很慢,因为这是一个解释器,几乎所有的减速都意味着。但是,它有 serializable 的奖励,您可以将该功能保存到文件中,稍后加载,然后执行然后

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

<小时/> 如果您可以将表达式修改为with,那么就不需要预先解析take(x*x with x in container),而是使用比表达式模板简单得多的东西。

x

答案 1 :(得分:1)

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

这是模板的用法。实施代码为here

#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,它适用于非常类似的概念。

这是一个单独的答案,因为代码不同,更复杂/令人生畏。这也是为什么实现不在答案本身。

答案 2 :(得分:0)

我不认为有可能使用预处理器获得这个“列表补偿”(不完全,但它做同样的事情)ala haskell。预处理器只进行简单的搜索并替换为参数的可能性,因此它不能执行任意替换。特别是改变表达部分的顺序是不可能的。

我无法在不更改顺序的情况下看到这样做的方法,因为您始终需要x以某种方式出现在x*x之前以定义此变量。使用lambda无济于事,因为在x部分前面仍然需要x*x,即使它只是一个参数。这使得这种语法无法实现。

有一些解决方法:

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

  • 稍微更改语法。这种列表理解在语义上只是对Monad的使用的语法简化。随着C ++ 11的出现,Monads已经成为可能。然而,语法糖可能不是。如果你决定用Monad包装你想要做的东西,很多事情仍然是可能的,你只需稍微改变语法。用C ++实现Monads虽然不是很有趣(尽管我首先预计不会这样)。看看here,了解如何在C ++中获取Monads。

答案 3 :(得分:0)

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

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

使用此语法,您可以将其转换为预处理程序序列(当然,使用Boost.Preprocessor库)。将它作为预处理程序序列后,您可以应用许多算法将其转换为您喜欢的程序。使用Linq C ++库可以实现类似的方法,您可以在其中编写:

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参数的序列)。现在继续我们如何定义使用while结构的TO_SEQ宏:

#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中使用。(虽然有解决方法)