Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is an old version of boost. Click here for the latest version's documentation home page.
PrevUpHomeNext

Starter Kit

Values
References
Arguments
Composites
Lazy Operators
Lazy Statements
Construct, New, Delete, Casts
Lazy Functions
More

Most "quick starts" only get you a few blocks from where you are. From there, you are on your own. Yet, typically, you'd want to get to the next city. This starter kit shall be as minimal as possible, yet packed as much power as possible.

So you are busy and always on the go. You do not wish to spend a lot of time studying the library. You wish to be spared the details for later when you need it. For now, all you need to do is to get up to speed as quickly as possible and start using the library. If this is the case, this is the right place to start.

This chapter is by no means a thorough discourse of the library. For more information on Phoenix, please take some time to read the rest of the User's Guide. Yet, if you just want to use the library quickly, now, this chapter will probably suffice. Rather than taking you to the details of the library, we shall try to provide you with annotated exemplars instead. Hopefully, this will get you into high gear quickly.

Functors everywhere

Phoenix is built on function objects (functors). The functor is the main building block. We compose functors to build more complex functors... to build more complex functors... and so on. Almost everything is a functor.

[Note] Note

Functors are so ubiquitous in Phoenix that, in the manual, the words "functor" and "function" are used interchangeably.

Values are functions! Examples:

val(3)
val("Hello, World")

The first evaluates to a nullary function (a function taking no arguments) that returns an int, 3. The second evaluates to a nullary function that returns a char const(&)[13], "Hello, World".

Lazy Evaluation

Confused? val(3) is a unary function, you say? Yes it is. However, read carefully: "evaluates to a nullary function". val(3) evaluates to (returns) a nullary function. Aha! val(3) returns a function! So, since val(3) returns a function, you can invoke it. Example:

cout << val(3)() << endl;

(See values.cpp)

The second function call (the one with no arguments) calls the nullary function which then returns 3. The need for a second function call is the reason why the function is said to be Lazily Evaluated. The first call doesn't do anything. You need a second call to finally evaluate the thing. The first call lazily evaluates the function; i.e. doesn't do anything and defers the evaluation for later.

Callbacks

It may not be immediately apparent how lazy evaluation can be useful by just looking at the example above. Putting the first and second function call in a single line is really not very useful. However, thinking of val(3) as a callback function (and in most cases they are actually used that way), will make it clear. Example:

template <typename F>
void print(F f)
{
    cout << f() << endl;
}

int
main()
{
    print(val(3));
    print(val("Hello World"));
    return 0;
}

(See callback.cpp)

References are functions. They hold a reference to a value stored somehere. For example, given:

int i = 3;
char const* s = "Hello World";

we create references to i and s this way:

ref(i)
ref(s)

Like val, the expressions above evaluates to a nullary function; the first one returning an int&, and the second one returning a char const*&.

(See references.cpp)

Arguments are also functions? You bet!

