Boost logo

Boost :

Subject: Re: [boost] [Fit] Purpose and Use Cases
From: Paul Fultz II (pfultz2_at_[hidden])
Date: 2016-02-16 19:39:47


> It isn't clear to me from the documentation what are the real world use
cases?

Well, I go over some simple use cases in quick start guide, maybe its not
entirely clear. As the Fit library is a collection of generic utilities
related to functions, there is many useful cases with the library, but a key
point of many of these utilities is that they can solve these problems with
much simpler constructs than whats traditionally been done with
metaprogramming.

Initialization
--------------

The `BOOST_FIT_STATIC_FUNCTION` will help initialize function objects at
global scope, and will ensure that it is initialized at compile-time and (on
platforms that support it) will have a unique address across translation
units, thereby reducing executable bloat and potential ODR violations.

In addition, `BOOST_FIT_STATIC_LAMBDA_FUNCTION` allows initializing a lambda
in the same manner. This allows for simple and compact function definitions
when working with generic lambdas and function adaptors.

Of course, the library can still be used without requiring global function
objects for those who prefer to avoid them will still find the library
useful.

Conditional overloading
-----------------------

Take a look at this example of defining a `stringify` function from
stackoverflow here:

http://stackoverflow.com/questions/30189926/metaprograming-failure-of-function-definition-defines-a-separate-function/30515874

The user would like to write `stringify` to call `to_string` where
applicable
and fallback on using `sstream` ot convert to a string. Most of the top
answers usually involve some amount of metaprogramming using either `void_t`
or `is_detected`. However, with the Fit library it can simply be written
like
this:

BOOST_FIT_STATIC_LAMBDA_FUNCTION(stringify) = conditional(
    [](auto x) BOOST_FIT_RETURNS(to_string(x)),
    [](auto x)
BOOST_FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);

In addition, this can be used with `fit::if_` to create static if-like
constructs. For example, Baptiste Wicht discusses how one could write static
if in C++ here:

http://baptiste-wicht.com/posts/2015/07/simulate-static_if-with-c11c14.html

He wants to be able to write this:

template<typename T>
void decrement_kindof(T& value){
    static_if(std::is_same<std::string, T>::value){
        value.pop_back();
    } else {
        --value;
    }
}

However, that isn't possible in C++. With the Fit library one can simply
write
this:

template<typename T>
void decrement_kindof(T& value)
{
    eval(conditional(
        if_(std::is_same<std::string, T>())([&](auto id){
            id(value).pop_back();
        }),
        [&](auto id){
            --id(value);
        }
    ));
}

The advantage of using the Fit library instead of the solution in Baptiste
Wicht's blog is `conditional` allows more than just two conditions. So if
there was another trait to be checked, such as `is_stack`, it could be
written
like this:

template<typename T>
void decrement_kindof(T& value)
{
    eval(conditional(
        if_(is_stack<T>())([&](auto id){
            id(value).pop();
        }),
        if_(std::is_same<std::string, T>())([&](auto id){
            id(value).pop_back();
        }),
        [&](auto id){
            --id(value);
        }
    ));
}

Furthermore, this technique can be used to write type traits as well. Jens
Weller was looking for a way to define a general purpose detection for
pointer
operands(such as `*` and `->`). One way to accomplish this is using Fit like
this:

template<class T>
auto has_pointer_member(const T&) -> decltype(
    &T::operator*,
    &T::operator->,
    std::true_type{}
);

BOOST_FIT_STATIC_LAMBDA_FUNCTION(has_pointer_operators) = conditional(
    BOOST_FIT_LIFT(has_pointer_member),
    [](auto* x) -> -> bool_constant<(!std::is_void<decltype(*x)>())>,
    always(std::false_type)
);

template<class T>
struct is_dereferenceable
: decltype(has_pointer_operators(std::declval<T>()))
{}

Which is much simpler than the other implementations that were written(it
was
about 3 times the amount of code):

https://gist.github.com/lefticus/6fdccb18084a1a3410d5

Projections
-----------

Instead of writing the projection multiple times in algorithms:

std::sort(std::begin(people), std::end(people),
          [](const Person& a, const Person& b) {
            return a.year_of_birth < b.year_of_birth;
          });

We can use the `by` adaptor to project `year_of_birth` on the comparison
operator:

std::sort(std::begin(people), std::end(people),
        by(&Person::year_of_birth, _ < _));

Ordering evaluation of arguments
--------------------------------

When we write `f(foo(), bar())` there is no guarantee from the standard what
order the arguments `foo()` and `bar()` are evaluated. So with `apply_eval`
we
can order them from left-to-right:

apply_eval(f, [&]{ return foo(); }, [&]{ return bar(); });

Varidiac parameters
-------------------

As shown in the guide the `by` adaptor can be used to apply a function to
each
argument, so we could write a simple varidiac print function like this:

BOOST_FIT_STATIC_FUNCTION(print) = by(std::cout << _);

We can also take binary functions and turn them easily into varidiac
functions
as well using `compress`. So a varidiac `max` function could be written like
this:

BOOST_FIT_STATIC_FUNCTION(max) = compress(BOOST_FIT_LIFT(std::max));

Polymorphic constructors
------------------------

Writing polymorphic constructors(such as `make_tuple`) is a boilerplate that
has to be written over and over again for template classes. With `construct`
this is easier. For example, `make_tuple` can be written simply as this:

BOOST_FIT_STATIC_FUNCTION(make_tuple) = construct<std::tuple>().by(decay());

Extension methods
-----------------

Chaining many function together, like what is done for range based libraries
can make things hard to read:

auto r = transform(
    filter(
        numbers,
        [](int x) { return x > 2; }
    ),
    [](int x) { return x * x; }
);

It would be nice to write this:

auto r = numbers
    .filter([](int x) { return x > 2; })
    .transform([](int x) { return x * x; });

However, UFCS in C++17 won't allow this to be done generically. So instead
pipable functions can be used. So it can be written like this:

auto r = numbers
    | filter([](int x) { return x > 2; })
    | transform([](int x) { return x * x; });

Now, if some users feel a little worried about overloading the bit or
operator, pipable functions can also be used with flow like this:

auto r = flow(
    filter([](int x) { return x > 2; }),
    transform([](int x) { return x * x; })
)(numbers);

No fancy or confusing operating overloading and everything is still quite
readable.

> What can't be done today (for example composition) with simple C++11
lambdas?

First, its not a replacement for lambdas, but rather it builds on top of
lambdas(and function objects) in C++, especially in C++14.

Some of the limitations of lambdas in C++11 of course, is that they are not
generic, and they don't support constexpr(not even in C++14). The Fit
library
provides a small lambda emulation using the `lazy` adaptor(like what is done
with `std::bind` or Boost.Lambda) with the main purpose to support some
simple
`constexpr` lambda expressions. Of course, it is not a full emulation like
what is done in Boost.Lambda or Boost.Phoenix. However, I have found that in
practice emulated-lambda expressions are generally only useful for small
simple expressions.

> I'd love to see several real world examples and use cases of Boost.FIT
> because at least for me I don't really understand why it is useful.

Well hopeful between this and the quick start guide, you can see some of the
usefulness of the library. Let me know if you have more questions.

Paul


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk