Boost.Hana  1.5.0
Your standard library for metaprogramming
Logical

Description

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.

Truth, falsity and logical equivalence

A Logical x is said to be true-valued, or sometimes also just true as an abuse of notation, if

if_(x, true, false) == true

Similarly, x is false-valued, or sometimes just false, if

if_(x, true, false) == false

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 Logicals 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

p if and only if q

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.

Minimal complete definition

eval_if, not_ and while_

All the other functions can be defined in those terms:

if_(cond, x, y) = eval_if(cond, lazy(x), lazy(y))
and_(x, y) = if_(x, y, x)
or_(x, y) = if_(x, x, y)
etc...

Laws

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 a, b and c be objects of a Logical data type, and let t and f be arbitrary true-valued and false-valued Logicals of that data type, respectively. Then,

// associativity
or_(a, or_(b, c)) == or_(or_(a, b), c)
and_(a, and_(b, c)) == and_(and_(a, b), c)
// equivalence through commutativity
or_(a, b) if and only if or_(b, a)
and_(a, b) if and only if and_(b, a)
// absorption
or_(a, and_(a, b)) == a
and_(a, or_(a, b)) == a
// left identity
or_(a, f) == a
and_(a, t) == a
// distributivity
or_(a, and_(b, c)) == and_(or_(a, b), or_(a, c))
and_(a, or_(b, c)) == or_(and_(a, b), and_(a, c))
// complements
or_(a, not_(a)) is true-valued
and_(a, not_(a)) is false-valued

Why is the above not a boolean algebra?

If you look closely, you will find that we depart from the usual boolean algebras because:

  1. we do not require the elements representing truth and falsity to be unique
  2. we do not enforce commutativity of the and_ and or_ operations
  3. because we do not enforce commutativity, the identity laws become left-identity laws

Concrete models

hana::integral_constant

Free model for arithmetic data types

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

eval_if(cond, then, else_) = cond ? then(id) : else(id)
not_(cond) = static_cast<T>(cond ? false : true)
while_(pred, state, f) = equivalent to a normal while loop

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 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 T to bool and then to T again; the result will be 0 or 1 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 using to<bool>.

Variables

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,. More...
 
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 hana::lazys, 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. More...
 
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 then and else_ to have different types, while others may require both values to have the same type. More...
 
constexpr auto boost::hana::not_
 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. More...
 
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,. More...
 
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. More...
 

Variable Documentation

constexpr auto boost::hana::and_

#include <boost/hana/fwd/and.hpp>

Initial value:
= [](auto&& x, auto&& ...y) -> decltype(auto) {
return tag-dispatched;
}

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,.

and_(x) == x
and_(x, y, ...z) == and_(and_(x, y), z...)

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
namespace hana = boost::hana;
BOOST_HANA_CONSTANT_CHECK(hana::and_(hana::true_c, hana::true_c, hana::true_c, hana::true_c));
static_assert(!hana::and_(hana::true_c, false, hana::true_c, hana::true_c), "");
int main() { }
constexpr auto boost::hana::eval_if

#include <boost/hana/fwd/eval_if.hpp>

Initial value:
= [](auto&& cond, auto&& then, auto&& else_) -> decltype(auto) {
return tag-dispatched;
}
constexpr auto then
Sequentially compose two monadic actions, discarding any value produced by the first but not its effe...
Definition: then.hpp:36

Conditionally execute one of two branches based on a condition.Given a condition and two branches in the form of lambdas or hana::lazys, 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.

Deferring compile-time evaluation inside eval_if

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,

template <typename N>
auto fact(N n) {
return hana::eval_if(n == hana::int_c<0>,
[] { return hana::int_c<1>; },
[=](auto _) { return n * fact(_(n) - hana::int_c<1>); }
);
}

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

template <typename T>
struct pointerize : decltype(
hana::eval_if(hana::traits::is_pointer(hana::type_c<T>),
[] { return hana::type_c<T>; },
[](auto _) { return _(hana::traits::add_pointer)(hana::type_c<T>); }
))
{ };

you could instead write

template <typename T>
auto pointerize_impl(T t) {
return hana::eval_if(hana::traits::is_pointer(t),
[] { return hana::type_c<T>; },
[](auto _) { return _(hana::traits::add_pointer)(hana::type_c<T>); }
);
}
template <typename T>
using pointerize = decltype(pointerize_impl(hana::type_c<T>));

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

