![]() |
Home | Libraries | People | FAQ | More |
If we limited ourselves to nothing but terminals and operator overloads, our domain-specific embedded languages wouldn't be very expressive. Imagine that we wanted to extend our calculator DSEL with a full suite of math functions like sin() and pow() that we could invoke lazily as follows.
// A calculator expression that takes one argument // and takes the sine of it. sin(_1);
We would like the above to create an expression template representing a function invocation. When that expression is evaluated, it should cause the function to be invoked. (At least, that's the meaning of function invocation we'd like the calculator DSEL to have.) You can define sin quite simply as follows.
// "sin" is a Proto terminal containing a function pointer proto::terminal< double(*)(double) >::type const sin = {&std::sin};
In the above, we define sin as a Proto terminal containing a pointer to the std::sin() function. Now we can use sin as a lazy function. The default_context that we saw in the Introduction knows how to evaluate lazy functions. Consider the following:
double pi = 3.1415926535; proto::default_context ctx; // Create a lazy "sin" invocation and immediately evaluate it std::cout << proto::eval( sin(pi/2), ctx ) << std::endl;
The above code prints out:
1
It is important to note that there is nothing special about terminals that contain function pointers. Any Proto expression has an overloaded function call operator. Consider:
// This compiles! proto::lit(1)(2)(3,4)(5,6,7,8);
That may look strange at first. It creates an integer terminal with proto::lit(), and then invokes it like a function again and again. What does it mean? To be sure, the default_context wouldn't know what to do with it. The default_context only knows how to evaluate expressions that are sufficiently C++-like. In the case of function call expressions, the left hand side must evaluate to something that can be invoked: a pointer to a function, a reference to a function, or a TR1-style function object. That doesn't stop you from defining your own evaluation context that gives that expression a meaning. But more on that later.
Now, what if we wanted to add a pow() function to our calculator DSEL that users could invoke as follows?
// A calculator expression that takes one argument // and raises it to the 2nd power pow< 2 >(_1);
The simple technique described above of making pow a terminal containing a function pointer doesn't work here. If pow is an object, then the expression pow< 2 >(_1) is not valid C++. pow needs to be a real function template. But it must be an unusual function; it must return an expression template.
Before we can write the pow() function, we need a function object that wraps an invocation of std::pow().
// Define a pow_fun function object template<int Exp> struct pow_fun { typedef double result_type; double operator()(double d) const { return std::pow(d, Exp); } };
Now, let's try to define a function template that returns an expression template. We'll use the proto::function<> metafunction to calculate the type of a Proto expression that represents a function call. It is analogous to proto::terminal<>. (We'll see a couple of different ways to solve this problem, and each will demonstrate another utility for defining Proto front-ends.)
// Define a lazy pow() function for the calculator DSEL. // Can be used as: pow< 2 >(_1) template<int Exp, typename Arg> typename proto::function< typename proto::terminal<pow_fun<Exp> >::type , Arg const & >::type pow(Arg const &arg) { typedef typename proto::function< typename proto::terminal<pow_fun<Exp> >::type , Arg const & >::type result_type; result_type result = {{{}}, arg}; return result; }
In the code above, notice how the proto::function<> and proto::terminal<> metafunctions are used to calculate the return type: pow() returns an expression template representing a function call where the first child is the function to call and the second is the argument to the function. (Unfortunately, the same type calculation is repeated in the body of the function so that we can initialize a local variable of the correct type. We'll see in a moment how to avoid that.)
![]() |
Note |
|---|---|
As with proto::function<>, there are metafunctions corresponding to all of the overloadable C++ operators for calculation expression types. | |
With the above definition of the pow() function, we can create calculator expressions like the one below and evaluate them using the calculator_context we implemented in the Introduction.
// Initialize a calculator context calculator_context ctx; ctx.args.push_back(3); // let _1 be 3 // Create a calculator expression that takes one argument, // adds one to it, and raises it to the 2nd power; and then // immediately evaluate it using the calculator_context. assert( 16 == proto::eval( pow<2>( _1 + 1 ), ctx ) );
Above, we defined a pow() function template that returns an expression template representing a lazy function invocation. But if we tried to call it as below, we'll run into a problem.
// ERROR: pow() as defined above doesn't work when // called with a non-Proto argument. pow< 2 >( 4 );
Proto expressions can only have other Proto expressions as children. But if we look at pow()'s function signature, we can see that if we pass it a non-Proto object, it will try to make it a child.
template<int Exp, typename Arg> typename proto::function< typename proto::terminal<pow_fun<Exp> >::type , Arg const & // <=== ERROR! This may not be a Proto type! >::type pow(Arg const &arg)
What we want is a way to make Arg into a Proto terminal if it is not a Proto expression already, and leave it alone if it is. For that, we can use proto::as_child(). The following implementation of the pow() function handles all argument types, expression templates or otherwise.
// Define a lazy pow() function for the calculator DSEL. Use // proto::as_child() to Protofy the argument, but only if it // is not a Proto expression type to begin with! template<int Exp, typename Arg> typename proto::function< typename proto::terminal<pow_fun<Exp> >::type , typename proto::result_of::as_child<Arg const>::type >::type pow(Arg const &arg) { typedef typename proto::function< typename proto::terminal<pow_fun<Exp> >::type , typename proto::result_of::as_child<Arg const>::type >::type result_type; result_type result = {{{}}, proto::as_child(arg)}; return result; }
Notice how we use the proto::result_of::as_child<> metafunction to calculate the return type, and the proto::as_child() function to actually normalize the argument.
The versions of the pow() function we've seen above are rather verbose. In the return type calculation, you have to be very explicit about wrapping non-Proto types. Worse, you have to restate the return type calculation in the body of pow() itself. Proto provides a helper for building expression templates directly that handles these mundane details for you. It's called proto::make_expr(). We can redefine pow() with it as below.
// Define a lazy pow() function for the calculator DSEL. // Can be used as: pow< 2 >(_1) template<int Exp, typename Arg> typename proto::result_of::make_expr< proto::tag::function // Tag type , pow_fun<Exp> // First child (by value) , Arg const & // Second child (by reference) >::type pow(Arg const &arg) { return proto::make_expr<proto::tag::function>( pow_fun<Exp>() // First child (by value) , boost::ref(arg) // Second child (by reference) ); }
There are some things to notice about the above code. We use proto::result_of::make_expr<> to calculate the return type. The first template parameter is the tag type for the expression node we're building -- in this case, proto::tag::function, which is the tag type Proto uses for function call expressions.
Subsequent template parameters to proto::result_of::make_expr<> represent children nodes. If a child type is not already a Proto expression, it is made into a terminal with proto::as_child(). A type such as pow_fun<Exp> results in terminal that is held by value, whereas a type like Arg const & (note the reference) indicates that the result should be held by reference.
In the function body is the runtime invocation of proto::make_expr(). It closely mirrors the return type calculation. proto::make_expr() requires you to specify the node's tag type as a template parameter. The arguments to the function become the node's children. When a child should be stored by value, nothing special needs to be done. When a child should be stored by reference, you must use the boost::ref() function to wrap the argument. Without this extra information, the proto::make_expr() function couldn't know whether to store a child by value or by reference.