Operators revisited

Each C++ operator has a special tag type associated with it. For example the binary + operator has a plus_op tag type associated with it. This operator tag is used to specialize either:

  1. unary_operator<TagT, T0>
  2. binary_operator<TagT, T0, T1>

template classes (see unary_operator and binary_operator below). Specializations of these unary_operator and binary_operator are the actual workhorses that implement the operations. The behavior of each lazy operator depends on these unary_operator and binary_operator specializations.

Preset specializations conform to the canonical operator rules modeled by the behavior of integers and pointers:

Special operators and extensibility

It is of course possible to override the standard operator behavior when appropriate. For example, the behavior of std::cout does not conform to the canonocal shift left operator << (i.e. the rhs std::cout is a mutable reference). Odd balls such as this are placed in special_ops.hpp. There you will find specializations for various classes found in the standard lib.

The library is meant to be extensible. Users may implement their own specializations to allow other libraries to be adapted to be partial-function-evaluation savvy. Later on, in the section "Interfacing (to applications, libraries and frameworks)", discussion will be focused on interfacing and extending the framework.

Operator tags

Each C++ operator has a corresponding tag type. This is used as a means for specializing the unary_operator and binary_operator (see below). The tag also serves as the lazy operator type compatible with a composite as an operation (see composite). Here are two examples of operator tags:

Unary example:

    struct negative_op {

        template <typename T0>
        struct result {

            typedef typename unary_operator<negative_op, T0>
                ::result_type type;
        };

        template <typename T0>
        typename unary_operator<negative_op, T0>::result_type
        operator()(T0& _0) const
        { return unary_operator<negative_op, T0>::eval(_0); }
    };

Binary example:

    struct plus_op {

        template <typename T0, typename T1>
        struct result {

            typedef typename binary_operator<plus_op, T0, T1>
                ::result_type type;
        };

        template <typename T0, typename T1>
        typename binary_operator<plus_op, T0, T1>::result_type
        operator()(T0& _0, T1& _1) const
        { return binary_operator<plus_op, T0, T1>::eval(_0, _1); }
    };

Notice that these are again perfect examples of a composite operation. This style of specialized function is ubiquitous in the framework. We shall see how the unary_operator<negative_op, T0> and the binary_operator<plus_op, T0, T1> template classes, work in a short while.

Here are the complete list of operator tags:

    //  Unary operator tags

    struct negative_op;         struct positive_op;
    struct logical_not_op;      struct invert_op;
    struct reference_op;        struct dereference_op;
    struct pre_incr_op;         struct pre_decr_op;
    struct post_incr_op;        struct post_decr_op;

    //  Binary operator tags

    struct assign_op;           struct index_op;
    struct plus_assign_op;      struct minus_assign_op;
    struct times_assign_op;     struct divide_assign_op;    struct mod_assign_op;
    struct and_assign_op;       struct or_assign_op;        struct xor_assign_op;
    struct shift_l_assign_op;   struct shift_r_assign_op;

    struct plus_op;             struct minus_op;
    struct times_op;            struct divide_op;           struct mod_op;
    struct and_op;              struct or_op;               struct xor_op;
    struct shift_l_op;          struct shift_r_op;

    struct eq_op;               struct not_eq_op;
    struct lt_op;               struct lt_eq_op;
    struct gt_op;               struct gt_eq_op;
    struct logical_and_op;      struct logical_or_op;

unary_operator

The unary_operator class implements most of the C++ unary operators. Each specialization is basically a simple static eval function plus a result_type typedef that determines the return type of the eval function.

TagT is one of the unary operator tags above and T is the data type (argument) involved in the operation. Here is an example:

    template <typename T>
    struct unary_operator<negative_op, T> {

        typedef T const result_type;
        static result_type eval(T const& v)
        { return -v; }
    };

This example is exactly what was being referred to by the first example we saw in the section on operator tags.

Only the behavior of C/C++ built-in types are taken into account in the specializations provided in operator.hpp. For user-defined types, these specializations may still be used provided that the operator overloads of such types adhere to the standard behavior of built-in types.

A separate special_ops.hpp file implements more STL savvy specializations. Other more specialized unary_operator implementations may be defined by the client for specific unary operator tags/data types.

binary_operator

The binary_operator class implements most of the C++ binary operators. Each specialization is basically a simple static eval function plus a result_type typedef that determines the return type of the eval function.

TagT is one of the binary operator tags above T0 and T1 are the (arguments') data types involved in the operation. Here is an example:

    template <typename T0, typename T1>
    struct binary_operator<plus_op, T0, T1> {

        typedef typename higher_rank<T0, T1>::type const result_type;
        static result_type eval(T0 const& lhs, T1 const& rhs)
        { return lhs + rhs; }
    };

This example is exactly what was being referred to by the second example we saw in the section on operator tags. higher_rank<T0, T1> is a type computer. We shall see how this works in a short while, pardon the forward information.

Only the behavior of C/C++ built-in types are taken into account in the specializations provided in operator.hpp. For user-defined types, these specializations may still be used provided that the operator overloads of such types adhere to the standard behavior of built-in types.

A separate special_ops.hpp file implements more STL savvy specializations. Other more specialized unary_operator implementations may be defined by the client for specific unary operator tags/data types.

All binary_operator except the logical_and_op and logical_or_op have an eval static function that carries out the actual operation. The logical_and_op and logical_or_op d are special because these two operators are short-circuit evaluated.

Short Circuiting || and &&

The logical_and_op and logical_or_op are special due to the C/C++ short circuiting rule, i.e. a || b and a && b are short circuit evaluated. A forwarding operation cannot be used because all function arguments are evaluated before a function is called. logical_and_op and logical_or_op are specialized composites with implied operations.

rank

rank<T> class has a static int constant 'value' that defines the absolute rank of a type. rank<T> is used to choose the result type of binary operators such as +. The type with the higher rank wins and is used as the operator's return type. A generic user defined type has a very high rank and always wins when compared against a user defined type. If this is not desireable, one can write a rank specialization for the type. Here are some predefined examples:

    template <> struct rank<char>           { static int const value = 20; };
    template <> struct rank<signed char>    { static int const value = 20; };
    template <> struct rank<unsigned char>  { static int const value = 30; };
    template <> struct rank<wchar_t>        { static int const value = 40; };

Take note that ranks 0..9999 are reserved by the framework.

A template type computer higher_rank<T0, T1> chooses the type (T0 or T1) with the higher rank. We saw in the binary_operator for plus_op how it was used. Specifically:

    higher_rank<T0, T1>::type

returns either T0 or T1 depending on which has a higher rank. In some operator applications such as a + b, the result is actually the one with the higher rank. For example if a is of type int and b is of type double, the result will be of type double. This facility can also be quite useful for evaluating some functions. For instance if we have a sum(a, b, c, d, e) function, we can call this type computer to get the type with the highest rank:

    higher_rank<TA,
        higher_rank<TB,
            higher_rank<TC,
                higher_rank<TD, TE>::type
            >::type
        >::type
    >::type
When used within templates, be sure to use 'typename' appropriately. See binary_operator<plus_op, T0, T1> above.