Until now, we have been dealing with expressions returning a nullary function. Arguments, on the other hand, evaluate to an N-ary function. An argument represents the Nth argument. There are a few predefined arguments arg1, arg2, arg3, arg4 and so on (and it's BLL counterparts: _1, _2, _3, _4 and so on). Examples:

arg1 // one-or-more argument function that returns its first argument
arg2 // two-or-more argument function that returns its second argument
arg3 // three-or-more argument function that returns its third argument

argN returns the Nth argument. Examples:

int i = 3;
char const* s = "Hello World";
cout << arg1(i) << endl;        // prints 3
cout << arg2(i, s) << endl;     // prints "Hello World"

(See arguments.cpp)

What we have seen so far, are what are called primitives. You can think of primitives (such as values, references and arguments) as atoms.

Things start to get interesting when we start composing primitives to form composites. The composites can, in turn, be composed to form even more complex composites.

You can use the usual set of operators to form composites. Examples:

arg1 * arg1
ref(x) = arg1 + ref(z)
arg1 = arg2 + (3 * arg3)
ref(x) = arg1[arg2] // assuming arg1 is indexable and arg2 is a valid index

Note the expression: 3 * arg3. This expression is actually a short-hand equivalent to: val(3) * arg3. In most cases, like above, you can get away with it. But in some cases, you will have to explicitly wrap your values in val. Rules of thumb:

  • In a binary expression (e.g. 3 * arg3), at least one of the operands must be a phoenix primitive or composite.
  • In a unary expression (e.g. arg1++), the single operand must be a phoenix primitive or composite.

If these basic rules are not followed, the result is either in error, or is immediately evaluated. Some examples:

ref(x) = 123    // lazy
x = 123         // immediate

ref(x)[0]       // lazy
x[0]            // immediate

ref(x)[ref(i)]  // lazy
ref(x)[i]       // lazy (equivalent to ref(x)[val(i)])
x[ref(i)]       // illegal (x is not a phoenix primitive or composite)
ref(x[ref(i)])  // illegal (x is not a phoenix primitive or composite)

First Practical Example

We've covered enough ground to present a real world example. We want to find the first odd number in an STL container. Normally we use a functor (function object) or a function pointer and pass that in to STL's find_if generic function:

Write a function:

bool
is_odd(int arg1)
{
    return arg1 % 2 == 1;
}

Pass a pointer to the function to STL's find_if algorithm:

find_if(c.begin(), c.end(), &is_odd)

Using Phoenix, the same can be achieved directly with a one-liner:

find_if(c.begin(), c.end(), arg1 % 2 == 1)

The expression arg1 % 2 == 1 automagically creates a functor with the expected behavior. In FP, this unnamed function is called a lambda function. Unlike the function pointer version, which is monomorphic (expects and works only with a fixed type int argument), the Phoenix version is fully polymorphic and works with any container (of ints, of longs, of bignum, etc.) as long as its elements can handle the arg1 % 2 == 1 expression.

(See find_if.cpp)

Lazy statements? Sure. There are lazy versions of the C++ statements we all know and love. For example:

if_(arg1 > 5)
    cout << arg1

Say, for example, we wish to print all the elements that are greater than 5 (separated by a comma) in a vector. Here's how we write it:

for_each(v.begin(), v.end(),
    if_(arg1 > 5)
    [
        cout << arg1 << ", "
    ]
);

(See if.cpp)

You'll probably want to work with objects. There are lazy versions of constructor calls, new, delete and the suite of C++ casts. Examples:

construct<std::string>(arg1, arg2)  // constructs a std::string from arg1, arg2
new_<std::string>(arg1, arg2)       // makes a new std::string from arg1, arg2
delete_(arg1)                       // deletes arg1 (assumed to be a pointer)
static_cast_<int*>(arg1)            // static_cast's arg1 to an int*
[Note] Note

Take note that, by convention, names that conflict with C++ reserved words are appended with a single trailing underscore '_'

As you write more lambda functions, you'll notice certain patterns that you wish to refactor as reusable functions. When you reach that point, you'll wish that ordinary functions can co-exist with phoenix functions. Unfortunately, the immediate nature of plain C++ functions make them incompatible.

Lazy functions are your friends. The library provides a facility to make lazy functions. The code below is a rewrite of the is_odd function using the facility:

struct is_odd_impl
{
    template <typename Arg>
    struct result
    {
        typedef bool type;
    };

    template <typename Arg>
    bool operator()(Arg arg1) const
    {
        return arg1 % 2 == 1;
    }
};

function<is_odd_impl> is_odd;

Things to note:

  • result is a nested metafunction that reflects the return type of the function (in this case, bool). This makes the function fully polymorphic: It can work with arbitrary Arg types.
  • There are as many Args in the result metafunction as in the actual operator().
  • is_odd_impl implements the function.
  • is_odd, an instance of function<is_odd_impl>, is the lazy function.

Now, is_odd is a truly lazy function that we can use in conjunction with the rest of phoenix. Example:

find_if(c.begin(), c.end(), is_odd(arg1));

(See function.cpp)

Predefined Lazy Functions

The library is chock full of STL savvy, predefined lazy functions covering the whole of the STL containers, iterators and algorithms. For example, there are lazy versions of container related operations such as assign, at, back, begin, pop_back, pop_front, push_back, push_front, etc. (See Container).

As mentioned earlier, this chapter is not a thorough discourse of the library. It is meant only to cover enough ground to get you into high gear as quickly as possible. Some advanced stuff is not discussed here (e.g. Scopes); nor are features that provide alternative (short-hand) ways to do the same things (e.g. Bind vs. Lazy Functions).


PrevUpHomeNext