C + + generic algorithm customization operation

Posted by Simplicity on Mon, 10 Jan 2022 11:00:56 +0100

Transfer function to algorithm

By default, another version of the generic algorithm is implemented, which accepts an additional parameter. For example, the sort function accepts the third parameter, which is a predicate.
A predicate is a callable expression whose return value is a value that can be used as a condition.
The predicates used by the standard library algorithm are divided into two categories:
unary predicate (meaning they only accept a single parameter) and binary predicate (meaning they have two parameters).
Algorithms that accept predicate parameters call predicates on elements in the input sequence. Therefore, the element type must be convertible to the parameter type of the predicate.
For the time being, we can understand predicates as functions

We use predicates to modify the sort rules of sort

bool isShort(const string &s1, const string &s2)
{
    return s1.size() < s2.size();
}

The above code modifies the rule to sort by length from small to large
Next, we implement a function call to sort and pass the parameter isShort

void use_predicate()
{
    vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    sort(words.begin(), words.end(), isShort);
    for (auto it = words.begin(); it != words.end(); it++)
    {
        cout << *it << endl;
    }
}

Function output above

za
zack
what
hello
no matter

lambda expressions

lambda expressions provide functions similar to functions, which can be understood as an anonymous function. They complete some logical processing by passing parameters and capturing references and values of external variables.
A lambda expression represents a callable unit of code.
We can understand it as an unnamed inline function.
Like any function, a lambda has a return type, a parameter list, and a function body.
But unlike functions, lambda may be defined inside functions.
A lambda expression has the following form

[capture list](parameter list) -> return type {function body}

Capture list refers to the capture list. If the lambda expression is defined inside a function, you can capture the reference or value of the local variable of the function through capture list.
Return type, parameter list and function body, like any ordinary function, represent the return type, parameter list and function body respectively.
We can ignore the return type. lambda can deduce the return type according to the return value.

   auto f = []()
    { return 42; };

In this example, we define a callable object f, which does not accept parameters and returns 42. The calling method of lambda is the same as that of ordinary functions, using the calling operator:

 cout << " f is " << f() << endl;

If the function body of a lambda contains anything other than a single return statement and no return type is specified, void is returned.
We define the isShorter function as a lambda expression

  [](const string &s1, const string &s2) -> bool
    { return s1.size() < s2.size(); };

We can call stable_sort, words of the same length maintain the original sequence

vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    stable_sort(words.begin(), words.end(), [](const string &s1, const string &s2) -> bool
                { return s1.size() < s2.size(); });

We print words

for (auto it = words.begin(); it != words.end(); it++)
    {
        cout << *it << endl;
    }

The output is as follows

za
zack
what
hello
no matter

We use the capture function of lambda expression to implement a function to find the number of words whose length is greater than the specified value.
We first implement a function that sorts words and removes duplicate words

void erase_dup(vector<string> &words)
{
    //First sort the words in words
    sort(words.begin(), words.end());
    // unique moves elements, placing non repeating elements in the front and repeating elements in the back
    // unique returns the position of the last element that is not repeated
    const auto uniqueiter = unique(words.begin(), words.end());
    //Call erase to delete duplicate elements
    words.erase(uniqueiter, words.end());
}

Next, we implement the biggers function to return the number of words greater than the specified length sz

int use_bigger(int sz)
{
    vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    //Sort first to remove duplicate words
    erase_dup(words);
    //Then sort stably, ranging from small to large according to the length
    stable_sort(words.begin(), words.end(), [](const string &s1, const string &s2) -> bool
                { return s1.size() < s2.size(); });
    auto findit = find_if(words.begin(), words.end(), [sz](const string &s)
                          { return s.size() > sz; });

    return words.end() - findit;
}

Let's test it

    cout << "count is " << use_bigger(3) << endl;
    cout << "count is " << use_bigger(5) << endl;
    cout << "count is " << use_bigger(10) << endl;

The program output is as follows

