C++ expression as variable
I am working on DSEL and would like to have the following:
Bra ij;
Ket kl, cd;
(ij||kl); // initialize some array
(ij||cd); // ditto
....
T(i,j,k,l)*(ij||kl); // do some math without recomputing (ij||kl)
So essentially i want to have expression acting as variable. Is it possible?
my idea so far is to have a "singleton" factory that generates/lo开发者_运维知识库oks up the arrays using expression (ij|kl)
. Anything else?
If you don't want to recompute ij||kl
, then just store them into a variable of whatever type that expression returns. This is precisely one of the reasons variables exists.
Okay, here's the only way I can think of doing this, though it doesn't sound very pretty. What you can do is, when the operator|| function is called, store the operands, as well as the result, into instance variables (if operator|| is a member function of some class), or into a statically allocated variable (if operator|| is declared on it lonesome).
The next time operator|| is called, check to see if the operands are the same as the last time it was called. If they are, simply return the last result that you stored. Otherwise, compute a new result.
That should do the trick. The nasty part is that it requires copying the operands to other variables, which could be costly depending on the circumstances. Howver, if the variables are immutable, you could just keep pointers to the operands, which would be a little cheaper.
If you want to take this even further, you could use a map
or something to store the operands and results of multiple previous calls. That way this will work if you need to do this for several distinct computations.
To do this kind of stuff, it is pretty clear that you need some sort of global knowledge. In other words, you will have to construct some sort of global or semi-global structure that keeps a record of the operations / expressions.
I would suggest you construct a graph (perhaps with BGL) where nodes are expressions (variables being a special case of a zero-arity operator). If you connect each operand as an incident vertex to the operator's vertex, you can construct a graph. Later, when it is time to actually evaluate the expression, you can first traverse the graph with some pruning rules such that redundant operations are eliminated. If you make sure that none of the vertices in the graph get duplicated, then the pruning is implicit, I would guess.
If you want to avoid using global data, I could suggest you use an expression manager class of some sort and register all the operations to it. For example, like this:
int main() {
expression_handler expression; //have some class to handle a sequence of operations.
expression
<< (ij||kl)
<< (ij||cd)
<< ....
<< T(i,j,k,l)*(ij||kl); //register all the expressions, in order.
expression.evaluate(); //evaluate the expression (possibly optimizing the graph before)
return 0;
};
I would do it as a combination of Ken and Mikael's suggestions. This kind of stuff is pretty easy with operator overloading particularly when you have custom types on each side.
the Bra::operator || method returns a temporary object ExpressionHandle.
When it creates the ExpressionHandle, it also registers the ExpressionBody internally with a global store (this lets you avoid having to use instance variables but means you have to retain all your expressions until the end of the program or an explicit release).
ExpressionBody objects are both a declaration (expression tree) and an optional result. You can use lazy techniques to only evaluate them when they are eventually called.
Invoking an ExpressionBody occurs when an ExpressionHandle is created by a line like
T(i,j,k,l)*(ij||kl);
The (ij||kl) above would create an ExpressionBody and seek to register it but find an existing global one, so just return an ExpressionHandle pointing to the global instance.
operator*(const ExpressionHandle&, blah) would ask the ExpressionBody to return its result, at which point it either returns a cached result or performs the first evaluation and caches it.
It's the compilers job to decide if the expression needs to be re-evaluated or can be re-used from previous evaluation. Let the compiler do its job.
You should concentrate on expressing yourself as concisely and clearly as possible (whether you use variables or not is totally dependent on you and the context). But if an expression uses unchanged objects (ie objects that have not been assigned too or modified by the use of a non cost method) then the compiler is likely to optimize away and re-calculation of the same expression.
Note. To help the compiler make sure properly mark methods as const.
精彩评论