template <typename T>
struct pointerize : decltype(
hana::eval_if(hana::traits::is_pointer(hana::type_c<T>),
hana::make_lazy(hana::type_c<T>),
hana::make_lazy(hana::traits::add_pointer)(hana::type_c<T>)
))
{ };
Parameters
condThe condition determining which of the two branches is selected.
thenAn expression called as eval(then) if cond is true-valued.
else_A function called as eval(else_) if cond is false-valued.

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
namespace hana = boost::hana;
// eval_if with heterogeneous branches and a Constant condition
BOOST_HANA_CONSTEXPR_LAMBDA auto safe_make_unsigned = [](auto t) {
return hana::eval_if(hana::traits::is_integral(t),
hana::make_lazy(hana::traits::make_unsigned)(t),
hana::make_lazy(t)
);
};
BOOST_HANA_CONSTANT_CHECK(safe_make_unsigned(hana::type_c<void>) == hana::type_c<void>);
BOOST_HANA_CONSTANT_CHECK(safe_make_unsigned(hana::type_c<int>) == hana::type_c<unsigned int>);
// eval_if with homogeneous branches and a constexpr or runtime condition
BOOST_HANA_CONSTEXPR_LAMBDA auto safe_divide = [](auto x, auto y) {
return hana::eval_if(y == 0,
[=](auto) { return 0; },
[=](auto _) { return _(x) / y; }
);
};
int main() {
BOOST_HANA_CONSTEXPR_CHECK(safe_divide(6, 3) == 2);
BOOST_HANA_CONSTEXPR_CHECK(safe_divide(6, 0) == 0);
}
constexpr auto boost::hana::if_

#include <boost/hana/fwd/if.hpp>

Initial value:
= [](auto&& cond, auto&& then, auto&& else_) -> decltype(auto) {
return tag-dispatched;
}
constexpr auto then
Sequentially compose two monadic actions, discarding any value produced by the first but not its effe...
Definition: then.hpp:36

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.

Parameters
condThe condition determining which of the two values is returned.
thenThe value returned when cond is true-valued.
else_The value returned when cond is false-valued.

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
namespace hana = boost::hana;
static_assert(hana::if_(true, 1, 2) == 1, "");
static_assert(hana::if_(false, 1, 2) == 2, "");
static_assert(
hana::if_(hana::true_c,
hana::make_tuple('t', 'r', 'u', 'e'),
hana::make_tuple('f', 'a', 'l', 's', 'e')
)
==
hana::make_tuple('t', 'r', 'u', 'e')
, "");
int main() { }

Referenced by boost::hana::literals::operator""_s().

constexpr auto boost::hana::not_

#include <boost/hana/fwd/not.hpp>

Initial value:
= [](auto&& x) -> decltype(auto) {
return tag-dispatched;
}

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.

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
namespace hana = boost::hana;
BOOST_HANA_CONSTANT_CHECK(hana::not_(hana::true_c) == hana::false_c);
static_assert(hana::not_(false) == true, "");
int main() { }
constexpr auto boost::hana::or_

#include <boost/hana/fwd/or.hpp>

Initial value:
= [](auto&& x, auto&& ...y) -> decltype(auto) {
return tag-dispatched;
}

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,.

or_(x) == x
or_(x, y, ...z) == or_(or_(x, y), z...)

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
namespace hana = boost::hana;
BOOST_HANA_CONSTANT_CHECK(hana::or_(hana::false_c, hana::false_c, hana::true_c));
BOOST_HANA_CONSTANT_CHECK(!hana::or_(hana::false_c, hana::false_c, hana::false_c));
int main() { }
constexpr auto boost::hana::while_

#include <boost/hana/fwd/while.hpp>

Initial value:
= [](auto&& pred, auto&& state, auto&& f) -> decltype(auto) {
return tag-dispatched;
}

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

f(...f(f(state)))

where f is iterated as long as pred(f(...)) is a true-valued Logical.

Parameters
predA 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.
stateThe initial state on which f is applied.
fA 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.

Example

// Copyright Louis Dionne 2013-2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
#include <vector>
namespace hana = boost::hana;
using namespace hana::literals;
int main() {
// while_ with a Constant condition (loop is unrolled at compile-time)
{
std::vector<int> ints;
auto final_state = hana::while_(hana::less.than(10_c), 0_c, [&](auto i) {
ints.push_back(i);
return i + 1_c;
});
// The state is known at compile-time
BOOST_HANA_CONSTANT_CHECK(final_state == 10_c);
BOOST_HANA_RUNTIME_CHECK(ints == std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
}
// while_ with a constexpr or runtime condition (loop is not unrolled)
{
std::vector<int> ints;
int final_state = hana::while_(hana::less.than(10), 0, [&](int i) {
ints.push_back(i);
return i + 1;
});
// The state is known only at runtime, or at compile-time if constexpr
BOOST_HANA_RUNTIME_CHECK(final_state == 10);
BOOST_HANA_RUNTIME_CHECK(ints == std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
}
}