Sometimes it is helpful to compute the result of logical operations like and
and or
at compile-time. These operations return a boolean result, so the first thing a compile-time logic library needs is classes to represent true and false results. Here is one way to do it.
template<bool c>
struct bool_ {
typedef bool_ type;
typedef bool value_type;
static bool const value = c;
operator bool() const { return c; }
};
typedef bool_<true> true_;
typedef bool_<false> false_;
It would also have been possible to simply write
struct true_ { };
struct false_ { };
but the nested typedef
s and bool
will be helpful later on. The implicit conversion to bool
is occasionally helpful but code that uses it can always be rewritten to make it unnecessary, for instance by using tag dispatching.
Before we continue with the presentation of the code, let's describe the algorithms we are writing. In the case of the short-circuit and
, it is this: if the first argument is false, then the result is false; otherwise it is the result of applying short-circuit and
to the remaining arguments. If there is only one argument, then the result is the same as the argument. The short-circuit or
is similar: if the first argument is true, then the result is true; otherwise it is the result of applying short-circuit or
to the remaining arguments. If there is only one argument, then the result is the same as the argument. Note that as stated these algorithms apply even to the logical and
or or
of a single argument, but for consistency with the built-in &&
and ||
operators we will require at least two arguments.
It should be clear from the statement of the algorithms that we need some means of branching, that is to say some means of representing an if-then-else
construct. This is accomplished by the if_
class. if_
has three template parameters: a boolean condition and two types. If the condition is true, then if_
selects the first type; otherwise it selects the second type.
template<bool c, class T1, class T2>
struct if_c { typedef T1 type; };
template<class T1, class T2>
struct if_c<false,T1,T2> { typedef T2 type; };
template<class C, class T1, class T2>
struct if_ : if_c<C::value,T1,T2> { };
There are two versions of if_
. The first is called if_c
and uses a bool
template parameter for its condition. The second is called if_
and uses a class with a nested value
for its condition; because value
is passed to if_c
, it must be a type convertible to bool
at compile-time.
Now we understand the purpose of the value
nested in bool_
. It allows that class to be used with if_
as a condition.
We are nearing the finish line now, but before we continue let's step back from the details of the code and take a high-level look at what we intend to achieve. We are going to write a class named and_
that takes two or more template arguments, computes the short-circuit logical and
of the arguments, and exposes the result as a nested type named type
that will be a typedef
for either true_
or false_
.
We already described the algorithm that computes the result; now we describe its implementation: if the first argument to and_
is false, then and_
inherits false_
; otherwise it inherits the result of applying and_
to the remainder of its arguments. Now we understand the purpose of the type type
nested in bool_
: when and_
inherits false_
(or true_
, as the case may be), it also inherits the appropriate nested type.
We intend to make and_
inherit from one class or another based on a condition, but this is useful behavior in its own right so let's encapsulate it in a class named eval_if
. Like if_
, eval_if
takes a boolean condition and two types as arguments. If the condition is true, then eval_if
inherits the first type; otherwise it inherits the second type. As with if_
, there are two versions of eval_if
: one that takes a bool
condition, and one that takes a class with a nested value
.
template<bool c, class F1, class F2>
struct eval_if_c : if_c<c,F1,F2>::type { };
template<class C, class F1, class F2>
struct eval_if : if_<C,F1,F2>::type { };
Now we are ready to implement and_
.
template<class... F>
struct and_;
template<class F1, class F2, class... F>
struct and_<F1,F2,F...> : eval_if<F1,and_<F2,F...>,false_>::type { };
template<class F1, class F2>
struct and_<F1,F2> : eval_if<F1,eval_if<F2,true_,false_>,false_>::type { };
Let's examine the two specializations of and_
one at a time. The critical bit of code in the first is eval_if<F1,and_<F2,F...>,false_>::type
. This names a type. If F1
is false, then the type is false_
; otherwise it is and_<F2,F...>
. Thus if the first argument to and_
is false, then it inherits false_
; otherwise it recurses and inherits and_<F2,F...>
.
The second specialization ends the recursing of the first when and_
is evaluating exactly two arguments. If the first argument to and_
is false, then this specialization inherits false_
; otherwise it inherits eval_if<F2,true_,false_>
, which in turn inherits either either true_
or false_
depending on F2
.
The implementation of logical or
is similar.
template<class... F>
struct or_;
template<class F1, class F2, class... F>
struct or_<F1,F2,F...> : eval_if<F1,true_,or_<F2,F...>>::type { };
template<class F1, class F2>
struct or_<F1,F2> : eval_if<F1,true_,eval_if<F2,true_,false_>>::type { };
The class names used here are borrowed from the the Boost Metaprogramming Library, which inspired this post. The implementation is my own.