functional.h - JonathanCline/JCLib GitHub Wiki

jclib/functional.h

Documentation and examples for using the jclib/functional.h header.


Terminology

  • operator - Any struct or class that inherits from jc::operator_tag and has an operator() overload defined.

Basic Design

The goal of the functional.h header and its contents is to provide a system for composing function objects together to reduce the amount of code it takes to write common lambdas and to simplify the usage of C++ code that uses a user-provided function (ie. std::find_if).

Here is an example of the systems provided in action:

bool has_number(const std::vector<std::unique_ptr<int>>& vec, int number)
{
   // Creates a new function object that is equivalent to (*x == number)
   const auto cmp = jc::dereference | jc::equals & number;

   // Call find_if using cmp as the predicate
   return std::find_if(vec.begin(), vec.end(), cmp);
}; 

Piping

Piping refers to invoking an operator with a value or with the results from calling another operator.

Here is an example of argument piping :

// Here we are piping the variable "a" into the negation operator.
int a = 5;

// This is the same as "-a" 
int na = a | jc::negate; // "na" is now equal to -5


// Here we are piping the variable "b" into the negation operator and then piping that result into the negation operator again.
int b = 9;

// This is the same as "-(-b)"
int nb = b | jc::negate | jc::negate; // b is now equal to 5

Piping will only work if you are piping a value to an operator that actually accepts said value.

int a = 6;

// This will not work as jc::plus takes 2 arguments
int sum = a | jc::plus; // compilation error!

Binding

An operator can have arguments "bound" to them to create a new operator with one argument now filled by the "bound" value. This is useful for common operator usage patterns.

Here is an example of argument binding:

// This is the equivalent of jc::plus(x, 1)
auto inc = jc::plus & 1;

// "inc" is a new operator that only takes 1 argument. It still calls jc::plus but now one of the jc::plus arguments will be 1
int a = 0;
int b = a | inc;        // b is equal to 1
int c = a | inc | inc;  // c is equal to 2

The side that the argument is bound on is important. See the following code snippet:

// This operator will return true if a value is greater than 4
auto a = jc::greater & 4; // a > 4

// This operator will return true if 4 is greater than a value
auto b = 4 & jc::greater; // 4 > a

// The results will be different!!!
int n = 8;
auto aresult = n | a;
auto bresult = n | b;

Composition

An operator can be composed with another operator to form a new operator using the regular piping semantics |.

This new operator will pipe the results from the left hand operator into the right hand operator, allowing the user to generate functionality by composing existing operators together.

Here is an small example of composition:

// This will check if a number plus 2 is equal to 4
auto f = (jc::plus & 2) | jc::equals & 4;     // (a + 2) == 4

int n = 2;
auto b = n | f;     // b will be "true" as 2 + 2 is equal to 4

Argument Packs

Argument packs allow for an operator with multiple arguments to be invoked with a single value. In order to prevent accidentally invoking operators incorrectly, this uses a special type that is essentially an invariant of std::tuple - jc::argpack.

jc::pack() is a function that can be used to make an argument pack from a set of arguments.

jc::pack(2, 2); // creates an argument pack containing (2, 2)

int n = 0;
jc::pack(n, 2); // creates an argument pack containing (&n, 2);

Here is an example of using argument packs to call jc::plus

auto sum = jc::pack(2, 2) | jc::plus; // 2 + 2 = 4 !

When created, argument packs will not make copies of given arguments! This means that if you create an argument pack with a variable, the argument pack will hold a reference to it. If the variable being referenced goes out of scope, the argument pack is now invalid and using it will result in undefined behavior.

auto foo()
{
   int n = 0;
   return jc::pack(n, 2);  // THIS IS NOT GOOD!!!!! "n" is a local variable and will go out of scope!!!
};

auto p = foo();
auto s = p | jc::plus;   // Undefined Behavior as "p" contains a reference to a variable that went out of scope ("n")

If you have a type that has a jc::get_ftor specialization it can be turned into an argument pack using the jc::repack operator. Anything in the standard library that works with std::get works by default (for example: std::tuple and std::pair).

An argument pack can also be used with jc::repack, returning the same argument pack.

// Create a pair containing (2, 2)
auto args = std::pair<int, int>{ 2, 2 };

// Turn the pair into an argument pack and call plus with it
auto sum = args | jc::repack | jc::plus;   // sum is equal to 4

// Calling repack multiple times is ok and just keeps returning the same argument pack
auto sum2 = args | jc::repack | jc::repack | jc::plus; // same as the previous line of code

// NOTICE: This will fail to compile! The only way to call an operator with multiple arguments using a single value is with an actual jc::argpack
auto bad = args | jc::plus;    // error! jc::plus takes 2 arguments, only 1 was provided
⚠️ **GitHub.com Fallback** ⚠️