![]() |
Sponsored Link •
|
Advertisement
|
My CS201 "Software Development Methods" professor taught that
all anyone would ever need to know about C++'s ternary
conditional operator (?:
) was that it was poorly
understood and best avoided. He was right about one thing: the
conditional operator is poorly understood, by me at
least. Ten years later, I've only begun to appreciate the
subtlety and the power of the conditional operator. But contrary
to what my professor would have me believe, it is a unique and
indispensable tool. In fact, the lowly conditional operator is
the magic ingredient in BOOST_FOREACH
, a macro that
makes it simple to loop over STL collections, arrays,
null-terminated strings, and iterator ranges, among other things.
In this article, I'll zoom in on this operator and discover its
unique properties. Using the conditional operator, you will see
how to encode the type of an expression without evaluating it and
how to detect whether an expression is an lvalue or an rvalue.
Thus armed, I'll take aim at a very vexing problem: how to write
a "safe" macro that does not reevaluate its arguments. Along the
way, I will implement a toy FOREACH
macro that
allows iteration over any STL container.
This article takes a very close look at a very small part of the language—the conditional operator. I make occasional reference to the standard, and the analysis gets fairly technical. Consider yourself warned.
In the November 2003 issue of the C/C++ Users' Journal, Anson
Tsao and I published an article entitled "FOREACH and
LOCK
" in which we described how to bring the functionality
of C#'s foreach
and lock
keywords to
C++. The FOREACH
macro lets you iterate over STL
containers with syntax like this:
vector< string > vect_str( /* ... */ ); FOREACH( string str, vect_str ) { cout << str << '\n'; }
Why would you want to do such a thing? It is more concise and
less error-prone than using iterators directly, certainly, but
wouldn't it be better to use the std::for_each
algorithm? It depends. std::for_each
is a good idea,
but in practice it is often a clumsy tool. If C++ had native
support for lambda expressions, std::for_each
would
make sense, but in the language we have today
std::for_each
forces you to move your loop body into
a function (or function object) at namespace scope, far from
where it will be used. Code that uses std::for_each
extensively is littered with these small, one-off functions that
make little sense in isolation. Also, you can't use
break
or continue
once you are
committed to using std::for_each
. By comparison,
FOREACH
clearly expresses the programmer's intent,
is easy to use correctly, and lets you put your loop body where
it makes sense. If you think that macros are ugly, you'll get no
argument from me. A foreach
keyword and lambda
functions sure would be nice, but that's not the language we have
to work with, unfortunately. And as we'll see, the conditional
operator is just the tool we need to teach those evil, unruly
macros some manners.
Listing 1 in file broken_foreach.cpp shows a bare-bones,
stripped down version of the FOREACH
macro as it was
implemented at the time. Don't sweat the details yet; we'll get
to them in short order. This version has problems—it evaluates
the container expression multiple times, and it doesn't work if
the container is an unnamed temporary object. The following code
illustrates the reevaluation problem. It uses
FOREACH
to loop over a container of doubles, but the
container expression, get_vect_dbl()
, is a
function, which leads to some surprising behavior.
vector<double> vect_dbl( 3, 1.0 ); vector<double> const & get_vect_dbl() { cout << "here!\n"; return vect_dbl; } // elsewhere .... FOREACH( double d, get_vect_dbl() ) { cout << d << '\n'; }You might expect to see "here!" displayed only once, but what you actually see is:
here! here! here! here! 1 here! here! here! 1 here! here! here! 1 here! here!This is because
get_vect_dbl()
is being
evaluated multiple times by the FOREACH
macro.
Surprise! This is a typical macro bug-a-boo, and one of the reasons
why macros have such a bad reputation. Squashing this bug is our
first order of business, but first we'll need to understand a bit
about what the FOREACH
macro is doing, and why.
Imagine that it is your job to implement the
FOREACH
macro. "Well, I'll probably need some
iterators," you think to yourself, and you write something like
this:
#define FOREACH( item, container ) \ ??? iter = (container).begin();Now you're stuck. What type do you use to declare your iterator? You don't know the type of the container, and you certainly don't know the type of its iterators. Somehow you must be able to declare variables for the iterators without knowing the iterators' type! It's not impossible, but it isn't obvious, either. Let's see how.
First, we use the ScopeGuard trick [1]: temporary objects of derived type can have their lifetime extended by binding them to a const reference to the base type. This is a magic property of const references and temporary objects. If you're surprised, don't worry—it surprises everyone at first. The following code illustrates the trick. It defines an empty base class, a derived class which wraps some piece of data, and a function which returns an iterator, suitably wrapped.
struct auto_any_base {}; template< class T > struct auto_any : auto_any_base { auto_any( T const & t ) : item( t ) {} mutable T item; }; template< class Container > auto_any< typename Container::const_iterator > begin( Container const & c ) { return c.begin(); }Now, in our
FOREACH
macro we can say:
#define BOOST_FOREACH( item, container ) \ auto_any_base const & iter = begin( container ); \ ...
begin( container )
returns the begin iterator,
wrapped in an auto_any
temporary object. That
temporary object gets bound to iter
, which is a const
reference to auto_any_base
, the empty base class. This
gives the temporary object a reprieve of sorts—its lifetime is
extended, along with the iterator stored inside.[2] As the name "auto_any
"
suggests, this is a general mechanism for putting an object of
unknown type in automatic storage (i.e. it is not dynamically
allocated using new).
We have succeeded in storing an iterator, but we have lost its
type information. iter
is a const reference to an
auto_any_base—that tells us nothing about what is actually stored
in there. It could be a vector iterator
, or
it could be your Auntie May's Christmas fruit cake. If we want to
be able to increment the iterator, for instance, we need to
recover the lost type information.
next()
below does just that.
// extract a reference to the internally stored item template< class T > T & auto_any_cast( auto_any_base const & base ) { return static_cast< auto_any< T > const & >( base ).item; } // infer the type of the iterator template< class Container > void next( auto_any_base const & iter, Container const& ) { ++auto_any_cast< typename Container::const_iterator >( iter ); }The function
auto_any_cast()
takes a const
reference to an auto_any_base
and casts it to the
requested derived type. It uses a static_cast
for
this; this is kosher because we are allowed to
static_cast
from a base to a derived type. It returns
a reference to the contained piece of data. The function
next()
uses auto_any_cast()
to access the
iterator and increment it. We use next()
by passing it
both iter
and the container, so that
next()
can deduce the type of the container and infer
the type of the iterator. If you recall, the begin()
function defined above puts a
Container::const_iterator
into the
auto_any
. The function next()
must follow
suit by casting back to a Container::const_iterator
.
Notice that the container is not actually used by
next()
for anything besides template argument
deduction. The snippet below shows how this all hangs together.
#define FOREACH( item, container ) \ auto_any_base const & iter = begin( container ); \ ... \ next( iter, container ); \ ...This works very well as long as container is something simple, like a variable of type
vector
. But imagine
what happens if container
is a function call. Since
container
appears twice, it will get called twice. And
if the function has side-effects, all heck breaks loose. This is
the crux of the problem. It is especially galling considering that
in the call to next()
, we don't even need the
container; we just need its type. There's gotta be a better way!
If only there were a way to get the type of an expression
without evaluating the expression. There is! The unique
properties of the conditional operator allow us to sidestep the
issue entirely. The ENCODED_TYPEOF
macro defined
below encodes the type of an expression without evaluating it.
Read on to see how it works.
// a simple type wrapper template< class T > struct type2type {}; // convert an expression of type T to an // expression of type type2type<T> template< class T > type2type< T > encode_type( T const & t ) { return type2type< T >(); } // convertible to type2type<T> for any T struct any_type { template< class T > operator type2type< T > () const { return type2type< T >(); } }; // convert an expression of type T to an // expression of type type2type<T> without // evaluating the expression #define ENCODED_TYPEOF( container ) \ ( true ? any_type() : encode_type( container ) )For any expression
expr
of type
Expr
, ENCODED_TYPEOF(expr)
evaluates to
type2type<Expr>
, and (here's the important
part) the expression expr
is not
evaluated! There is some subtlety in
ENCODED_TYPEOF
, so it is worth a few words to describe
what's going on.
At the heart of ENCODED_TYPEOF
is the conditional
operator. As you probably know, the conditional operator behaves
like an if/else statement. It takes three operands: (1) a Boolean
condition, (2) an expression to evaluate if the condition is
true, and (3) an expression to evaluate if the condition is
false. The most familiar use of the conditional operator is to
implement the infamous min/max
macros:
#define min(a,b) ( (a) < (b) ? (a) : (b) ) #define max(a,b) ( (a) > (b) ? (a) : (b) )Looks simple, right? Don't be fooled, there's more going on here than meets the eye. A conditional expression is just that—an expression. As such, it must have a type. The type of a conditional expression depends on the types of both its second and third operands—the two branches of the condition. Figuring out what the resulting type of a conditional expression is from the types of its two branches is enough to give even the most seasoned compiler writer a splitting headache.
Glossing over some complications which become important later,
it works as follows: for a conditional expression ( b ? x :
y )
where x
is an expression of type
X
and y
is an expression of type
Y
where X
and Y
are
different, and one is a class type, the compiler checks to see if
X
can be converted to Y
and if
Y
can be converted to X
. If only one,
unambiguous conversion is found, that is the one that gets used.
Things get more complicated still, but this is enough to
understand ENCODED_TYPEOF
.
If you recall, ENCODED_TYPEOF(container)
expands
to (true ? any_type() : encode_type(container))
. So
X
is any_type
and Y
is
type2type<Container>
. Now we try the
conversions. type2type<Container>
cannot be
converted to any_type
, but any_type
can be converted to
type2type<Container>
because it defines the
appropriate conversion operator. We have found one, unambiguous
conversion, so we're done. The type of
ENCODED_TYPEOF(container)
is
type2type<Container>
.
You may be thinking this is needlessly convoluted. After all,
couldn't we just use encode_type(container)
without
the conditional operator and get the same result? No, because
that would have caused container
to be evaluated.
With the conditional operator, only one branch of the condition
is ever executed. In this case, since the condition is always
true, the first branch will always be taken. The second branch is
"dead code"—it will never execute—yet it extends a ghostly finger
into the land of the living and exerts its influence on the
conditional expression's type. Spooky!
By using ENCODED_TYPEOF
in our
FOREACH
macro, we can avoid reevaluating the
container expression needlessly. All we have to do is change our
function templates to take
type2type<Container>
instead of
Container const &
, as follows:
template< class Container > void next( auto_any_base const & iter, type2type<Container > ) { ++auto_any_cast< typename Container::const_iterator >( iter ); } // elsewhere ... #define FOREACH( item, container ) \ auto_any_base const & iter = begin( container ); \ ... \ next( iter, ENCODED_TYPEOF( container ) ); \ ...The call to
begin()
above evaluates container
and returns container.begin()
. It gets bound to the
const reference a-la the ScopeGuard trick. And then the call to
next()
increments the iterator without reevaluating
container. Perfect!
Now that we have a way of ensuring that the container
expression only gets evaluated once, we can tackle the other
problem with the FOREACH
macro: making it work with
rvalue container expressions. First, some terminology. The C++
standard uses the terms lvalue and rvalue to discriminate between
objects that have names (lvalues) and those that don't
(rvalues)[3]. Every variable
you explicitly declare is an lvalue. rvalues are more elusive;
they are the unnamed temporary objects that flit into and out of
existence as a result of operations, conversions and function
calls.
Consider the following invocation of FOREACH
:
FOREACH( char ch, string("Hello") ) { cout << ch; }This should write every character in the
string
to cout
, but making it do that is no mean trick. The
problem is that string("Hello")
is a temporary object
- an rvalue. It winks out of existence before we have a chance to
iterate over it, unless we pin it down. The problem is obvious once
we expand the macro a bit:
auto_any_base const & iter = begin( string("Hello") ); ...Here, we see that
begin()
is going to return an
iterator to a temporary string
object that is going to
die at the semicolon. An iterator into a deceased container isn't
very useful, so clearly something must be done. Let's make a copy
of the container and iterate over that! We need to store the copy
somewhere, but we don't know the container's type (in general). But
hey, we already know how to solve this problem! Just wrap the copy
in an auto_any
and bind it to a const reference to
auto_any_base
. That will ensure that the container
sticks around long enough to iterate over it. Of course, making a
copy of an STL container is an expensive proposition, and we don't
want to have to pay that price when we don't have to. If the
container is an lvalue (that is, a named object), we don't have to
make a copy because it is not in danger of going out of scope while
we iterate over it. Only when the container is an rvalue does
FOREACH
actually need to make a copy.
FOREACH
should automatically detect whether the container
is an lvalue or an rvalue, and store a copy only if it is needed.
When mathematicians have a problem, they sometimes start by
assuming the solution. It has always seemed like
cheating to me, but let's use the same trick here. Let's assume
(for now) that we can detect the lvalue-ness or rvalue-ness of an
expression and write the result into a Boolean flag called
is_rvalue
. Then, we could pass the container and
is_rvalue
to a helper function, which returned
either a pointer to the container (for lvalues) or a copy (for
rvalues). Since we need to return a thingy or a
pointer-to-thingy, we need something like a union for the return
type. boost::variant
[4] is a perfect fit. The helper
function looks like this:
// contain() - returns a Container or a Container* template< class Container > auto_any< boost::variant<Container const*,Container> > contain( Container const & t, bool const & is_rvalue ) { // Return either a Container or a Container* depending on whether // the container is an rvalue or not. typedef boost::variant<Container const*,Container> variant_t; return is_rvalue ? variant_t(t) : variant_t(&t); }(In case you are wondering why
contain()
takes
is_rvalue
by reference-to-const instead of by value,
the reason becomes clear later.) Now that the container is wrapped
in a variant
, which is wrapped in an
auto_any
, we need to make our begin()
function privy to this subterfuge. begin()
will take
the container all wrapped up, the Boolean is_rvalue
flag, and the encoded type of the container, and it will return the
container's begin iterator, like this:
// begin() - returns the begin iterator template< class Container > auto_any< typename Container::const_iterator > begin( auto_any_base const & container, bool is_rvalue,type2type<Container> ) { typedef boost::variant<Container const*,Container> variant_t; variant_t & var = auto_any_cast< variant_t>(container); // Extract either a Container or a Container* depending on whether // the container is an rvalue or not. Container const & c = is_rvalue ? boost::get<Container>(var) : *boost::get<Container const *>(var); return c.begin(); }We've assumed that there is some way to detect the lvalue- ness or rvalue-ness of an expression. Let's call it
EVAL
and further assume that it is a macro that takes
an expression and a Boolean flag. It evaluates the expression and
returns it. As a side-effect, it writes into the flag whether the
expression is an rvalue or not. Using the hypothetical
EVAL
macro, we can make FOREACH
work with
rvalues like this:
#define FOREACH( item, container ) \ bool is_rvalue; \ auto_any_base const & cont = contain(EVAL(container,is_rvalue), is_rvalue ); \ auto_any_base const & iter = begin( cont, is_rvalue, ENCODED_TYPEOF(container) ); \ ...The function
contain()
takes two arguments:
EVAL(container,is_rvalue)
and is_rvalue
.
The first argument writes to is_rvalue
as a
side-effect, and the second argument passes is_rvalue
to contain()
. If this strikes you as dodgy, you're
right. In C++, argument evaluation order is unspecified. What if
the second argument (is_rvalue
) is evaluated before
the first argument writes to it? Will the contain()
function see garbage for the value of is_rvalue
? No,
and this is why contain()
takes is_rvalue
by reference-to-const instead of by value. Since we are passing a
reference, the value of is_rvalue
is not read until we
are inside the contain()
function, at which point all
of its function arguments have been evaluated and all their
side-effects are visible. Had we just passed is_rvalue
by value, all bets would be off. The moral is: don't rely on the
evaluation order of function arguments!
If we merely assume the existence of
EVAL
, our C++ compiler will complain. (Assumed
solutions are good enough for mathematicians, but not C++
compilers.) We need to be more explicit, but detecting whether an
expression is an lvalue or an rvalue presents a challenge
reminiscent of the Heisenberg Uncertainty Principle—the act of
measuring it tends to change the very property you are trying to
measure [5]. Once again, the
lowly conditional operator saves the day. The following code
illustrates the principle by defining a macro
RVALUE_TEST
that displays "rvalue" for rvalue
expressions, and "lvalue" for lvalue expressions. We'll get to
the details in a minute.
struct rvalue_probe { template< class R > operator R () { throw "rvalue"; } template< class L > operator L & () const { throw "lvalue"; } }; #define RVALUE_TEST(container) \ try { ( true ? rvalue_probe() : (container) ); } \ catch( char const * result ) { cout <<result << '\n'; }Now you can use
RVALUE_TEST(expression)
to test
the rvalue-ness or lvalue-ness of an expression. For instance,
RVALUE_TEST(cout)
will display "lvalue" since cout is
a named object. But RVALUE_TEST(make_pair(1,2))
will
display "rvalue". It's not immediately obvious how this
works[6], so we'll need to
consult the Good Book (the C++ standard) to make sense of it.
According to section 5.16/3:
This section is describing the case of
RVALUE_TEST(cout)
. Since cout
is an
lvalue of type ostream
, the compiler first tries to
convert the expression rvalue_probe()
to type
"reference to ostream
". This succeeds, because
rvalue_probe
defines a conversion operator to
L &
for any L
. This conversion operator
gets called, and it throws the string "lvalue", which gets caught
and printed.
The other case is more interesting. If we read further in the standard, it says (deep breath):
This is actually much simpler than it sounds. If the
expression is an rvalue of type R
, then the compiler
will try to convert rvalue_probe()
to an rvalue of
type R
. But there's a catch. (You knew it couldn't
be that simple, right?) There are two ways for this conversion to
happen. The compiler could chose to use
rvalue_probe::operator R()
and be done, or it could
use rvalue_probe::operator L &() const
, and then
use the standard lvalue-to-rvalue conversion (4.1) to achieve the
same result.
Two possible conversions! Rather than throw up its hands, the
compiler performs overload resolution to pick the best conversion
sequence. Let's not go into the subtleties of overload resolution
(Heaven help us!), but the end result is that operator
R()
gets picked over operator L & ()
const
.
If you've made it this far, congratulations! There's no doubt
that this is arcane stuff, but we are rewarded with a robust way
to detect the rvalue-ness and lvalue-ness of any expression. We
can use this technique to finally implement the EVAL
macro assumed above. Just a few small tweaks to the
rvalue_probe struct
give it the ability to pass an
object through unchanged while recording whether it is an rvalue
or lvalue. It is explained in detail below.
struct rvalue_probe { template< class T > rvalue_probe( T const & t, bool & b ) : ptemp( const_cast< T * >( &t ) ), is_rvalue( b ) {} template< class R > operator R() { is_rvalue = true; return *static_cast< R * >( ptemp ); } template< class L > operator L &() const { is_rvalue = false; return *static_cast< L * >( ptemp ); } void * ptemp; bool & is_rvalue; };And now at last we can define the EVAL macro as:
#define EVAL( container, is_rvalue ) \ ( true ? rvalue_probe( (container), is_rvalue ) : (container) )The
rvalue_probe
object stores a pointer to the
container expression container and a reference to a Boolean flag.
The other operand of the condition operator is container, raw and
unadorned. As before, the second operand is "dead code" that will
never execute, but its rvalue-ness or lvalue-ness will influence
which conversion operator gets called on the
rvalue_probe
object. Both conversion operators do
essentially the same thing: dereference the pointer to the
container expression and return the result. As a side-effect, it
records in the Boolean flag which conversion operator was called.
The upshot is that the is_rvalue
flag can be used by
the rest of the FOREACH
macro to either make a copy of
the container or not, accordingly. Listing 2 (see fixed_foreach.cpp) uses this trick to
improve upon the code in Listing
1—it does not reevaluate the container expression, and it works
with rvalue containers.
You may not have thought much about the conditional operator
before now, but you have to admit, there is a lot there to love.
The standard devotes a whole page and a half of dense standardese
to this curious little beast, and we have just scratched the
surface. But I think you'll forgive me if I take my language
lawyer hat off now. I encourage you to download the code for
BOOST_FOREACH
. There you will find a version that
works with other types of sequences besides STL containers.
BOOST_FOREACH
is currently under consideration for
inclusion in Boost. You can find the code in the
Boost Sandbox File Vault in the file
foreach.zip
.
BOOST_FOREACH
to work with other collection types
besides STL containers. Finally, I would like to thank Anson Tsao
for his initial insight which led to BOOST_FOREACH
in the first place.
FOREACH
expand to one big statement, which makes
it play nicely with any surrounding code. I leave the if/else
statements out here because it would only obscure the
point.operator L & () const
is
const- qualified and operator R ()
is not. Since
the expression rvalue_probe()
is not const, in
order to call a const- qualified member function, the compiler
must add a const qualification to the rvalue_probe
object. This is considered a conversion. As a result, the
conversion sequence using operator R()
is shorter
than the sequence using operator L &() const
,
and hence it is preferred.
Sponsored Links
|