count is 4
count is 1
count is 0

It can be seen that there are 4 words with a length greater than 3, 1 words with a length greater than 5 and 0 words with a length greater than 10.
We capture use by means of a lambda expression [sz]_ The formal parameter sz of the bigger.
If we want to print out all words with a length greater than sz, we can use the foreach function, which accepts three parameters. The first two are iterators representing the traversal range, and the third is an expression representing the operation on each element. We improve the use_bigger function

int use_bigger(int sz)
{
    vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    //Sort first to remove duplicate words
    erase_dup(words);
    //Then sort stably, ranging from small to large according to the length
    stable_sort(words.begin(), words.end(), [](const string &s1, const string &s2) -> bool
                { return s1.size() < s2.size(); });
    auto findit = find_if(words.begin(), words.end(), [sz](const string &s)
                          { return s.size() > sz; });
    for_each(findit, words.end(), [](const string &s)
             { cout << s << " "; });
    cout << endl;
    return words.end() - findit;
}

lambda capture type

lambda capture is divided into value capture and reference capture.
lambda uses value capture. Similar to value transfer parameters, the premise of value capture is that variables can be copied.
Unlike parameters, the values of captured variables are copied when lambda is created, not when called.

void lambda_catch()
{
    int val = 10;
    auto fn = [val]
    { return val; };
    val = 200;
    auto fv = fn();
    cout << "fv is " << fv << endl;
}

The above code fv will output 10 because fn captures the value of val. Val is captured when the lambda expression is created. At this time, the value of Val is 10
If captured by reference

void lambda_catch_r()
{
    int val = 10;
    auto fn = [&val]
    { return val; };
    val = 200;
    auto fv = fn();
    cout << "fv is " << fv << endl;
}

At this time, fv is 200 is output, because fn captures the reference of val.
We can return a lambda from a function, which cannot contain reference capture. Because if lambda contains a reference to a function local variable, the lambda call will crash when the secondary local variable is released.

To capture a common variable, such as int, string or other non pointer types, you can usually use a simple value capture method.
In this case, we only need to pay attention to whether the variable has the value we need when capturing.
If we capture a pointer or iterator or use reference capture, we must ensure that the object bound to the iterator, pointer or reference still exists when lambda executes.
Moreover, you need to ensure that the object has the expected value.
During the time between the creation of a lambda and its execution, there may be code that changes the value of the bound object.
That is, when the pointer (or reference) is captured, the value of the bound object is what we expect, but when lambda is executed, the value of the object may be completely different.
Generally speaking, we should minimize the amount of data captured to avoid problems caused by potential capture. Also, if possible, avoid capturing pointers or references.

Implicit capture

To instruct the compiler to infer the capture list, write an & or =& Tell the compiler to use the capture reference method, = means to use the value capture method.
For example, we modify use_ For the bigger function, add a delimiter of ostream and char to the parameter, and use_bigger internal use for_each call

int use_bigger2(ostream &os, char c, int sz)
{
    vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    //Sort first to remove duplicate words
    erase_dup(words);
    //Then sort stably, ranging from small to large according to the length
    stable_sort(words.begin(), words.end(), [](const string &s1, const string &s2) -> bool
                { return s1.size() < s2.size(); });
    auto findit = find_if(words.begin(), words.end(), [sz](const string &s)
                          { return s.size() > sz; });
    // os is captured by reference, and the other variables c are captured by = value.
    for_each(findit, words.end(), [=, &os](const string &s)
             { os << s << c; });

    // c is captured by value and the rest by reference.
    for_each(findit, words.end(), [&, c](const string &s)
             { os << s << c; });
    cout << endl;
    return words.end() - findit;
}

The above two codes are for_each captures local variables in different implicit ways.

mutable change value

By default, lambda does not change the value of a variable whose value is captured. Lambda can declare mutable, which can modify the captured variable value.

