Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext
Example: Calculator Arity

Now that we have the basics of Proto transforms down, let's consider a slightly more realistic example. We can use transforms to improve the type-safety of the calculator EDSL. If you recall, it lets you write infix arithmetic expressions involving argument placeholders like _1 and _2 and pass them to STL algorithms as function objects, as follows:

double a1[4] = { 56, 84, 37, 69 };
double a2[4] = { 65, 120, 60, 70 };
double a3[4] = { 0 };

// Use std::transform() and a calculator expression
// to calculate percentages given two input sequences:
std::transform(a1, a1+4, a2, a3, (_2 - _1) / _2 * 100);

This works because we gave calculator expressions an operator() that evaluates the expression, replacing the placeholders with the arguments to operator(). The overloaded calculator<>::operator() looked like this:

// Overload operator() to invoke proto::eval() with
// our calculator_context.
template<typename Expr>
double
calculator<Expr>::operator()(double a1 = 0, double a2 = 0) const
{
    calculator_context ctx;
    ctx.args.push_back(a1);
    ctx.args.push_back(a2);

    return proto::eval(*this, ctx);
}

Although this works, it's not ideal because it doesn't warn users if they supply too many or too few arguments to a calculator expression. Consider the following mistakes:

(_1 * _1)(4, 2);  // Oops, too many arguments!
(_2 * _2)(42);    // Oops, too few arguments!

The expression _1 * _1 defines a unary calculator expression; it takes one argument and squares it. If we pass more than one argument, the extra arguments will be silently ignored, which might be surprising to users. The next expression, _2 * _2 defines a binary calculator expression; it takes two arguments, ignores the first and squares the second. If we only pass one argument, the code silently fills in 0.0 for the second argument, which is also probably not what users expect. What can be done?

We can say that the arity of a calculator expression is the number of arguments it expects, and it is equal to the largest placeholder in the expression. So, the arity of _1 * _1 is one, and the arity of _2 * _2 is two. We can increase the type-safety of our calculator EDSL by making sure the arity of an expression equals the actual number of arguments supplied. Computing the arity of an expression is simple with the help of Proto transforms.

It's straightforward to describe in words how the arity of an expression should be calculated. Consider that calculator expressions can be made of _1, _2, literals, unary expressions and binary expressions. The following table shows the arities for each of these 5 constituents.

Table 1.8. Calculator Sub-Expression Arities

Sub-Expression

Arity

Placeholder 1

1

Placeholder 2

2

Literal

0

Unary Expression

arity of the operand

Binary Expression

max arity of the two operands


Using this information, we can write the grammar for calculator expressions and attach transforms for computing the arity of each constituent. The code below computes the expression arity as a compile-time integer, using integral wrappers and metafunctions from the Boost MPL Library. The grammar is described below.

struct CalcArity
  : proto::or_<
        proto::when< proto::terminal< placeholder<0> >,
            mpl::int_<1>()
        >
      , proto::when< proto::terminal< placeholder<1> >,
            mpl::int_<2>()
        >
      , proto::when< proto::terminal<_>,
            mpl::int_<0>()
        >
      , proto::when< proto::unary_expr<_, CalcArity>,
            CalcArity(proto::_child)
        >
      , proto::when< proto::binary_expr<_, CalcArity, CalcArity>,
            mpl::max<CalcArity(proto::_left),
                     CalcArity(proto::_right)>()
        >
    >
{};

When we find a placeholder terminal or a literal, we use an object transform such as mpl::int_<1>() to create a (default-constructed) compile-time integer representing the arity of that terminal.

For unary expressions, we use CalcArity(proto::_child) which is a callable transform that computes the arity of the expression's child.

The transform for binary expressions has a few new tricks. Let's look more closely:

// Compute the left and right arities and
// take the larger of the two.
mpl::max<CalcArity(proto::_left),
         CalcArity(proto::_right)>()

