2
0
mirror of https://github.com/boostorg/hof.git synced 2026-02-23 15:52:12 +00:00
Files
hof/doc/src/more_examples.md
2016-03-11 10:38:15 -05:00

7.0 KiB

More examples

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. Lets take look at some of the use cases for using the Fit library.

Initialization

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

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

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

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

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 that 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{}
);

FIT_STATIC_LAMBDA_FUNCTION(has_pointer_operators) = conditional(
    FIT_LIFT(has_pointer_member),
    [](auto* x) -> bool_constant<(!std::is_void<decltype(*x)>())> { return {}; },
    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, which were about 3 times the amount of code(see here).

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()), the standard does not guarantee the order in which the foo() and bar() arguments are evaluated. So with apply_eval we can order them from left-to-right:

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

Variadic parameters

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

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

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

FIT_STATIC_FUNCTION(max) = compress(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:

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

Extension methods

Chaining many functions 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 bitwise 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.