void mutalble_lam()
{
    int val = 100;
    auto fn = [val]() mutable
    {
        return ++val;
    };

    cout << "val is " << val << endl;

    cout << "fn val is " << fn() << endl;

    val = 200;
    cout << "val is " << val << endl;

    cout << "fn val is " << fn() << endl;
}

Program output

val is 100
fn val is 101
val is 200
fn val is 102

fn captures the value of val. because fn is mutable, Val can be modified without affecting external val.

lambda return type

We want to do a function that returns the absolute value of the value in the sequence

void rt_lambda()
{
    vector<int> nums = {-1, 2, 3, -5, 6, 7, -9};
    transform(nums.begin(), nums.end(), nums.begin(), [](int a)
              { return a < 0 ? -a : a; });
    for_each(nums.begin(), nums.end(), [](int a)
             { cout << a << endl; });
}

All values in nums are changed to their absolute values by transform. The first two parameters of transform represent the input sequence, and the third parameter represents the written destination sequence. If the destination sequence iterator is the same as the iterator at the beginning of the input sequence, it represents all elements of the transform sequence. The lambda expression does not write the return value type, but it is an expression of a ternary operator, so lambda can infer the return type. If the lambda expression is written as follows, an error will be reported.

[](int a){ if(a<0) return -a; else return a; }

At this point, we modify the above lambda expression to explicitly write out that the return type is int

 transform(nums.begin(), nums.end(), nums.begin(), [](int a) -> int
              { if (a < 0)  return -a; else return a; });

bind binding parameters

The form of bind is

auto newCallable = bind(callable, arg_list);

newCallable itself is a callable object, arg_list is a comma separated parameter list corresponding to the given callable parameter.
That is, when we call newCallable, newCallable will call callable and pass it arg_list.

arg_ The parameters in the list may contain_ The name of N, where n is an integer. These parameters are "placeholders" representing the parameters of newCallable, which occupy the "position" of the parameters passed to newCallable.
The value n represents the position of the parameter in the generated callable object:_ 1 is the first parameter of newCallable_ 2 is the second parameter, and so on.
We first implement a function to judge the length of a string

bool check_size(const string &str, int sz)
{
    if (str.size() > sz)
    {
        return true;
    }

    return false;
}

check_size returns true if the length of the string str is greater than sz, otherwise false.
Next, use the bind operation to generate a new function that accepts only one sz parameter.
Using the bind function to include the header file functional, you also need to use using namespace std::placeholders

void calsize_count()
{
    string str = "hello";
    //Will check_ The first parameter of size is bound to bind_check
    auto bind_check = bind(check_size, _1, 6);
    //It is equivalent to calling check_size(str,6)
    bool bck = bind_check(str);
    if (bck)
    {
        cout << "check res is true" << endl;
    }
    else
    {
        cout << "check res is false" << endl;
    }
}

Check by bind_ The first parameter of size is bound to bind_check, the second parameter is 6
So call bind_check(str) is equivalent to calling check_size(str,6).
We can use bind to implement find_if because find_ The predicate accepted by if can only have one parameter, so check through bind_ Size is generated as a single parameter function.

int use_bigger3(ostream &os, char c, int sz)
{
    vector<string> words = {"hello", "za", "zack", "no matter", "what"};
    //Sort first to remove duplicate words
    erase_dup(words);
    //Then sort stably, ranging from small to large according to the length
    stable_sort(words.begin(), words.end(), [](const string &s1, const string &s2) -> bool
                { return s1.size() < s2.size(); });
    auto findit = find_if(words.begin(), words.end(), bind(check_size, _1, sz));

    // c is captured by value and the rest by reference.
    for_each(findit, words.end(), [&, c](const string &s)
             { os << s << c; });
    cout << endl;
    return words.end() - findit;
}

Generate a new function through bind and pass it to find_if can be used.
bind greatly facilitates the extensibility of generic programming.
To learn about C + + throughout the year, click the C + + feature article below
Love FengChen official blog

Topics: C++