4

I was playing with Boost.Proto, mostly for fun and to see if in future I could make some use of it in my own projects. That said, as probably most beginners of this library, i've played with a modified version of the 'lazy vector' example, but using transforms instead of contexts to perform the evaluation. The vector is defined as follows (ok, i know, 'vector' is not a good name for something defined at the global namespace scope...)

template <std::size_t D, class T>
class vector { 
    T data_[D];
    enum { dimension = D };
    // Constructors, destructors...
};

// expression wrapper
template <class> class vector_expr;

it is templated on the dimension and the data type, kind of boost::array (i did not use that since i'd like to overload operator= to accept expression trees, as usually done in this sort of things). I defined scalars using code from proto's manual

// scalar = everything convertible to double
struct scalar_terminal :
    proto::terminal<proto::convertible_to <double> >
{};

// vector = everything for which the is_vector returns true_
template <class T> struct is_vector : mpl::false_ {};
template <std::size_t D, class T> struct is_vector <vector <D, T> > : mpl::true_ {};

struct vector_terminal :
    proto::and_ <
       proto::terminal<_>
     , proto::if_<is_vector<proto::_value>()>
  >
{};

// domain   
struct vector_domain
    : proto::domain <proto::generator <vector_expr> >
{};

// expression wrapper
template <class Expr>
struct vector_expr : proto::extends <Expr, vector_expr <Expr>, vector_domain>
{
    typedef proto::extends <Expr, vector_expr <Expr>, vector_domain> base_type;

    // Construct from expression (enough to compile)
    vector_expr (Expr const &e) : base_type (e) {}
};

// Bring in operators
BOOST_PROTO_DEFINE_OPERATORS(is_vector, vector_domain)

Now, the first thing i wanted to do is: check if all vector terminals in an expression have the same dimension D. I ended up with the following working code

// a meta-function that returns the vector dimension
template <class T>
struct vector_dim
{
    typedef mpl::int_ <T::dimension> type;
};

// a meta-function that combines dimensions from subtrees. int<-1> means
// that sub-trees store vectors of differing static dimension. No good.
template <class D1, class D2>
struct dim_combine
{
   typedef mpl::int_ < -1 > type;
};

// ok, dimensions are the same, propagate up the value
template <class D>
struct dim_combine <D, D>
{
   typedef D type;
};

// 0 is used to mark scalars. It is ok to mix vectors and scalars
// but propagate up the vector dimension only. This is for vector
// on the left and scalar on the right.
template <class D>
struct dim_combine <D, mpl::int_ <0> >
{
   typedef D type;
};

// this is for scalar on the left, vector to the right of some 
// binary operator.
template <class D>
struct dim_combine <mpl::int_ <0>, D>
{
   typedef D type;
};

// need this too to avoid ambiguity between the two specializations
// above when D is int_ <0>. Even if this combination should never
// happen
template <>
struct dim_combine <mpl::int_ <0>, mpl::int_<0> >
{
   typedef mpl::int_<0> type;
};


// A transform that check that all arrays have the same dimension
struct vec_dim_check
    : proto::or_ <
        proto::when <
            vector_terminal
          , vector_dim<proto::_value>()
        >
      , proto::when <
            scalar_terminal
          , boost::mpl::int_<0>()
        >
      , proto::when <
            proto::nary_expr<_, proto::vararg<_> >
          , proto::fold<_, boost::mpl::int_<0>(), dim_combine<vec_dim_check, proto::_state>()>
        >
    >
{};

template <class E>
void check_dim (E const&)
{
    typedef typename boost::result_of<vec_dim_check(E)>::type type;
    BOOST_ASSERT(type::value == 3);
}

int main (int, char**)
{
    vector <3,double> a,b,c;
    check_dim (2*a+b/c);
    return 0;
}

The question is: since the dimension of the arrays is already encoded in the expression, then it should be possible to detect invalid combination already at compile time. It should be even possible to avoid creating the tree in the first place. How is this achieved ?

Thanks in advance, best regards

Giuliano
  • 640
  • 3
  • 15

1 Answers1

2

Very nice. Now you need to define a grammar that only accepts valid vector expressions something like this:

struct vector_grammar_untyped
  : proto::or_<
        scalar_terminal,
        vector_terminal,
        proto::nary_expr<proto::_, proto::vararg<vector_grammar_untyped> >
    >
{};

struct vector_grammar
  : proto::and_<
        vector_grammar_untyped,
        proto::if_<mpl::not_equal_to< mpl::int_<-1>, vec_dim_check >()>
    >
{};

Then, you change your definition of vector_domain as follows:

struct vector_domain
    : proto::domain <proto::generator <vector_expr>, vector_grammar >
{};

That should keep you from creating expressions that don't pass your custom type-checking. The second template parameter to proto::domain is the grammar to which all expressions in that domain must conform.

Disclaimer: The above code is untested, but it should get you moving in the right direction.

Eric Niebler
  • 5,927
  • 2
  • 29
  • 43
  • That works fine, thank you. For the the answer and for the beautiful/hard work you did to give us proto. Only issue with the grammar: now the compiler rewards me with several Kb of template instantiation backtrace (I use g++). Before I could just do BOOST_MPL_ASSERT_MSG(...) in the assignment operator of vector and see what the real problem was. It is someway possible to equip the grammar with sensible static assertions? Thank you again – Giuliano Aug 25 '12 at 11:00
  • 1
    Without seeing the code that is causing the error, or what the error is, it's hard for me to speculate. But let me guess: You've succeeded is disabling the operators that would build invalid expressions, so now when the compiler sees an invalid expression, it lists all the overloads of `operator X` that _didn't_ match. Another approach for you to try would be to allow all operators but add a `static_assert` in the `vector_expr` constructor to ensure that `Expr` is the type of a valid expression. That might give better errors. – Eric Niebler Aug 25 '12 at 16:06
  • Exactly that! The problem is that the grammar is doing its job too well, let's say. So probably the path to follow is: use the grammar to disable nonsensical expressions or operators that should be managed differently (e.g assignment), and use static assertions to check semantic-like errors that could happen when writing applications. – Giuliano Aug 25 '12 at 16:49
  • Sounds like an excellent approach. This would even make a good "best practices" item for Proto's docs. – Eric Niebler Aug 25 '12 at 17:10
  • Concerning the "best practices" part, it would be nice for beginners like me, to see something more (example-wise) on transforms in the docs. For example, in my case i need a bunch of transforms to: index the expression, convert the vectors in the expression into iterators (modified copy of the tree), advance all iterators... i'm using only primitive transforms which is probably overkill... So yes, a "best practices" section would be excellent. Thank you again for help! – Giuliano Aug 25 '12 at 17:55
  • @Giuliano you're rather right. Dont do too much strong checking in grammar, it's usuqlly better to do it at th elatest evalaution point. – Joel Falcou Sep 13 '12 at 07:13