The Logical
concept represents types with a truth value.
Intuitively, a 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.
More specifically, 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.
A Logical
x
is said to be true-valued, or sometimes also just true as an abuse of notation, if
Similarly, 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 Logical
s x
and y
are logically equivalent if they have the same truth value. To denote that some expressions p
and 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.
eval_if
, not_
and while_
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 Logical
s, 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 a
, b
and c
be objects of a Logical
data type, and let t
and f
be arbitrary true-valued and false-valued Logical
s 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
and_
andor_
operations- 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 The
not_
method can not be implemented in a meaningful way for all of those types. For example, one can not cast a pointer typeT*
to bool and then back again toT*
in a meaningful way. With an arithmetic typeT
, however, it is possible to cast fromT
to bool and then toT
again; the result will be0
or1
depending 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 usingto<bool>
.
Variables | |
constexpr auto | boost::hana::and_ |
Return whether all the arguments are true-valued. More... | |
constexpr auto | boost::hana::eval_if |
Conditionally execute one of two branches based on a condition. More... | |
constexpr auto | boost::hana::if_ |
Conditionally return one of two values based on a condition. More... | |
constexpr auto | boost::hana::not_ |
Negates a Logical . More... | |
constexpr auto | boost::hana::or_ |
Return whether any of the arguments is true-valued. More... | |
constexpr auto | boost::hana::while_ |
Apply a function to an initial state while some predicate is satisfied. More... | |
|
constexpr |
#include <boost/hana/fwd/and.hpp>
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 |
#include <boost/hana/fwd/eval_if.hpp>
Conditionally execute one of two branches based on a condition.
Given a condition and two branches in the form of lambdas or hana::lazy
s, 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 eval
function.
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 eval_if
anyway.
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 eval(then) if cond is true-valued. |
else_ | A function called as eval(else_) if cond is false-valued. |
|
constexpr |
#include <boost/hana/fwd/if.hpp>
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 then
and 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 cond is true-valued. |
else_ | The value returned when cond is false-valued. |
|
constexpr |
#include <boost/hana/fwd/not.hpp>
Negates a Logical
.
This method returns a Logical
with the same tag, but whose truth-value is negated. Specifically, not_(x)
returns a false-valued Logical
if x
is a true-valued Logical
, and a true-valued one otherwise.
|
constexpr |
#include <boost/hana/fwd/or.hpp>
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 |
#include <boost/hana/fwd/while.hpp>
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.
Specifically, while_(pred, state, f)
is equivalent to
where f
is iterated as long as pred(f(...))
is a true-valued Logical
.
pred | A predicate called on the state or on the result of applying f a certain number of times to the state, and returning whether f should be applied one more time. |
state | The initial state on which f is applied. |
f | A function that is iterated on the initial state. Note that the return type of f may change from one iteration to the other, but only while pred returns a compile-time Logical . In other words, decltype(f(stateN)) may differ from decltype(f(stateN+1)) , but only if pred(f(stateN)) returns a compile-time Logical . |