This is an object transform; it default-constructs ... what exactly? The mpl::max<> template is an MPL metafunction that accepts two compile-time integers. It has a nested ::type typedef (not shown) that is the maximum of the two. But here, we appear to be passing it two things that are not compile-time integers; they're Proto callable transforms. Proto is smart enough to recognize that fact. It first evaluates the two nested callable transforms, computing the arities of the left and right child expressions. Then it puts the resulting integers into mpl::max<> and evaluates the metafunction by asking for the nested ::type. That is the type of the object that gets default-constructed and returned.

More generally, when evaluating object transforms, Proto looks at the object type and checks whether it is a template specialization, like mpl::max<>. If it is, Proto looks for nested transforms that it can evaluate. After any nested transforms have been evaluated and substituted back into the template, the new template specialization is the result type, unless that type has a nested ::type, in which case that becomes the result.

Now that we can calculate the arity of a calculator expression, let's redefine the calculator<> expression wrapper we wrote in the Getting Started guide to use the CalcArity grammar and some macros from Boost.MPL to issue compile-time errors when users specify too many or too few arguments.

// The calculator expression wrapper, as defined in the Hello
// Calculator example in the Getting Started guide. It behaves
// just like the expression it wraps, but with extra operator()
// member functions that evaluate the expression.
//   NEW: Use the CalcArity grammar to ensure that the correct
//   number of arguments are supplied.
template<typename Expr>
struct calculator
  : proto::extends<Expr, calculator<Expr>, calculator_domain>
{
    typedef
        proto::extends<Expr, calculator<Expr>, calculator_domain>
    base_type;

    calculator(Expr const &expr = Expr())
      : base_type(expr)
    {}

    typedef double result_type;

    // Use CalcArity to compute the arity of Expr: 
    static int const arity = boost::result_of<CalcArity(Expr)>::type::value;

    double operator()() const
    {
        BOOST_MPL_ASSERT_RELATION(0, ==, arity);
        calculator_context ctx;
        return proto::eval(*this, ctx);
    }

    double operator()(double a1) const
    {
        BOOST_MPL_ASSERT_RELATION(1, ==, arity);
        calculator_context ctx;
        ctx.args.push_back(a1);
        return proto::eval(*this, ctx);
    }

    double operator()(double a1, double a2) const
    {
        BOOST_MPL_ASSERT_RELATION(2, ==, arity);
        calculator_context ctx;
        ctx.args.push_back(a1);
        ctx.args.push_back(a2);
        return proto::eval(*this, ctx);
    }
};

Note the use of boost::result_of<> to access the return type of the CalcArity function object. Since we used compile-time integers in our transforms, the arity of the expression is encoded in the return type of the CalcArity function object. Proto grammars are valid TR1-style function objects, so you can use boost::result_of<> to figure out their return types.

With our compile-time assertions in place, when users provide too many or too few arguments to a calculator expression, as in:

(_2 * _2)(42); // Oops, too few arguments!

... they will get a compile-time error message on the line with the assertion that reads something like this[3]:

c:\boost\org\trunk\libs\proto\scratch\main.cpp(97) : error C2664: 'boost::mpl::asse
rtion_failed' : cannot convert parameter 1 from 'boost::mpl::failed ************boo
st::mpl::assert_relation<x,y,__formal>::************' to 'boost::mpl::assert<false>
::type'
   with
   [
       x=1,
       y=2,
       __formal=bool boost::mpl::operator==(boost::mpl::failed,boost::mpl::failed)
   ]

The point of this exercise was to show that we can write a fairly simple Proto grammar with embedded transforms that is declarative and readable and can compute interesting properties of arbitrarily complicated expressions. But transforms can do more than that. Boost.Xpressive uses transforms to turn expressions into finite state automata for matching regular expressions, and Boost.Spirit uses transforms to build recursive descent parser generators. Proto comes with a collection of built-in transforms that you can use to perform very sophisticated expression manipulations like these. In the next few sections we'll see some of them in action.



[3] This error message was generated with Microsoft Visual C++ 9.0. Different compilers will emit different messages with varying degrees of readability.


PrevUpHomeNext