Interfacing |
The modular design of Phoenix makes it extremely extensible. We have seen that layer upon layer, the whole framework is built on a solid foundation. There are only a few simple well designed concepts that are laid out like bricks. Overall the framework is designed to be extended. Everything above the composite and primitives can in fact be considered just as extensions to the framework. This modular design was inherited from the Spirit inline parser framework.
Extension is non-intrusive. And, whenever a component or module is extended, the new extension automatically becomes a first class citizen and is automatically recognized by all modules and components in the framework. There are a multitude of ways in which a module is extended.
1) Write and deploy a new primitive:
So far we have presented only a few primitives 1) arguments 2) values and 3) variables. For the sake of illustration, let us write a simple primitive extension. Let us call it static_int. It shall be parameterized by an integer value. It is like a static version of the the value<int> class, but since it is static, holds no data at all. The integer is encoded in its type. Here is the complete class (sample5.cpp):
template <int N>
struct static_int {
template <typename TupleT>
struct result { typedef int type; };
template <typename TupleT>
int eval(TupleT const&) const { return N; }
};
That's it. Done! Now we can use this as it is already a full- fledged Phoenix citizen due to interface conformance. Let us write a suitable generator to make it easier to use our static_int. Remember that it should be wrapped as an actor before it can be used. Let us call our generator int_const:
template <int N>
phoenix::actor<static_int<N> >
int_const()
{
return static_int<N>();
}
Now we are done. Let's use it:
cout << (int_const<5>() + int_const<6>())() << endl;
Prints out "11". There are lots of things you can do with this form of extension. For instance, data type casts come to mind. Example:
lazy_cast<T>(some_lazy_expression)
2) Write and deploy a new composite:
This is more complicated than our first example (writing a primitive). Nevertheless, once you get the basics, writing a composite is almost mechanical and boring (read: easy ). Check out statements.hpp. All the lazy statements are written in terms of the composite interface.
Ok, let's get on with it. Recall that the if_ else_ lazy statement (and all statements for that matter) return void. What's missing, and will surely be useful, is something like C/C++'s "cond ? a : b" expression. It is really unfortunate that C++ fell short of allowing this to be overloaded. Sigh. Anyway here's the code (sample6.cpp):
template <typename CondT, typename TrueT, typename FalseT>
struct if_else_composite {
typedef if_else_composite<CondT, TrueT, FalseT> self_t;
template <typename TupleT>
struct result {
typedef typename higher_rank<
typename actor_result<TrueT, TupleT>::plain_type,
typename actor_result<FalseT, TupleT>::plain_type
>::type type;
};
if_else_composite(
CondT const& cond_, TrueT const& true__, FalseT const& false__)
: cond(cond_), true_(true__), false_(false__) {}
template <typename TupleT>
typename actor_result<self_t, TupleT>::type
eval(TupleT const& args) const
{
return cond.eval(args) ? true_.eval(args) : false_.eval(args);
}
CondT cond; TrueT true_; FalseT false_; // actors
};
Ok, this is quite a mouthfull. Let's digest this piecemeal.
template <typename CondT, typename TrueT, typename FalseT>
struct if_else_composite {
This is basically a specialized composite that has 3 actors. It has no operation since it is implied. The 3 actors are cond (condition of type CondT) true_ (the true branch of type TrueT), false_ the (false branch or type FalseT).
typedef if_else_composite<CondT, TrueT, FalseT> self_t;
self_t is a typedef that declares its own type: "What am I?"
template <typename TupleT>
struct result {
typedef typename higher_rank<
typename actor_result<TrueT, TupleT>::plain_type,
typename actor_result<FalseT, TupleT>::plain_type
>::type type;
};
We have seen result before. For actor base-classes such as composites and primitives, the parameter is a TupleT, i.e. the tupled arguments passed in from the actor.
So given some arguments, what will be our return type? TrueT and FalseT are also actors remember? So first, we should ask them "What are your *plain* (stripped from references) return types?"
Knowing that, our task is then to know which type has a higher rank (recall rank<T> and higher_rank<T0, T1>). Why do we have to do this? We are emulating the behavior of the "cond ? a : b" expression. In C/C++, the type of this expression is the one (a or b) with the higher rank. For example, if a is an int and b is a double, the result should be a double.
Following this, finally, we have a return type typedef'd by result<TupleT>::type.
if_else_composite(
CondT const& cond_, TrueT const& true__, FalseT const& false__)
: cond(cond_), true_(true__), false_(false__) {}
This is our constructor. We just stuff the constructor arguments into our member variables.
template <typename TupleT>
typename actor_result<self_t, TupleT>::type
eval(TupleT const& args) const
Now, here is our main eval member function. Given a self_t, our type, and the TupleT, the return type deduction is almost canonical. Just ask actor_result, it'll surely know.
{
return cond.eval(args) ? true_.eval(args) : false_.eval(args);
}
We pass the tupled args to all of our actors: cond, args and args appropriately. Notice how this expression reflects the C/C++ version almost to the letter.
Well that's it. Now let's write a generator for this composite:
template <typename CondT, typename TrueT, typename FalseT>
actor<if_else_composite<
typename as_actor<CondT>::type,
typename as_actor<TrueT>::type,
typename as_actor<FalseT>::type> >
if_else_(CondT const& cond, TrueT const& true_, FalseT const& false_)
{
typedef if_else_composite<
typename as_actor<CondT>::type,
typename as_actor<TrueT>::type,
typename as_actor<FalseT>::type>
result;
return result(
as_actor<CondT>::convert(cond),
as_actor<TrueT>::convert(true_),
as_actor<FalseT>::convert(false_));
}
Now this should be trivial to explain. I hope. Again, let's digest this piecemeal.
template <typename CondT, typename TrueT, typename FalseT>
Again, there are three elements involved: The CondT condition 'cond', the TrueT true branch 'true_, and the FalseT false branch 'false_'.
actor<if_else_composite<
typename as_actor<CondT>::type,
typename as_actor<TrueT>::type,
typename as_actor<FalseT>::type> >
This is our target. We want to generate this actor. Now, given our arguments (cond, true_ and false_), we are not really sure if they are really actors. What if the user passes the boolean true as the cond? Surely, that has to be converted to an actor<value<bool> >, otherwise Phoenix will go berzerk and will not be able to accommodate this alien.
as_actor<T>::type
is just what we need. This type computer converts from an arbitrary type T to a full-fledged actor citizen.
if_else_(CondT const& cond, TrueT const& true_, FalseT const& false_)
These are the arguments to our generator 'if_else_'.
typedef if_else_composite<
typename as_actor<CondT>::type,
typename as_actor<TrueT>::type,
typename as_actor<FalseT>::type>
result;
Same as before, this is our target return type, this time stripped off the actor. That's OK because the actor<T> has a constructor that takes in a BaseT object: 'result' in this case.
return result(
as_actor<CondT>::convert(cond),
as_actor<TrueT>::convert(true_),
as_actor<FalseT>::convert(false_));
Finally, we construct and return our result. Notice how we called the as_actor<T>::convert static function to do the conversion from T to a full-fledged actor for each of the arguments.
At last. Now we can use our brand new composite and its generator:
// Print all contents of an STL container c and
// prefix " is odd" or " is even" appropriately.
for_each(c.begin(), c.end(),
cout
<< arg1
<< if_else_(arg1 % 2 == 1, " is odd", " is even")
<< val('\n')
);
3) Write an as_actor<T> converter for a specific type:
By default, an unknown type T is converted to an actor<value<T> >. Say we just wrote a special primitive my_lazy_class following example 1. Whenever we have an object of type my_class, we want to convert this to a my_lazy_class automatically.
as_actor<T> is Phoenix's type converter. All facilities that need to convert from an unknown type to an actor passes through this class. Specializing as_actor<T> for my_class is just what we need. For example:
template <>
struct as_actor<my_class> {
typedef actor<my_lazy_class> type;
static type convert(my_class const& x)
{ return my_lazy_class(x); }
};
For reference, here is the main is_actor<T> interface:
template <typename T>
struct as_actor {
typedef ??? type;
static type convert(T const& x);
};
where ??? is the actor type returned by the static convert function. By default, this is:
typedef value<T> type;
4) Write a specialized overloaded operator for a specific type:
Consider the handling of operator << std::ostream such as cout. When we see an expression such as:
cout << "Hello World\n"
the operator overload actually takes in cout by reference, modifies it and returns the same cout again by reference. This does not conform to the standard behavior of the shift left operator for built-in ints.
In such cases, we can provide a specialized overload for this to work as a lazy-operator in expressions such as "cout << arg1 << arg2;" where the operatior behavior deviates from the standard operator:
We supply a special overload then (see special_ops.hpp):
template <typename BaseT>
actor<composite<
shift_l_op, // an operator tag
variable<std::ostream>, // an actor LHS
actor<BaseT>, // an actor RHS
> >
operator<<(
std::ostream& _0, // LHS argument
actor<BaseT> const& _1) // RHS argument
{
return actor<composite<
shift_l_op, // an operator tag
variable<std::ostream>, // an actor LHS
actor<BaseT>, // an actor RHS
> >(var(_0), _1); // construct #em
}
Take note that the std::ostream reference is converted to a actor<variable<std::ostream> > instead of the default actor<value<std::ostream> > which is not appropriate in this case.
This is not yet complete. Take note also that a specialization for binary_operator also needs to be written (see no. 6).
5) Specialize a rank<T> for a specific type or group of types:
Scenario: We have a set of more specialized numeric classes with higher precision than the built-in types. We have integer, floating and rational classes. All of the classes allow type promotions from the built-ins. These classes have all the pertinent operators implemented along with a couple of mixed type operators whenever appropriate. The operators conform to the canonical behavior of the built-in types. We want to enable Phoenix support for our numeric classes.
Solution: Write rank specializations for our numeric types. This is trivial and straightforward:
template <> struct rank<integer> { static int const value = 10000; };
template <> struct rank<floating> { static int const value = 10020; };
template <> struct rank<rational> { static int const value = 10030; };
Now, whenever there are mixed-type operations such as a + b where a is a primitive built-in int and b is our rational class, the correct promotion will be applied, and the result will be a rational. The type with the higher rank will win.
6) Specialize a unary_operator<TagT, T> or binary_operator<TagT, T0, T1> for a specific type:
Scenario: We have a non-STL conforming iterator named my_iterator. Fortunately, its ++ operator works as expected. Unfortunately, when applying the dereference operator *p, it returns an object of type my_class but does not follow STL's convention that iterator classes have a typedef named reference.
Solution, write a unary_operator specialization for our non- standard class:
template <>
struct unary_operator<dereference_op, my_iterator> {
typedef my_class result_type;
static result_type eval(my_iterator const& iter)
{ return *iter; }
};
Scenario: We have a legacy bigint implementation that we use for cryptography. The class design is totally brain-dead and disobeys all the rules. For example, its + operator is destructive and actually applies the += semantics for efficiency (yes, there are such brain-dead beasts!).
Solution: write a binary_operator specialization for our non- standard class:
template <>
struct binary_operator<plus_op, bigint, bigint> {
typedef bigint& result_type;
static result_type eval(bigint& lhs, bigint const& rhs)
{ return lhs + rhs; }
};
Going back to our example in no. 4, we also need to write a binary_operator<TagT, T0, T1> specialization for ostreams because the << operator for ostreams deviate from the normal behavior.
template <typename T1>
struct binary_operator<shift_l_op, std::ostream, T1> {
typedef std::ostream& result_type;
static result_type eval(std::ostream& out, T1 const& rhs)
{ return out << rhs; }
};
7) Simply write a lazy-function.
Consider this:
struct if_else_func {
template <typename CondT, typename TrueT, typename FalseT>
struct result {
typedef typename higher_rank<TrueT, FalseT>::type type;
};
template <typename CondT, typename TrueT, typename FalseT>
typename higher_rank<TrueT, FalseT>::type
operator()(CondT cond, TrueT const& t, FalseT const& f) const
{ return cond ? t : f; }
};
function<if_else_func> if_else_;
And this corresponding usage:
// Print all contents of an STL container c and
// prefix " is odd" or " is even" appropriately.
for_each(c.begin(), c.end(),
cout
<< arg1
<< if_else_(arg1 % 2 == 1, " is odd", " is even")
<< val('\n')
);
What the $%^!? If we can do this, why on earth did we go to all the trouble twisting our brains inside out with the if_else_ composite in no. 2? Hey, not so fast, there's a slight difference that justifies the if_else_ composite: It is not apparent in the example, but the composite version of the if_else_ evaluates either the true or the false branch, **but not both**. The lazy-function version above always eagerly evaluates all its arguments before the function is called. Thus, if we are to adhere strongly to C/C++ semantics, we need the composite version.
Besides, I need to show an example... Hmmm, so what's the point of no. 7 then? Well, in most cases, a lazy-function will suffice. These beasts are quite powerful, you know.
Copyright © 2001-2002 Joel de Guzman
Use, modification and distribution is subject to the Boost Software
License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)