Logical concept represents types with a truth value.
Logical is just a
bool, or something that can act like one. However, in the context of programming with heterogeneous objects, it becomes extremely important to distinguish between those objects whose truth value is known at compile-time, and those whose truth value is only known at runtime. The reason why this is so important is because it is possible to branch at compile-time on a condition whose truth value is known at compile-time, and hence the return type of the enclosing function can depend on that truth value. However, if the truth value is only known at runtime, then the compiler has to compile both branches (because any or both of them may end up being used), which creates the additional requirement that both branches must evaluate to the same type.
Logical (almost) represents a boolean algebra, which is a mathematical structure encoding the usual properties that allow us to reason with
bool. The exact properties that must be satisfied by any model of
Logical are rigorously stated in the laws below.
x is said to be true-valued, or sometimes also just true as an abuse of notation, if
x is false-valued, or sometimes just false, if
This provides a standard way of converting any
Logical to a straight
bool. The notion of truth value suggests another definition, which is that of logical equivalence. We will say that two
y are logically equivalent if they have the same truth value. To denote that some expressions
q of a Logical data type are logically equivalent, we will sometimes also write
which is very common in mathematics. The intuition behind this notation is that whenever
p is true-valued, then
q should be; but when
p is false-valued, then
q should be too. Hence,
p should be true-valued when (and only when)
q is true-valued.
All the other functions can be defined in those terms:
As outlined above, the
Logical concept almost represents a boolean algebra. The rationale for this laxity is to allow things like integers to act like
Logicals, which is aligned with C++, even though they do not form a boolean algebra. Even though we depart from the usual axiomatization of boolean algebras, we have found through experience that the definition of a Logical given here is largely compatible with intuition.
The following laws must be satisfied for any data type
L modeling the
Logical concept. Let
c be objects of a
Logical data type, and let
f be arbitrary true-valued and false-valued
Logicals of that data type, respectively. Then,
Why is the above not a boolean algebra?
If you look closely, you will find that we depart from the usual boolean algebras because:
- we do not require the elements representing truth and falsity to be unique
- we do not enforce commutativity of the
- because we do not enforce commutativity, the identity laws become left-identity laws
A data type
T is arithmetic if
std::is_arithmetic<T>::value is true. For an arithmetic data type
T, a model of
Logical is provided automatically by using the result of the builtin implicit conversion to
bool as a truth value. Specifically, the minimal complete definition for those data types is
Rationale for not providing a model for all contextually convertible to bool data types
not_method can not be implemented in a meaningful way for all of those types. For example, one can not cast a pointer type
T*to bool and then back again to
T*in a meaningful way. With an arithmetic type
T, however, it is possible to cast from
Tto bool and then to
Tagain; the result will be
1depending on the truth value. If you want to use a pointer type or something similar in a conditional, it is suggested to explicitly convert it to bool by using
|Return whether all the arguments are true-valued.|
|Conditionally execute one of two branches based on a condition.Given a condition and two branches in the form of lambdas or |
|Conditionally return one of two values based on a condition.Specifically, |
|Negates a |
|Return whether any of the arguments is true-valued.|
|Apply a function to an initial state while some predicate is satisfied.This method is a natural extension of the |
|constexpr auto boost::hana::and_|
Return whether all the arguments are true-valued.
and_ can be called with one argument or more. When called with two arguments,
and_ uses tag-dispatching to find the right implementation. Otherwise,.
|constexpr auto boost::hana::eval_if|
Conditionally execute one of two branches based on a condition.Given a condition and two branches in the form of lambdas or
eval_if will evaluate the branch selected by the condition with
eval and return the result. The exact requirements for what the branches may be are the same requirements as those for the
By passing a unary callable to
eval_if, it is possible to defer the compile-time evaluation of selected expressions inside the lambda. This is useful when instantiating a branch would trigger a compile-time error; we only want the branch to be instantiated when that branch is selected. Here's how it can be achieved.
For simplicity, we'll use a unary lambda as our unary callable. Our lambda must accept a parameter (usually called
_), which can be used to defer the compile-time evaluation of expressions as required. For example,
What happens here is that
eval_if will call
eval on the selected branch. In turn,
eval will call the selected branch either with nothing – for the then branch – or with
hana::id – for the else branch. Hence,
_(x) is always the same as
x, but the compiler can't tell until the lambda has been called! Hence, the compiler has to wait before it instantiates the body of the lambda and no infinite recursion happens. However, this trick to delay the instantiation of the lambda's body can only be used when the condition is known at compile-time, because otherwise both branches have to be instantiated inside the
There are several caveats to note with this approach to lazy branching. First, because we're using lambdas, it means that the function's result can't be used in a constant expression. This is a limitation of the current language.
The second caveat is that compilers currently have several bugs regarding deeply nested lambdas with captures. So you always risk crashing the compiler, but this is a question of time before it is not a problem anymore.
Finally, it means that conditionals can't be written directly inside unevaluated contexts. The reason is that a lambda can't appear in an unevaluated context, for example in
decltype. One way to workaround this is to completely lift your type computations into variable templates instead. For example, instead of writing
you could instead write
Note: This example would actually be implemented more easily with partial specializations, but my bag of good examples is empty at the time of writing this.
Now, this hoop-jumping only has to be done in one place, because you should use normal function notation everywhere else in your metaprogram to perform type computations. So the syntactic cost is amortized over the whole program.
Another way to work around this limitation of the language would be to use
hana::lazy for the branches. However, this is only suitable when the branches are not too complicated. With
hana::lazy, you could write the previous example as
|cond||The condition determining which of the two branches is selected.|
|then||An expression called as |
|else_||A function called as |
|constexpr auto boost::hana::if_|
Conditionally return one of two values based on a condition.Specifically,
then is returned iff
cond is true-valued, and
else_ is returned otherwise. Note that some
Logical models may allow
else_ to have different types, while others may require both values to have the same type.
|cond||The condition determining which of the two values is returned.|
|then||The value returned when |
|else_||The value returned when |
Referenced by boost::hana::literals::operator""_s().
|constexpr auto boost::hana::not_|
Logical.This method returns a
Logical with the same tag, but whose truth-value is negated. Specifically,
not_(x) returns a false-valued
x is a true-valued
Logical, and a true-valued one otherwise.
|constexpr auto boost::hana::or_|
Return whether any of the arguments is true-valued.
or_ can be called with one argument or more. When called with two arguments,
or_ uses tag-dispatching to find the right implementation. Otherwise,.
|constexpr auto boost::hana::while_|
Apply a function to an initial state while some predicate is satisfied.This method is a natural extension of the
while language construct to manipulate a state whose type may change from one iteration to another. However, note that having a state whose type changes from one iteration to the other is only possible as long as the predicate returns a
Logical whose truth value is known at compile-time.
while_(pred, state, f) is equivalent to
f is iterated as long as
pred(f(...)) is a true-valued
|pred||A predicate called on the state or on the result of applying |
|state||The initial state on which |
|f||A function that is iterated on the initial state. Note that the return type of |