万字长文【C++】函数式编程【上】

2022-06-16 16:59:39 浏览数 (1)

Part1函数式编程

https://www.manning.com/books/functional-programming-in-c-plus-plus

命令式编程—首先是面向过程的C语言,然后是面向对象的C 和Java,函数式编程无须改变编译器就可以扩展编程预言的能力,使得结构简化,仍然是面向对象。

1第1章 函数式编程简介

面向对象的范式基于对数据的抽象,它允许编程者隐藏对象的内部细节,并通过对象的API给其他部分留下可以操作的接口。

函数式编程 (FP, functional programming)基于对函数的抽象,可以创建比底层语言提供的复杂得多的控制结构。

下面给出一个例子进行说明:

假设要读取一系列文件,并统计每个文件的行数

1, 命令式编程的方式:

1, 打开每个文件 2,定义一个 counter变量保存行数 3,逐个读取文件的每个字符,并在读取到换行符时将counter 1 4,一个文件读取结束,存储计算的行数

代码语言:javascript复制
std::vector<int>
count_lines_in_files_Order(const std::vector<std::string> &files)
{
    std::vector<int> results;

    std::vector<int> results;
    char c =0;
    for(const auto &file:files)
    {
        int line_count = 0;
        std::ifstream in(file);

        while (in.get(c))
        {
            if(c == 'n')
            {
                line_count  ;
            }
        }
        results.push_back(line_count);
    }

    return results;
}

有几处容易出现的错误:初始化有问题的变量,不适宜的更新状态,错误的循环条件。

2,声明式编程的方式:

1,不需要关心统计是如何进行的,只需要说明在给定的流中统计换行符的数目就可以 2,使用抽象来表述用户的目的,而不是说明如何去做 3,使用std::count, 不用手动计算行数目

std::count

https://vimsky.com/examples/usage/std-count-cpp-stl.html

返回给定范围内元素出现的次数,返回 [first, last)范围内等于 val的元素数目

代码语言:javascript复制
// Returns count of occurrences of value in
// range [begin, end]
int count(Iterator first, Iterator last, T &val)

first, last:Input iterators to the initial and final positions of the sequence of elements.
val: Value to match
代码语言:javascript复制
/**
 * This function opens a file specified by the filename argument,
 * and counts the number of lines in said file
 */
int count_lines(const std::string &filename)
{
    std::ifstream in(filename);
    in.unsetf(std::ios_base::skipws);

    // We are creating an iterator over the input stream and
    // passing it to the count algorithm to count the number
    // of newline characters
    return std::count(
        std::istream_iterator<char>(in),
        std::istream_iterator<char>(),
        'n');
}

/**
 * Given a list of files, this function returns a list of
 * line counts for each of them
 */
std::vector<int>
count_lines_in_files(const std::vector<std::string> &files)
{
    std::vector<int> results;

    for (const auto &file: files) {
        results.push_back(count_lines(file));
    }

    return results;
}

以上解决方案的优点:需要考虑的状态变量更少,而且可以在更高的抽象层次上表达自己,而没有必要指定求得结果的每一步细节。

2PLUS.基于范围的foreach循环

http://www.4k8k.xyz/article/cunchi4221/107471404

C 11中引入了foreach循环,或更具体地说, 基于范围的for循环 。这种类型的for循环结构简化了对可迭代数据集的遍历。它通过消除初始化过程并遍历每个元素而不是遍历迭代器来做到这一点。

https://blog.csdn.net/weixin_42136255/article/details/102468959

for循环

代码语言:javascript复制
#include <iostream>
#include <set>
 
using namespace std;
 
void main()
{
 int a[] = {1,2,3,4,5};
 
 for (int i = 0; i < sizeof(a) / sizeof(*a); i  )
 {
  cout << a[i] << endl;
 }
 
 cout << endl;
 
 //set<int> v = {4,3,2,1};
 
 set<int> v = { 0,1,2,3,4,5};
 
 for (auto it = v.begin(); it != v.end(); it  )
 {
  cout << *it << endl;
 }
 
 cin.get();
}

foreach循环

代码语言:javascript复制
#include <iostream>
#include <set>
 
using namespace std;
 
void main()
{
 int a[] = {1,2,3,4,5};
 
 for (auto i : a)
 {
  cout << i << endl;
 }
 
 cout << endl;
 
 set<int> v = {4,3,2,1,0,8,5};
 
 for (auto i : v) //从小到大进行排序,然后才输出,并且里面没有重复的元素,如果有重复的元素,则会剔除
 {
  cout << i << endl;
 }
 
 cin.get();
}

std::transform

https://blog.csdn.net/fengbingchun/article/details/63252470

在指定范围内应用于给定的操作,并将结果存储在指定的另一个范围内。

代码语言:javascript复制
template <class InputIterator, class OutputIterator, class UnaryOperation>
  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperation op);
 
template <class InputIterator1, class InputIterator2,
          class OutputIterator, class BinaryOperation>
  OutputIterator transform (InputIterator1 first1, InputIterator1 last1,
                            InputIterator2 first2, OutputIterator result,
                            BinaryOperation binary_op);

对于一元操作,将op应用于[first1, last1)范围内的每个元素,并将每个操作返回的值存储在以result开头的范围内。给定的op将被连续调用last1-first1次。op可以是函数指针或函数对象或lambda表达式。

如op的一个实现 即将[first1, last1)范围内的每个元素加5,然后依次存储到result中。

代码语言:javascript复制
int op_increase(int i) {return (i   5)};
调用std::transform的方式如下:

std::transform(first1, last1, result, op_increase);

对于二元操作,使用[first1, last1)范围内的每个元素作为第一个参数调用binary_op,并以first2开头的范围内的每个元素作为第二个参数调用binary_op,每次调用返回的值都存储在以result开头的范围内。给定的binary_op将被连续调用last1-first1次。binary_op可以是函数指针或函数对象或lambda表达式。

如binary_op的一个实现即将first1和first2开头的范围内的每个元素相加,然后依次存储到result中。

代码语言:javascript复制
 int op_add(int, a, int b) {return (a   b)};
调用std::transform的方式如下:

std::transform(first1, last1, first2, result, op_add);

std::transform支持in place,即result和first1指向的位置可以是相同的。std::transform的主要作用应该就是省去了我们自己写for循环实现。

代码语言:javascript复制
/**
 * This function opens a file specified by the filename argument,
 * and counts the number of lines in said file
 */
int count_lines(const std::string &filename)
{
    std::ifstream in(filename);

    // We are creating an iterator over the input stream and
    // passing it to the count algorithm to count the number
    // of newline characters
    return std::count(
        std::istream_iterator<char>(in >> std::noskipws),
        std::istream_iterator<char>(),
        'n');
}

/**
 * Given a list of files, this function returns a list of
 * line counts for each of them
 */
std::vector<int>
count_lines_in_files(const std::vector<std::string> &files)
{
    // Since we know the size of the resulting vector, we can
    // preallocate the needed number of elements
    std::vector<int> results(files.size());

    // Instead of using the raw for loop like in the count-lines-stdcount
    // example, we are using the std::transform algorithm to
    // convert the list of file names into a list of line counts
    std::transform(files.cbegin(), files.cend(),
                   results.begin(),
                   count_lines);

    return results;
}

优点:省去算法步骤,移除状态变量,依赖标准库,使得代码不容易出错

缺点:包含太多的模板代码,可读性差

3.range管道操作符

https://zhuanlan.zhihu.com/p/47315860

纯函数(Pure functions)

https://zhuanlan.zhihu.com/p/135962442

FP的核心思想是纯函数:满足一个条件:输入值确定,输出值就确定的函数

函数只使用而不修改传递给他们的实际参数计算结果,如果使用相同的实参多次调用纯函数,将得到相同的结果,并不会留下调用痕迹,意味着纯函数不能改变程序的状态。纯函数的调用除了接收他的返回结果之外,看不到任何执行的痕迹。

纯函数编写的并行程序很简单,因为这些函数并不修改任何东西,不需要原子或信号量进行显式同步,可以把用于单线程的代码,几乎不加修改地用于多线程系统。

以函数的方式思考问题:

应该考虑输入是什么,输出是什么。从输入到输出需要什么样的转换,而不是思考算法的步骤。

C 是一种泛型编程语言:

STL提供的向量模板,可用于不同的类型,包括整型,字符型和其他满足前置条件的类型,编译器会对每一种特定的类型优化代码,被称作“静态或编译时多态”,与之相对的是动态或运行时多态,是靠继承或虚函数支持的。

例子:纯函数,输出完全依赖输入

代码语言:javascript复制
priate static count(double money){
    return money * 0.8;
}

非纯函数:输出结果不是完全由输入结果决定的

代码语言:javascript复制
priate static count(double money){
    double rate = 0.8
    if(today() > Date(2020.4.24)){
        rate = 0.85;
    }
    return money * rate;
}

非纯函数带来的问题:输出结果并非完全由输入结果确定的,这会使得我们的测试变得很麻烦,因为不同的时间测试的结果可能不一样!我同样的输入参数,今天执行和明天执行,得到的结果不一样,那么,对于函数使用者而言,他回觉得这个函数有问题,毕竟他不知道函数内部实现是怎样的。

改写纯函数

代码语言:javascript复制
priate static count(Date date,double money){
    double rate = 0.8
    if(date > Date(2020.4.24)){
        rate = 0.85;
    }
    return money * rate;
}

2第2章 函数式编程之旅

函数式编程语言的特色:

函数可被看作一个普通的值,它们可被存储于变量中,放到集合或结构中,作为参数传递给其他函数,并可以作为其他函数的返回结果。

高阶函数:能够接收函数作为参数或返回函数作为结果的函数。

过滤和转换是通用的编程模式,许多程序员在项目中不断重复

2.1.求平均值

假设有一个电影评分列表,需要计算它的平均分。

命令式编程如下:

代码语言:javascript复制
// Imperative version
double average_score(const std::vector<int> &scores)
{
    int sum = 0;

    for (int score: scores) {
        sum  = score;
    }

    return sum / (double) scores.size();
}

容易出错:遍历集合时用错类型,在循环中因输入错误改变了代码的语义,但仍允许其编译。并且,累加很容易地在多核上并行执行,甚至可由硬件完成。

函数式编程:

std::accumulate

是一个高阶函数,提供了对递归结构,如向量、列表和树等的遍历处理,并允许逐步构建自己需要的结果。

https://blog.csdn.net/weixin_37160123/article/details/93845663

代码语言:javascript复制
// 1. 无op
template <class InputIterator, class T>
   T accumulate (InputIterator first, InputIterator last, T init);
 
// 2. 有op
template <class InputIterator, class T, class BinaryOperation>
   T accumulate (InputIterator first, InputIterator last, T init,
                 BinaryOperation binary_op);

accumulate接收了三个参数,使用迭代器来标识开始和结束区间,即第一个参数为开始区间,第二个参数为结束区间,而第三个参数至关重要,它不仅仅是累加的初值,还规定了该函数返回值的类型。这就要求迭代器元素的类型要与第三个参数的类型一致,亦或者是能够转化(类似于double--->int,会损失精度) 对于有运算操作的accumulate使用,我们第一个参数与第二个参数与无运算操作的accumulate一样,均是迭代器的起始与结束位置,第三个是我们进行运算的初始值,最后一个参数是我们自定义的操作,通常可以通过定义一个函数或者是使用lamda式子来进行定义;并且该函数不仅仅限于累加操作,累乘累除均可以,只要自定义。

代码语言:javascript复制
//1
//将数组从开头起至结尾的每一个元素进行相加,最后再加上35,返回结果。
vector<int> vec {2, 0, 12, 3, 5, 0, 2, 7, 0, 8};
std::accumulate(vec.begin(),vec.end(),35);

//2
//自动过滤掉小于3的数组元素,然后进行自定义的运算操作,即累加
std::vector<int> values {2, 0, 12, 3, 5, 0, 2, 7, 0, 8};
int min {3};
auto sum = std::accumulate(std::begin(values), std::end(values), 0, [min] (int sum, int v)
{
    if(v < min)
        return sum;
    return sum   v;
});
std::cout << "The sum of the elements greater than " << min-1<<"is " << sum << std::endl;  // 35

代码语言:javascript复制
// Calculating the average score with std::accumulate.
// By default, accumulate uses addition as the folding operation
// over a collection
// (see section 2.2.1)
double average_score(const std::vector<int> &scores)
{
    return std::accumulate(
               scores.cbegin(), scores.cend(),
               0
            ) / (double) scores.size();
}

// We can provide a custom operation. In this case,
// we are multiplying all the scores.
double scores_product(const std::vector<int> &scores)
{
    return std::accumulate(
               scores.cbegin(), scores.cend(),
               1,
               std::multiplies<int>()
            );
}

统计换行符数目

代码语言:javascript复制
#include <iostream>
#include <numeric>

// Implementing counting through folding.
// We have our previous count, and we are returning the new count
// after checking out the current character.
int counter(int previous_count, char c)
{
    return (c != 'n') ? previous_count
                       : previous_count   1;
}

int count_lines(const std::string &s)
{
    return std::accumulate(
            s.cbegin(), s.cend(),
            0,
            counter
        );
}

int main(int argc, char *argv[])
{
    std::cout << count_lines("an ancient pond n"
                             "a frog jumps in n"
                             "the splash of watern")
              << 'n';

  return 0;
}

std::reduce

标准算法的并行版本

https://bobjin.com/blog/c_cpp_docs/reference/en/cpp/algorithm/reduce.html

从C 17开始,STL提供了使算法并行执行的操作,需要传递 std::execution::par,注意需要理解STL内部实现的机制,有些不能并行

std::accumulate保证集合中的每个元素逐个累加,使得不改变其他行为的情况下不可能将它并行化。并行得累加,可以用std::reduce算法

目前还不支持 g 编译

https://www.zhihu.com/question/278066373

代码语言:javascript复制
// We can provide a custom operation. In this case,
// we are multiplying all the scores.
double scores_product(const std::vector<int> &scores)
{
    // return std::accumulate(
    //            scores.cbegin(), scores.cend(),
    //            1,
    //            std::multiplies<int>()
    //         );

    return std::reduce(std::exception::par,
            scores.cbegin(), scores.cend(),
            0
        )/ (double) scores.size();

}

2.2.删除字符串空白符

假设给定一个字符串,需要除去开头和结尾的空白字符。

std::find_if

http://www.cplusplus.com/reference/algorithm/find_if/

查找集合中第一个满足指定胃词的元素,返回一个迭代器,指向字符串中满足胃词函数的第一个字符。删除字符串中从开头到这个元素的所有字符,也就删除了所有前面的空白符。

代码语言:javascript复制
template<class InputIterator, class UnaryPredicate>
  InputIterator find_if (InputIterator first, InputIterator last, UnaryPredicate pred)
{
  while (first!=last) {
    if (pred(*first)) return first;
      first;
  }
  return last;
}

为在输入迭代器所定义的范围内查找单个对象的算法,它可以在前两个参数指定的范围内查找可以使第三个参数指定的谓词返回 true 的第一个对象。谓词不能修改传给它的对象。find_if() 会返回一个指向被找到对象的迭代器,如果没有找到对象,会返回这个 序列的结束迭代器。

https://blog.csdn.net/ypy9323/article/details/79049746

代码语言:javascript复制
///[method 1]
bool fun1(int a)
{
    if (a < 30) {
        return true;
    } else {
        return false;
    }
}
///[method 2]
struct Fun2
{
public:
    bool operator()(int a)
    {
        if (a < 80) {
            return true;
        } else {
            return false;
        }
    }
};
///[method 3] 这种方式要求使用c  11,为了集中表达三种方式定义成了全局的,实际可做实参传入
auto fun3 = [] (int a) ->bool {
    if (a < 10) {
       return true;
    } else {
       return false;
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QList<int> l = {78, 87, 27, 90, 45, 6};
    qDebug() << "*******使用全局函数**********";
    auto iterator_ = std::find_if(l.begin(), l.end(), fun1);
    qDebug() << (*iterator_);
    qDebug() << "*******使用重载()**********";//
    auto it2 = std::find_if(l.begin(), l.end(), Fun2());
    qDebug() << (*it2);
    qDebug() << "*******使用lambda表达式**********";//
    QList<int>::Iterator it3 = find_if(l.begin(), l.end(),fun3);
    qDebug() << (*it3);
    return a.exec();
}

删除空白字符完整程序

代码语言:javascript复制
#include <iostream>
#include <string>
#include <algorithm>

//https://blog.51cto.com/u_15127674/3722086

class is_not_space
{
    public:

        bool operator()(char& s)
        {
            if(&s == NULL)
                return false;
            return true;
        }
};

//删除字符串中从开头到这个元素的所有字符,也就删除了所有前导空白符
std::string trim_left(std::string s)
{
   s.erase(s.begin(), std::find_if(s.begin(), s.end(), is_not_space()));

  // std::string::iterator a = find_if(s.begin(), s.end(), is_not_space());
    return (s);
}

//如果传递相反的迭代器 将从末尾向前搜索字符串 这样可以删除尾部空白符
std::string trim_right(std::string s)
{
   s.erase(std::find_if(s.rbegin(),s.rend(),is_not_space()).base(),s.end());

    return s;
}

//组合以上两个函数 得到删除空白字符的全部功能
std::string trim(std::string s)
{
    return trim_left(trim_right(std::move(s)));
}

int main()
{
    std::string s = "    ssddd    ";
    //std::string left = trim_left(s);
    std::string result = trim(s);

    std::cout<<result<<std::endl;
}

2.3.基于谓词分割集合

假设有一个人的集合,需要把所有女性移到集合的前面。

common.h

代码语言:javascript复制
#ifndef PERSON_H
#define PERSON_H

class person_t {
public:
    enum gender_t {
        female,
        male,
        other
    };

    enum output_format_t {
        name_only,
        full_name
    };

    person_t()
        : m_name("John")
        , m_surname("Doe")
        , m_gender(other)
    {
    }

    person_t(std::string name, gender_t gender, int age = 0)
        : m_name(name)
        , m_surname("Doe")
        , m_gender(gender)
        , m_age(age)
    {
    }

    person_t(std::string name, const std::string &surname, gender_t gender, int age = 0)
        : m_name(name)
        , m_surname(surname)
        , m_gender(gender)
        , m_age(age)
    {
    }

    std::string name() const
    {
        return m_name;
    }

    std::string surname() const
    {
        return m_surname;
    }

    gender_t gender() const
    {
        return m_gender;
    }

    int age() const
    {
        return m_age;
    }

    void print(std::ostream &out,
               person_t::output_format_t format) const
    {
        if (format == person_t::name_only) {
            out << name() << 'n';

        } else if (format == person_t::full_name) {
            out << name() << ' '
                << surname() << 'n';

        }
    }

private:
    std::string m_name;
    std::string m_surname;
    gender_t m_gender;
    int m_age;
};

#endif // PERSON_H

std::partition

https://blog.csdn.net/u014023993/article/details/47657967

接收一个集合和一个谓词,对原集合中的元素进行重排,把符合条件的与不符合条件的分开。符合谓词条件的元素移动到集合的前面,不符合条件的元素移动到集合的后面,算法返回一个迭代器,指向第二部分的第一个元素(不符合谓词条件的第一个元素)。

返回的迭代器与原集合开头的迭代器配合,获取集合中满足谓词条件的元素,与原集合尾端迭代器配合,可获得集合中不符合谓词条件的元素,即使这些集合中存在的空集合也是正确的。

该算法不能保证元素的初始相对位置,如果需要保证初始相对位置,应该使用stable_partition.

https://tool.oschina.net/uploads/apidocs/cpp/en/cpp/algorithm/partition.html

代码语言:javascript复制
template< class ForwardIt, class UnaryPredicate >
ForwardIt partition( ForwardIt first, ForwardIt last, UnaryPredicate p );
//重新排序范围[first, last)中的元素,使得谓词 p 返回 true 的所有元素都在谓词p返回false的元素之前。不保留元素的相对顺序。
 
template<class BidirectionalIterator, class UnaryPredicate>
BidirectionalIterator partition(BidirectionalIterator first,
                                BidirectionalIterator last,
                                UnaryPredicate p)
{
    while (1) {
        while ((first != last) && p(*first)) {
              first;
        }
        if (first == last--) break;
        while ((first != last) && !p(*last)) {
            --last;
        }
        if (first == last) break;
        std::swap(*first  , *last);
    }
    return first;
}
代码语言:javascript复制
#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <vector>
 
bool is_even(int i) { return i % 2 == 0; }
 
int main()
{
    std::vector<int> v;
    for (int i = 0; i < 10;   i) v.push_back(i);
 
    std::cout << "Original vector:n    ";
    std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
 
    // Partition the vector
    std::vector<int>::iterator p =
        std::partition(v.begin(), v.end(), std::ptr_fun(is_even));
 
    std::cout << "nPartitioned vector:n    ";
    std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << "nBefore partition:n    ";
    std::copy(v.begin(), p,       std::ostream_iterator<int>(std::cout, " "));
    std::cout << "nAfter partition:n    ";
    std::copy(p,         v.end(), std::ostream_iterator<int>(std::cout, " "));
}
代码语言:javascript复制
Original vector:
    0 1 2 3 4 5 6 7 8 9 
Partitioned vector:
    0 8 2 6 4 5 3 7 1 9 
Before partition:
    0 8 2 6 4 
After partition:
    5 3 7 1 9
std::stable_partition

同上,只不过会保留原来的顺序不变

https://tool.oschina.net/uploads/apidocs/cpp/en/cpp/algorithm/stable_partition.html

代码语言:javascript复制
#include <iostream>
#include <algorithm>
 
int main()
{
    std::vector<int> v{0, 0, 3, 0, 2, 4, 5, 0, 7};
    std::stable_partition(v.begin(), v.end(), [](int n){return n>0;});
    for (int n : v) {
        std::cout << n << ' ';
    }
    std::cout << 'n';
}
代码语言:javascript复制
3 2 4 5 7 0 0 0 0

分割选定的元素移动到列表中间:将列表按指定的分割点分成两部,那么一个 子表中的选定元素移动到表的底端,另一个子表中的元素移动到顶端。

满足条件的在前,不满足条件的在后,如下完整程序。

代码语言:javascript复制
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

#include "../../common/person.h"

typedef std::pair<std::string, bool> list_item;

std::string title(const list_item &item)
{   
    //string(size_type,char c)创建一个包含 n个元素的对象,其中每个元素都被初始化为字符 c
    return item.first   std::string(item.second, '*');
}

bool is_selected(const list_item &item)
{
    return item.second;
}

bool is_not_selected(const list_item &item)
{
    return !item.second;
}

// This function groups the selected items in a collection
// and moves them to the desired location
// (see section 2.2.4 and figure 2.11)
template <typename It>
void move_selected_to(It first, It last, It destination)
{
    std::stable_partition(first, destination, is_not_selected);//满足的放在前面,不满足放在后面,false在前 true在后
    std::stable_partition(destination, last,  is_selected);//满足在前,不满足在后 true在前,false在后
}

int main(int argc, char *argv[])
{
    std::vector<list_item> people {
        { "David"  , true },
        { "Jane"   , false },
        { "Martha" , false },
        { "Peter"  , false },
        { "Rose"   , true },
        { "Tom"    , true }
    };

    move_selected_to(people.begin(), people.end(), people.begin()   3);

    for (const auto& person: people) {
        std::cout << title(person) << 'n';
    }

    return 0;
}

2.4.过滤和转换

题目:获取一组人中所有女性的名字

首先要过滤集合得到一个只包含女性的问题:若不想改变原来的集合,使用 std::copy_if算法,把所有符合谓词条件的元素复制到新的集合中。这个算法要求传递一对迭代器来定义输入的集合,一个迭代器指向复制结果的目标集合,一个迭代器返回是否需要复制的谓词。

下一步是获取已经过滤集合中的人员姓名,可以通过std::transform来完成,将输入集合作为一对迭代器,转换函数和结果存放位置传递给他。

代码语言:javascript复制
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

#include "../../common/person.h"

std::string name(const person_t &person)
{
    return person.name();
}

bool is_female(const person_t &person)
{
    return person.gender() == person_t::female;
}

bool is_not_female(const person_t &person)
{
    return !is_female(person);
}

int main(int argc, char *argv[])
{
    std::vector<person_t> people {
        { "David"  , person_t::male   },
        { "Jane"   , person_t::female },
        { "Martha" , person_t::female },
        { "Peter"  , person_t::male   },
        { "Rose"   , person_t::female },
        { "Tom"    , person_t::male   }
    };

    // Filtering the collection by copying (see section 2.2.5)
    std::vector<person_t> females;

    // The std::copy_if algorithm copies items that satisfy the
    // is_female predicate into the destination collection
    std::copy_if(people.cbegin(), people.cend(),
                 std::back_inserter(females),
                 is_female);

    // Transforming to get the names
    std::vector<std::string> names(females.size());

    std::transform(females.cbegin(), females.cend(),
                   names.begin(),
                   name);

    for (const auto& name: names) {
        std::cout << name << 'n';
    }

    return 0;
}

std::copy

https://blog.csdn.net/u013066730/article/details/88892580

代码语言:javascript复制
template<class InputIt, class OutputIt>
OutputIt copy(InputIt first, InputIt last, 
              OutputIt d_first)
{
    while (first != last) {
        *d_first   = *first  ;
    }
    return d_first;
}

复制 [first, last) 所定义的范围中的元素到始于 d_first 的另一范围。

返回值:指向目标范围中最后复制元素的下个元素的输出迭代器。

效率:copy的效率高于for循环

代码语言:javascript复制
int main()
{

 vector<string>a{ "Hello","this","is","an","example" };
 list<string>b;
 copy(a.cbegin(), a.cend(), back_inserter(b));
 copy(b.cbegin(), b.cend(), ostream_iterator<string>(cout, " "));
 cout << endl;

 copy(a.crbegin(), a.crend(), b.begin());
 copy(b.cbegin(), b.cend(), ostream_iterator<string>(cout, " "));
 cout << endl;

}

img

std::copy_if

https://blog.csdn.net/qq_44800780/article/details/103702684

http://www.cplusplus.com/reference/algorithm/copy_if/

代码语言:javascript复制
template<class InputIt, class OutputIt, class UnaryPredicate>
OutputIt copy_if(InputIt first, InputIt last, 
                 OutputIt d_first, UnaryPredicate pred)
{
    while (first != last) {
        if (pred(*first))
            *d_first   = *first;
          first;
    }
    return d_first;
}

copy_if的由四个参数,前两个是输入元素的迭代器,拷贝两个迭代器之间的元素,第三个将元素拷贝到的位置,第四个是选择条件,即只拷贝使条件返回true的元素。

返回值:返回目标区间内最后一个被复制元素的下一个位置,也就是第一个未被覆盖的元素的位置,源区间和目标区间不可重叠。

代码语言:javascript复制
// copy_if example
#include <iostream>     // std::cout
#include <algorithm>    // std::copy_if, std::distance
#include <vector>       // std::vector

int main () {
  std::vector<int> foo = {25,15,5,-5,-15};
  std::vector<int> bar (foo.size());

  // copy only positive numbers:
  auto it = std::copy_if (foo.begin(), foo.end(), bar.begin(), [](int i){return !(i<0);} );
  bar.resize(std::distance(bar.begin(),it));  // shrink container to new size

  std::cout << "bar contains:";
  for (int& x: bar) std::cout << ' ' << x;
  std::cout << 'n';

  return 0;
}
代码语言:javascript复制
bar contains: 25 15 
std::remove

http://www.cplusplus.com/reference/algorithm/remove/

http://c.biancheng.net/view/617.html

代码语言:javascript复制
template <class ForwardIterator, class T>
ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
{
    ForwardIterator result = first;
    while (first!=last) 
    {
        if (!(*first == val)) 
        {
            *result = move(*first);
              result;
        }
          first;
    }
    return result;
}

从他的前两个正向迭代器参数指定的序列中移除和第三个参数相等的对象,基本上每个元素都是通过用他后面的元素覆盖它来实现移除的

返回值:返回一个指向鑫的最后一个元素之后的位置的迭代器。

代码语言:javascript复制
std::deque<double> samples {1.5, 2.6, 0.0, 3.1, 0.0, 0.0, 4.1, 0.0, 6.7, 0.0};
samples.erase(std::remove(std::begin(samples), std::end(samples), 0.0), std::end(samples));
std::copy(std::begin(samples),std::end(samples), std::ostream iterator <double> {std::cout," "});
std::cout << std::endl;
// 1.5 2.6 3.1 4.1 6.7
std::remove_if

http://www.cplusplus.com/reference/algorithm/remove_if/

代码语言:javascript复制
template <class ForwardIterator, class UnaryPredicate>
  ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
                             UnaryPredicate pred)
{
  ForwardIterator result = first;
  while (first!=last) {
    if (!pred(*first)) {
      if (result!=first)
        *result = *first;
        result;
    }
      first;
  }
  return result;
}

可以从前两个正向迭代器指定的序列中移除能使作为第三个参数的谓词返回true的元素。

代码语言:javascript复制
// remove_if example
#include <iostream>     // std::cout
#include <algorithm>    // std::remove_if

bool IsOdd (int i) { return ((i%2)==1); }

int main () {
  int myints[] = {1,2,3,4,5,6,7,8,9};            // 1 2 3 4 5 6 7 8 9

  // bounds of range:
  int* pbegin = myints;                          // ^
  int* pend = myints sizeof(myints)/sizeof(int); // ^                 ^

  pend = std::remove_if (pbegin, pend, IsOdd);   // 2 4 6 8 ? ? ? ? ?
                                                 // ^       ^
  std::cout << "the range contains:";
  for (int* p=pbegin; p!=pend;   p)
    std::cout << ' ' << *p;
  std::cout << 'n';

  return 0;
}
代码语言:javascript复制
the range contains: 2 4 6 8

解决本题目:

代码语言:javascript复制
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

#include "../../common/person.h"

std::string name(const person_t &person)
{
    return person.name();
}

bool is_female(const person_t &person)
{
    return person.gender() == person_t::female;
}

bool is_not_female(const person_t &person)
{
    return !is_female(person);
}

int main(int argc, char *argv[])
{
    std::vector<person_t> people {
        { "David"  , person_t::male   },
        { "Jane"   , person_t::female },
        { "Martha" , person_t::female },
        { "Peter"  , person_t::male   },
        { "Rose"   , person_t::female },
        { "Tom"    , person_t::male   }
    };

    // Filtering with the erase-remove idiom (section 2.2.5, page 35)
    people.erase(
        std::remove_if(people.begin(), people.end(),
                       is_not_female),
        people.end());

    for (const person_t& person: people) {
        std::cout << person.name() << 'n';
    }

    return 0;
}

2.5.编写自己的高阶函数

从头实现一个STL中的算法

2.5.1.接收函数作为参数和使用循环实现

假设有一个人的集合,经常需要获得满足条件的名字,但又不想限制为指定的谓词,如 is_female,需要接收 person_t的任何谓词。用户可能基于年龄,头发颜色,婚姻状况等对人们进行划分。

因此,创建一个多次使用的函数是必要的,这个函数需要接收一个人的向量和一个用于过滤的谓词,返回一个满足谓词条件的人的名字的字符串向量。

因此,函数类型作为模板参数,让编译器在编译时确定具体的类型,而不是猜测哪种类型更好。

代码语言:javascript复制
template <typename FilterFunction>
std::vector<std::string> names_for(
        const std::vector<person_t> &people,
        FilterFunction filter)
{
    std::vector<std::string> result;

    for (const person_t& person: people) {
        if (filter(person)) {
            result.push_back(name(person));
        }
    }

    return result;
}

以上函数允许用户传递任何类型函数的东西作为参数。

我们知道,使用STL算法将导致不必要的内存分配,因此,手动编写的循环实现上面的例子会更好。但是,我们也应明白,STL使用的好处:简单,正确性。

2.5.递归和尾递归优化

上面实现不是 纯函数,因为当发现一个新的符合条件的人员时,它就要修改结果变量。

在纯FP语言中是不存在循环的,遍历集合的函数通常是由递归实现的。

对于一个非空向量,可以递归地处理它的头(第一个元素)和尾(所有其他元素),这又可以被看作一个向量。如果头满足谓词,则把它包含在结果中,如果接收一个空向量,则什么也不需要处理,返回一个空向量。

假设有一个 tail函数,它接收一个向量并返回它的尾。

代码语言:javascript复制
template <typename T>
T tail(const T &collection)
{
    return T(collection.cbegin()   1, collection.cend());
}

有一个prepend函数,它接收一个元素和一个向量,返回原来向量的副本,把这个元素添加到结果向量的前面。

代码语言:javascript复制
template <typename T, typename C>
C prepend(T &&item, C collection)
{
    C result(collection.size()   1);
    result[0] = std::forward<T>(item);
    std::copy(collection.cbegin(), collection.cend(), result.begin()   1);
    return result;
}

相互递归的实现:

代码语言:javascript复制
template <typename FilterFunction>
std::vector<std::string> names_for(
        const std::vector<person_t> &people,
        FilterFunction filter)
{
    if (people.empty()) {
        return {};//如果集合和空,返回空集合

    } else {
        const auto head = people.front();
        const auto processed_tail = names_for(
                tail(people),
                filter);//递归调用函数处理集合的尾

        if (filter(head)) {
            return prepend(name(head), processed_tail);//如果第一个元素符合谓词要求,把他包含在结果中,否则跳过
        } else {
            return processed_tail;
        }
    }
}

这种实现是低效的,首先由于某种原因导致向量的 tail 函数不存在,它需要创建一个新向量并将旧向量的所有数据复制到其中。

tail函数的问题可由一对迭代器代替向量作为输入来解决,在这种情况下,获取向量尾变得很简单——只需要移动迭代器,使他指向第一个元素即可。

代码语言:javascript复制
template <typename FilterFunction,typename Iterator>
std::vector<std::string> names_for(
        Iterator people_begin,
        Iterator people_end,
        FilterFunction filter)
{
    const autp processed_tail = names_for(
        people_begin   1,
        people_end,
        filter
    );
}

这种实现的问题是:每次递归调用都有占用堆栈中的内存,如果堆栈溢出则程序崩裂,即使集合不够大,不会导致堆栈溢出,但函数调用也要付出代价。

尾递归:递归调用是函数的最后一件事,递归后不能做任何事情。

前面的例子都不是,因为用户从递归调用获取结果,当 filter为 true时,向它添加一个元素,然后返回结果。把函数改成尾递归,必须寻找另一种策略收集中间结果,就要使用一个附件参数。

代码语言:javascript复制
template <typename FilterFunction, typename Iterator>
std::vector<std::string> names_for_helper(
        Iterator people_begin,
        Iterator people_end,
        FilterFunction filter,
        std::vector<std::string> previously_collected)
{
    if (people_begin == people_end) {
        return previously_collected;

    } else {
        const auto head = *people_begin;

        if (filter(head)) {
            previously_collected.push_back(name(head));
            return names_for_helper(
                    people_begin   1,
                    people_end,
                    filter,
                    previously_collected);

        } else {
            return names_for_helper(
                    people_begin   1,
                    people_end,
                    filter,
                    previously_collected);
        }
    }
}

以上还有一点小问题:必须使用附件参数调用函数,因此,传递一个空向量作为参数设计:

代码语言:javascript复制
template <typename FilterFunction, typename Iterator>
std::vector<std::string> names_for(
        Iterator people_begin,
        Iterator people_end,
        FilterFunction filter)
{
        return names_for_helper(people_begin,
                                people_end,
                                filter,
                                {});
}

在这种情况下,支持尾递归调用优化的编译器就可以将这个递归函数转成一个简单的循环,循环就很高效了!

3第3章 函数对象

本章讲述:C 中所有被认为是函数对象的东西。

3.1. 函数和函数对象

C 定义了一种 末尾返回类型的格式 函数,如下:

代码语言:javascript复制
auto max(int arg1, int arg2) -> int{
    return std::max(arg1,arg2);
}

这一语法主要用于编写函数模板,其中返回值类型与实参类型有关。也就是,当编写函数模板并需要通过参数类型推断返回值类型时,才需要将返回值类型写在函数名和参数的后面。

从C 14 开始,完成可以忽略返回值类型,而由编译器根据 return 语句中的表达式进行推断。但是,返回值类型的编写规则需要注意:

1, 编写递归函数时,要么指定返回值类型,要么先编写非递归的返回语句;

2, 编写把函数的返回结果不加修改地传递给其他函数的通用函数可以用 decltype(auto), 不需要判断传递给用户什么函数,也不需要知道是向调用者传递它的结果值,还是结果值的引用

2,1: 作为引用传递,可能返回一个临时值的引用,可能产生未定义的行为

2.2:作为值传递,可能造成不必要的副本,副本会产生性能问题,并且有时会产生语法错误

代码语言:javascript复制
template<typename Object, typename Function>
decltype(auto) call_on_object(Object&& object, Function fun)
{
    return fun(std::forward<Object>(object));
}

decltype&auto

https://www.jb51.net/article/217094.htm

decltype:在编译时期进行自动类型推导,不要求变量必须初始化

auto:根据右边的初始值推导出变量的类型,要求变量必须初始化

代码语言:javascript复制
auto varName=value;
decltype(exp) varName=value;
decltype(exp) varName;

解释:exp只是一个普通的表达式,可以是任意复杂类型,但不能是 void

代码语言:javascript复制
int x = 0;
decltype(x) y = 1;           // y -> int
decltype(x   y) z = 0;       // z -> int
const int& i = x;
decltype(i) j = y;           // j -> const int &
const decltype(z) * p = &z;  // *p  -> const int, p  -> const int *
decltype(z) * pi = &z;       // *pi -> int      , pi -> int *
decltype(pi)* pp = &pi;      // *pp -> int *    , pp -> int * *

exp的分类:

1, 如 exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致

代码语言:javascript复制
#include<string> 
#include<iostream>
using namespace std;
 
class A{
public:
    static int total;
    string name;
    int age;
    float scores;
}
 
int A::total=0;
 
int main()
{
int n=0;
const int &r=n;
A a;
decltype(n) x=n;    //n为Int,x被推导为Int
decltype(r) y=n;    //r为const int &,y被推导为const int &
decltype(A::total)  z=0;  ///total是类A的一个int 类型的成员变量,z被推导为int
decltype(A.name) url="www.baidu.com";//url为stringleix
return 0;
}

2, 如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致

exp中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码

代码语言:javascript复制
int& func1(int ,char);//返回值为int&
int&& func2(void);//返回值为int&&
int func3(double);//返回值为int
 
const int& func4(int,int,int);//返回值为const int&
const int&& func5(void);//返回值为const int&&
 
int n=50;
decltype(func1(100,'A')) a=n;//a的类型为int&
decltype(func2()) b=0;//b的类型为int&&
decltype(func3(10.5)) c=0;//c的类型为int
 
decltype(func4(1,2,3)) x=n;//x的类型为const int&
decltype(func5()) y=0;//y的类型为const int&&

3,如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是 exp的引用,假设exp的类型是T,则 decltype(exp)的类型为 T&

左值:表达式执行结束后依然存在的数据,即持久性数据;

右值是指那些在表达式执行结束不再存在的数据,即临时性数据。

一个区分的简单方法是:对表达式取地址,如果编译器不报错就是左值,否则为右值

代码语言:javascript复制
class A{
public:
   int x;
}
 
int main()
{
const A obj;
decltype(obj.x) a=0;//a的类型为int
decltype((obj.x)) b=a;//b的类型为int&
 
int n=0,m=0;
decltype(m n) c=0;//n m得到一个右值,c的类型为int
decltype(n=n m) d=c;//n=n m得到一个左值,d的类型为int &
return 0;
}

实际应用:

类的静态成员可以使用auto,对类的非静态成员无法使用auto,如果想推导类的非静态成员的类型,只能使用 decltype

代码语言:javascript复制
template<typename T>
class A
{
private :
   decltype(T.begin()) m_it;
   //typename T::iterator m_it;   //这种用法会出错
public:
void func(T& container)
{
   m_it=container.begin();
}
};
 
int main()
{
 
const vector<int> v;
A<const vector<int>> obj;
obj.func(v);
return 0;
}

函数指针

函数指针是一个存放函数地址的变量,可以通过这个变量调用该函数,多态通过改变函数指针来实现,因此在调用函数指针时,改变函数的行为。函数指针和引用也是函数对象,因为其可以像普通函数那样进行调用。

1, 创建一个函数指针 ask_ptr指向一个普通函数

2, 创建一个引用ask_ref引用相同的函数

3, 像调用函数本身一样使用平常的函数调用语法来调用

4, 创建一个可以转换成函数指针的对象,不加任何说明地像普通函数一样调用他。

代码语言:javascript复制
int ask(){
    return 42;
}

typedef decltype (ask)* funPtr;

class ConVertFunPtr{
    public:
        //转换操作符可以只返回一个函数指针,虽然可以根据条件返回不同的函数,
        //但是不能向他们传递任何数据
        operator funPtr() const
        {
            return ask;
        }
};

int main()
{
    //指向函数的指针
    auto ask_ptr = &ask;
    std::cout<< ask_ptr() <<endl;

    //函数引用
    auto& ask_ref = ask;
    std::cout<<ask_ref()<<endl;

    //可以自动转换成函数指针的对象
    ConVertFunPtr ask_wapper;
    std::cout<<ask_wapper()<<endl;
}

调用操作符重载

创建一个类并重载它们的调用操作符,与其他操作符不同,调用操作符可以有任意数目的参数,参数可以是任意类型,因此可以创建任意签名的函数对象。

重载调用操作符的语法与定义成员函数一样简单——只有一个特殊的名字 operator(),需要指明返回值类型和函数所需要的所有参数。

https://blog.csdn.net/damon_x/article/details/51672425

代码语言:javascript复制
#include <iostream>  
#include <vector>  
#include <algorithm>  
  
using namespace std;  
  
class A  
{  
  public:  
    int operator() (int val)  
    {  
      return val > 0 ? val : -val;  
    }  
  
    void operator() (char *p)  
    {  
      *p = 'B';  
    }  
};  
  
class B  
{  
  public:  
    B(int val = 0) : ival(val) {} //构造函数初始化成员 ival;  
    bool operator()(const int &v)  
    {  
      return v > ival;  //返回真值,如果传递进来的实参大于成员ival,ival在调用构造函数时被初始化;  
    }  
  
  private:  
    int ival;  
};  
  
int main()  
{  
  int i = -42;  
  A  a;  
  unsigned int ui = a(i);  
 // unsigned int ui = a.operator()(i);  
  cout << ui << endl;  
  char arr[] = "ABC";  
  char *pa = arr;  
  a(pa);  
  //a.operator()(pa);  
  cout << pa << endl;  
  
  int b[] = {1, 52, 12, 30, 9, 19};  
  vector<int> ivec(b, b 6);  
  vector<int>::iterator ite = ivec.begin();  
  while((ite = find_if(ite, ivec.end(), B(10))) != ivec.end())  
  {  
    cout << *ite << " ";  
    ite  ;  
  }  
  cout << endl;  
  
  int n = count_if(ivec.begin(), ivec.end(), B(10));  
  cout << "n:" << n << endl;  
  
  return 0;  
}  

42 BBC 52 12 30 19 n:4

与普通函数相比,以上函数都有一个优点:每一个实例都有自己的状态,不论是可变还是不可变的状态,这些状态可用于自定义函数的行为,而无需调用这指定。

假设有一个人的列表,每个人都有姓名、年龄和其他现在无须关心的属性,需要用户统计比指定年龄大的人数。

明智做法:创建一个合适的函数对象,将年龄限制作为其内部状态,谓词可以只定义一次,然后根据不同的年龄限制进行实例化。

代码语言:javascript复制
class older_than {
public:
    older_than(int limit)
       : m_limit(limit)
    {
    }

    // Defining a call operator as a template function,
    // we will create a generic function object
    // that deduces the type of its argument when invoked
    template <typename T>
    bool operator() (T &&object) const
    {
        return std::forward<T>(object).age() > m_limit;
    }

private:
    int m_limit;
};


int main(int argc, char *argv[])
{
    std::vector<person_t> persons;

    older_than predicate(42);

    std::count_if(persons.cbegin(), persons.cend(), predicate);

    return 0;
}
std::count_if

http://www.cplusplus.com/reference/algorithm/count_if/

代码语言:javascript复制
template <class InputIterator, class UnaryPredicate>
  typename iterator_traits<InputIterator>::difference_type
    count_if (InputIterator first, InputIterator last, UnaryPredicate pred)
{
  typename iterator_traits<InputIterator>::difference_type ret = 0;
  while (first!=last) {
    if (pred(*first))   ret;
      first;
  }
  return ret;
}

函数返回满足条件的范围内的元素数

代码语言:javascript复制
#include <vector>
#include <algorithm>
#include <iostream>
bool greater10(int value)
{
    return value >10;
}
int main()
{
    using namespace std;
    vector<int> v1;
    vector<int>::iterator Iter;
    v1.push_back(10);
    v1.push_back(20);
    v1.push_back(10);
    v1.push_back(40);
    v1.push_back(10);
    cout << "v1 = ( ";
    for (Iter = v1.begin(); Iter != v1.end(); Iter  )
       cout << *Iter << " ";
    cout << ")" << endl;
    vector<int>::iterator::difference_type result1;
    result1 = count_if(v1.begin(), v1.end(), greater10);
    cout << "The number of elements in v1 greater than 10 is: "
         << result1 << "." << endl;
}

1 2 v1 = ( 10 20 10 40 10 ) The number of elements in v1 greater than 10 is: 2.

创建通用的函数对象

前面例子中,创建了一个函数对象检查一个人是否比设定的年龄大,它解决了不同的年龄限制需要定义不同函数的问题,但仍然不灵活,它只能接收“人”作为输入。如果传入 汽车、树木等就不使用了!

因此,此时定义一个函数对象,可用于各种需要检测年龄信息的类型,不必为每种类型编写不同的函数对象,如何实现呢?

1, 面向对象,创建一个包含 age()虚函数的超类,但这种方法会影响运行时的性能,而且对与支持 older_than函数对象的所有类都必须强制继承这个超类,破坏了封装性

2, 将 older_than类改造成类模板,对于需要检测年龄的类型创建模板类

代码语言:javascript复制
template<typename T>
class older_than
{
    public:
        older_than(int limit):m_limit(limit){}

        bool operator()(const T& object) const{
            return object.age() > m_limit;
        }
    private:
        int m_limit;
};
//对于具有 .age() get方法的任意类型都可以使用 older_than
std::count_if(persons.cbegin(), persons.cend(), older_than<person_t>(42));
std::count_if(cars.cbegin(), cars.cend(), older_than<cars_t>(42));
std::count_if(dogs.cbegin(), dogs.cend(), older_than<dogs_t>(42));

以上缺点是:这种方法在实例化的时候要检测对象是,必须指定对象的类型,虽然有时候这种做法很有用,但是在大多数情况下是非常长的,而且很可能导致指定的类型与调用操作符要求的类型不一致的问题。

因此,可以把调用操作符合作为一个模板成员函数,而不是创建一个模板了,这种情况在示例话 older_than函数对象时,就不需要指定类型,编译器在调用调用操作符时,会自动推测参数的类型。

代码语言:javascript复制
template<typename T>
class older_than
{
    public:
        older_than(int limit):m_limit(limit){}
        
        template<typename T>
        bool operator()( T&& object) const{
            //age成员函数有不同的重载 左值和右值,可调用正确重载
            return std::forward<T>(object).age() > m_limit;
        }
    private:
        int m_limit;
};
//再使用 older_than函数对象时,就不用显式指明对象类型了,甚至可以对不同的类型使用相同的对象示例
older_than pp(5);
std::count_if(persons.cbegin(), persons.cend(), pp);
std::count_if(cars.cbegin(), cars.cend(), pp);
std::count_if(dogs.cbegin(), dogs.cend(), pp);

以上相同的实例可以dubio包含 age()成员函数的任意类型的对象进行检测,返回一个整数或与整数类似的值。

3.2.lambda和闭包(closure)

lambda允许创建内联函数对象——在要使用它们的地方——而不是正在编写的函数之外。

前面例子:有一组人,需要从中挑出所有的女性成员

使用lambda表达式实现形同功能,同时保持代码局部优化,并且不污染程序的命名空间。

代码语言:javascript复制
std::copy_if(people.cbegin(),people.cend(),std::back_inserter(females),[](
    const person_t &person){
        return person.gender() == person_t::female;
    }
)

C 的lambda表达式由 3个主要的部分组成——头,参数列表和体;

[a, &b] (int x, int y) {return a *x b *y ;}

[a, &b]:头指明了 周围的哪些变量在体内可见。a作为值进行捕获,b作为引用进行捕获

[]:这样的lambda不使用周围的任何变量,没有任何的内部状态,可以自动转换成普通函数的指针

[&]:所有lambda体中使用的变量都作为引用进行捕获

[=]:都作为值进行捕获

[this]:以值的方式捕获 this指针

[&,a]:除了a是值,其余都是引用

[=,&b]:除了b是引用,其余都是值

举例:假设仍然要处理一个人的列表,但这次它们是一个公司的职员,公司被分成若干个小组,每个小组由自己的名字。

代码语言:javascript复制
class company_t {
public:
    std::string team_name_for(const person_t &) const;

    int count_team_members(const std::string &team_name) const;

private:
    std::vector<person_t> m_employees;

};

std::string company_t::team_name_for(const person_t &person) const
{
    return "Team1";
}

int company_t::count_team_members(const std::string &team_name) const
    
    //需要捕获 this指针,因为在调用team_name_for成员函数要调用它
    return std::count_if(
            m_employees.cbegin(), m_employees.cend(),
            [this, &team_name]
                (const person_t &employee)
            {
                return team_name_for(employee) == team_name;
            }
        );
}

但是,这样写的话,lambda会发生什么呢?

C 在编译时,lambda表达式将转换成一个包含两个成员变量的新类——指向company_t对象的指针和一个 std::string的引用——每个成员对应一个捕获的变量。等同于如下类设计:

代码语言:javascript复制
class lambda_implementation{
    public:
        lambda_implementation(const company_t* _this,const std::string &team_name):
            m_this(_this),m_team_name(team_name){}
        bool operator()(const person_t &employee) const
        {
            return m_this->team_name_for(employee) == m_team_name;
        }
    private:
        const company_t *m_this;
        const std::string &m_team_name;
};

在lambda中创建任意成员变量

1,创建可以使用外部变量的 lambda,无论是使用外部变量的引用或者是值的副本

2, 通过编写自己的调用操作符类,可以创建任意多的成员变量,而无须把它们关联到外部变量

3, 把它们初始化为固定的值,也可以初始化为函数的调用结果。可以在创建 lambda之前声明一个具有特定值的局部变量,然后在 lambda中捕获它。

但是,不能将 只能移动 的对象保持在lambda内部,只定义了 move构造函数,而没有 copy构造函数的类实例。

举例 : std::unique_ptr指向一个 lambda (没有 copy构造函数)

假设要创建一个网络请求,并且把 session数据存放在一个唯一的指针对象中。在请求完成之后执行一个lambda,在这个lambda中需要访问session中的数据,因此需要在 lambda中捕获 session中的数据。

代码语言:javascript复制
std::unique_ptr<seesion_t> session = create_session();
auto request = server.request("Get /", session->id());
//错误,std::unique_ptr中没有copy构造,不能直接传给 lambda
request.on_completed([session](respose_t response){
    std::cout<<response<<" "<<session<<endl;
});

因此,此时可以单独定义任意的成员变量和它的初始值,而不是要指定要捕获哪个变量,变量的类型可以根据指定的值进行自动推测。

代码语言:javascript复制
//把session的所有移到lambda中,创建一个lambda的成员变量并给其值,把外围的对象移动到 lambda中
request.on_completed([session = std::move(sessont), time = current_time()](respose_t response){
    std::cout<<response<<" "<<session<<endl;
});

通用的lambda表示式

通过指明参数类型为 auto的表达式,lambda 允许创建通用的函数对象,可以很容易地创建一个通用地 lambda,接收任何具有 .age()成员函数地对象,检查该对象是否超过了指定地限制。

代码语言:javascript复制
auto predicate = [limit = 42 ](auto &&object)
{
    return std::forward<decltype(object)>(object).age() > limit;
};
std::count_if(person.cbegin(),person.cend(),predicate);
std::count_if(cars.cbegin(),cars.cend(),predicate);

C 20中小技巧:

1,接收两个相同类型地参数并比较它们是否相等,可以把first参数声明为auto,second参数声明为 decltype(first)

[](auto first, decltype(first)second) { }

2, 显示声明模板参数,而不再需要声明为 decltype

[](T first, T second) { }

3.3.编写比 lambda更简洁地函数对象

假设正在编写一个 Web客户端,已经向服务器发送了几个请求并收到一个响应 response_t类型地集合,因为请求可能失败,所以 response_t提供了 .error()成员函数返回失败时地信息。如果请求失败,这个函数返回true,否则返回false。

代码语言:javascript复制
ok_response = filter(responses,[](const respose_t &response){
    return !response.error();
});
failed_response = filter(responses,[](const respose_t &response){
    return response.error();
});

如果需要经常执行此操作,对于具有 .error()成员函数并返回bool值地其他类型,或者其他可以转换成 bool类型,那这些呆板地代码量将远远超过手工定义函数对象地呆板代码量。

因此,改变以上策略,实现一个简单地类重载调用操作符,需要存储一个单独的bool值,来高速用户是选择正确的还是错误的响应。

代码语言:javascript复制
class error_test_t{
    public:
        error_test_t(bool error =true):m_error(error){}

        template<typename T>
        bool operator()(T&& value) const{
            return m_error == (bool)std::forward<T>(value).error();
        }
        //如果test为true,就返回谓词当前的状态,如果为false,返回逆状态
        error_test_t operator==(bool test) const{
            return error_test_t(test? m_error: !m_error);
        }
        //如果为false,返回逆状态
        error_test_t operator!() const{
            return error_test_t(!m_error);
        }
    private:
        m_error;
};
error_test_t(true);
error_test_t(false);

3.4.用std::function包装函数对象

当需要把函数对象保存为类的成员时,就不能将函数对象的类型模板化,因此必须显示指明它的类型,或者在两个独立的编译单元中使用一个函数,就必须指明具体的类型。在这些情况下,没有适合于所有函数对象的超类型,所以,标准库提供了一个 std::function类模板,它可以包装任何类型的函数对象:

std::function

(451条消息) C std::function的用法_Ziven-Hu的博客-CSDN博客_c function

好用并实用的东西才会加入标准的。因为好用,实用,我们才在项目中使用它。std::function实现了一套类型消除机制,可以统一处理不同的函数对象类型。以前我们使用函数指针来完成这些;现在我们可以使用更安全的std::function来完成这些任务

代码语言:javascript复制
#include <iostream>  
#include <vector>  
#include <list>  
#include <map>  
#include <set>  
#include <string>  
#include <algorithm>  
#include <functional>  
#include <memory>  
using namespace std;  
  
//声明一个模板  
typedef std::function<int(int)> Functional;  
  
  
//normal function  
int TestFunc(int a)  
{  
    return a;  
}  
  
//lambda expression  
auto lambda = [](int a)->int{return a;};  
  
//functor仿函数  
class Functor  
{  
public:  
    int operator() (int a)  
    {  
        return a;  
    }  
};  
  
  
//类的成员函数和类的静态成员函数  
class CTest  
{  
public:  
    int Func(int a)  
    {  
        return a;  
    }  
    static int SFunc(int a)  
    {  
        return a;  
    }  
};  
  
  
int main(int argc, char* argv[])  
{  
    //封装普通函数  
    Functional obj = TestFunc;  
    int res = obj(0);  
    cout << "normal function : " << res << endl;  
  
    //封装lambda表达式  
    obj = lambda;  
    res = obj(1);  
    cout << "lambda expression : " << res << endl;  
  
    //封装仿函数  
    Functor functorObj;  
    obj = functorObj;  
    res = obj(2);  
    cout << "functor : " << res << endl;  
  
    //封装类的成员函数和static成员函数  
    CTest t;  
    obj = std::bind(&CTest::Func, &t, std::placeholders::_1);  
    res = obj(3);  
    cout << "member function : " << res << endl;  
  
    obj = CTest::SFunc;  
    res = obj(4);  
    cout << "static member function : " << res << endl;  
  
    return 0;  
} 

并不是对包含的而类型进行模板化,而是对函数对象的签名进行模板化。模板参数指定了函数的返回值和参数的类型,可以使用相同的类型存储普通函数,函数指针,lambda表达式和其他可以调用的对象。

代码语言:javascript复制
//结果类型float,参数类型 float,float
std::function<float(float,float)> test_fun;
//普通函数
test_fun = std::fmaxf;
//含有调用操作符的类
test_fun = std::multiplies<float>();
//含有通用调用操作符的类
test_fun = std::multiplies<>();
//lambda
test_fun = [x](float a,float b){return a*x  b;};
//通用lambda
test_fun = [x](auto a,auto b){return a*x  b;};
//ERROR,错误的签名
test_fun = [](string s){return s.empty();};

但是,std::function不能滥用,因为它有明显的性能问题,为了隐藏包含的类型并提供一个对所有可能调用类型的通用接口,std::function使用类型擦除的技术。

4第4章 以旧函数创建新函数

函数式编程提供了轻松创建新函数的方法,可以组合已经存在的函数,升级专用的函数适应更通用的场合。

4.1.偏函数应用

偏函数:通过把已知函数的一个或多个参数设定为特定值的方法创建新函数的概念,偏的意思是在计算函数结果时,只需要传递部分参数,而不需要传递所有参数。

代码语言:javascript复制
//4-1 将参数与预定值比较
class greater_than{
    public:
        greater_than(int value):m_value(value){}
        bool operator()(int arg) const{
            return arg > m_value;
        }
    private:
        int m_value;
};
greater_than greater_than_42(42);
greater_than_42(1);//false
greater_than_42(50);//true

把二元函数转换成一元函数的通用方法

做法:创建参数类型的模板类

代码语言:javascript复制
//4-2 模板类的基本结构
template<typename Fun, typename SecondeArg>
class paratial_apply{
    public:
        paratial_apply(Fun fun, SecondeArg sec):m_fun(fun),m_sec(sec){

        }
    
    private:
        Fun m_fun;
        SecondeArg m_sec;
};

由于不能事先知道第一个参数的类型,所有调用操作符也要做成模板:调用存在 m_fun成员中的函数,并把调用操作符的参数作为第一个参数传递给它,且把存放在m_sec成员中的值作为第二个参数。

代码语言:javascript复制
//4-3 偏函数应用的调用操作符
template<typename Fun, typename SecondeArg>
class paratial_apply{
    public:
        paratial_apply(Fun fun, SecondeArg sec):m_fun(fun),m_sec(sec){

        }

        template<typename FirstArg>
        auto operator()(FirstArg fir)cosnt -> decltype(auto) //如果编译器不支持返回值类型推断 则需要使用 decltype完成相同类型的功能
        {   
            //调用操作符的参数被传递给函数作为第一个参数
            //保存的值作为函数的第二个参数
            return m_fun(std::forward<FirstArg>(fir), m_sec);
        }
    
    private:
        Fun m_fun;
        SecondeArg m_sec;
};

至此已经定义了完整的类。使用如下:

需要在创建类的实例时候显式指定模板参数的类型,这些写代码比较难看,而且有时候不可能。比如 lambda

如何改进呢?让编译器自动推断类型

可以创建一个函数模板,唯一的任务就是生成这个类型的实例。因为模板参数推断在调用函数时发生,所以在调用时不需要指明类型。这个函数调用前面定义的类的构造函数,并把它的参数传递给构造函数。

代码语言:javascript复制
//4-4 用于创建先前函数对象的包装函数
template<typename Fun, typename SecondeArg>
paratial_apply<Fun,SecondeArg>
bind2nd(Fun &&fun, SecondeArg &&sec)
{
    return (paratial_apply<Fun,SecondeArg>(
        std::forward<Fun>(fun),
        std::forward<SecondeArg>(sec)
    ));
}

现在使用新创建的函数代替前面 的 greater_than

代码语言:javascript复制
//4-5 使用 bind2nd创建函数对象
auto greater_than_42 = bind2nd(std::greater<int>(),42);
greater_than_42(1);//false
greater_than_42(50);//true

该函数更加通用,替换了所有的 greater_than函数。举例说明:

代码语言:javascript复制
//4-6 使用 bind2nd 把度转成弧度
std::vector<double> degree = {0,30,45,60};
std::vector<double>  rad(degree.size());
std::transform(degree.cbegin(), degree.cend(), rad.begin(),
    bind2nd(std::multiplies<double>(), PI / 180));

没有必要局限于谓词,通过把第二个参数绑定为特定值,可以把任何二元函数转换成一元函数。

std::bind

https://www.jianshu.com/p/f191e88dcc80

std::bind函数是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。主要作用有

1,将可调用的对象和其参数绑定成一个防函数

2,只绑定部分参数,减少可调用对象传入的参数

a, 绑定普通函数

代码语言:javascript复制
double my_divide(double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);  
std::cout << fn_half(10) << 'n';                        // 5

普通函数做实参时,会隐式转换成函数指针

b, 绑定一个成员函数

代码语言:javascript复制
struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1 n2 << 'n';
    }
    int data = 10;
};
int main() 
{
    Foo foo;
    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1);
    f(5); // 100
}

编译器不会将对象的成员函数隐式转换成函数指针,所以必须在 Foo:print_sun 前添加 &

第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址

c,指向成员函数的指针

代码语言:javascript复制
#include <iostream>
struct Foo {
    int value;
    void f() { std::cout << "f(" << this->value << ")n"; }
    void g() { std::cout << "g(" << this->value << ")n"; }
};
void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
    (foo1->*fun)();  // call fun on the object foo1
    (foo2->*fun)();  // call fun on the object foo2
}
int main() {
    Foo foo1{1};
    Foo foo2{2};
    apply(&foo1, &foo2, &Foo::f);
    apply(&foo1, &foo2, &Foo::g);
}

成员函数指针的定义:void (Foo::*fun)(),调用是传递的实参: &Foo::f;

fun为类成员函数指针,所以调用是要通过解引用的方式获取成员函数*fun,即(foo1->*fun)();

一个占位符

std::bind把一个函数的所有参数绑定为特定的值,但是并不调用它,第一个参数是要绑定的函数,其他参数是要绑定的值。如下是要把 std::greater比较函数的参数绑定为 6和42.

代码语言:javascript复制
auto bound = std::bind(std::greater<double>(),6,42);
bool is_6_greater_than_42 = bound();
//转换成lambda,没有参数绑定到变量,因此不需要捕获任何东西,没有占位符
auto bound = []{
    return std::greater<double>()(6,42);
}
//占位符
auto is_greater_than_42 = std::bind(std::greater<double>(),_1,42);
auto is_less_than_42 = std::bind(std::greater<double>(),42,_1);
//转换成lambda,把一个参数绑定为特定地只,而另一个绑定为占位符,因为只有一个占位符,lambda只有一个参数
//不需要捕获任何变量
auto is_greater_than_42 = [](double value){
    return std::greater<double>()(value,42);
};
auto is_less_than_42 = [](double value){
    return std::greater<double>()(42,value);
};
is_less_than_42(6);
is_greater_than_42(6);

二个占位符

假设一个double类型的向量需要按升序排列

less_than = std::bind(greater,_2,_1)

_1占位符用传递给 less_than的第一个参数填充,因此less_than的第一个参数变成 greater的第二个参数。

代码语言:javascript复制
std::sort(scores.begin(),scores.end(),std::bind(std::greater<double>(),_2,_1))
//转成lambda,两个占位符,两个参数
std::sort(scores.begin(),scores.end(),[](double value1,double value2){
    return std::greater<double>()(value2,value1);
});

多参数普通函数

把所有的人都写到标准输出或其他输出中,定义非成员函数,有3个参数

代码语言:javascript复制
void print_persion(const person_t &person, std::ostream &out,
 person_t::output_format_t format)
{
 if(format == person_t::name_only)
 {
  out << person.name();
 }
 else if(format == person_t::full_name)
 {
  out << person.name()<<" "<<person.surname();
 }
}

输出集合中所有成员的信息,可以把print_person 传递给 std::for_each 算法,并把 out和format 参数绑定。

默认情况下,std::bind在它返回的函数对象中保存绑定值得副本,因为std::out不能复制,所以需要把out参数绑定到 std::out得引用,而不是它得副本。因此,需要std::ref得帮助:

代码语言:javascript复制
std::for_each(people.cbegin(),people.cend(),std::bind(
 print_person, _1, std::ref(std::out), person_t::name_only));
//输出到文件
std::for_each(people.cbegin(),people.cend(),std::bind(
 print_person, _1, std::ref(file), person_t::name_only));
//转成lambda,一个占位符,创建一个一元lambda
std::for_each(people.cbegin(),people.cend(),
             [](const person_t &person){
                 print_person(person, std::count, person_t::name_only);
             });
//输出到文件
std::for_each(people.cbegin(),people.cend(),
             [&file](const person_t &person){
                 print_person(person, file, person_t::full_name);
             });

类成员函数

因为类成员不被认为是函数对象,不支持函数调用语法,暗含第一个参数 this指针,该参数指向要调用成员函数得类实例,该上述函数如下:

代码语言:javascript复制
class person_t{
 void print(std::ostream &out, output_format_t format) const
 {
 
 }
};
//使用成员函数得指针创建一元函数
std::for_each(people.cbegin(),people.cend(),std::bind(
 &person_t::print, _1, std::ref(file), person_t::name_only));

使用lambda替代std::bind

lambda是语言得核心特性,编译器比较容易优化,语法上虽然有点长,但是它可以让编译器更加自由地优化代码。

std::bind调用转成lambda规则如下:

1,把任何绑定变量或引用变量地参数转换成捕获变量

2,把所有占位符转换成 lambda参数

3, 把所有绑定地特定值地参数直接写在 lambda中

代码语言:javascript复制
std::bind(greater,_1,value);
//转换
[value](int arg){
 return greater(arg,value);
}

4.2.看待函数不同的方式

柯里化

柯里化:不允许创建多于一个参数的函数——可以创建一个返回一元函数的一元函数,而不是创建一个接收两个参数返回一个值的函数,当第二个函数被调用时,就意味着已经接收了两个需要的参数。

实例1

代码语言:javascript复制
bool greater(double first,double second)
{
 return first > second;
}
//柯里化之后
auto greater_curried(doublr first)
{
 return [first](double second){
  return first > second;
 };
}
//调用
greater(2,3);
//返回一个一元函数对象,检查他的参数是否小于2
greater_curried(2);
//返回false
greater_curried(2)(3);

实例2:嵌套足够多的lambda,逐个捕获print_persion函数所需要的参数

代码语言:javascript复制
void print_persion(const person_t &person, std::ostream &out,
 person_t::output_format_t format)
//调用
print_person(person, std::cout, person_t::full_name);
//柯里化
auto print_person_cd(const person_t &person)
{
 return [&](std::ostream &out){
  return [&](person_t::output_format_t format){
   print_person(person, out, format);
  }
 }
}

函数组合

读取一个文本文件,找出 n个使用频率最高的单词,并且按使用频率大小进行排序输出。

1,第一个函数接收 std::string作为参数,返回值是单词的集合

代码语言:javascript复制
std::vector<std::string> words(const std::string& text);

2,第二个函数获得一个单词的列表,保存所有单词和它在文本中出现的次数

代码语言:javascript复制
template<typename T>

std::unordered_map<T,unsigned int> count_occurrences(const std::vector<T>& items)

3, 第三个函数获取容器中的每一键值对的值,并按相反的顺序创建一个键值对,返回新得键值对的集合。

代码语言:javascript复制
template <typename C, typename P1, typename P2>

std::vector<std::pair<P2,P1>> reverse_pairs(const C& collection);

4,第四个函数对向量进行排序

5, 第五个函数把向量进行输出

组合之后:

代码语言:javascript复制
void print_common_words(const std::string &text)
{
 return print_pairs(
  sort_by_frequency(
   reverse_pairs(
    count_occurrences(
     words(text)))));
}

函数提升 lifting

lifting是一种编程模式,它提供了一种方式,把给定的函数转换成一个类似可广泛应用的函数。

比如将一个操作字符串的函数 提升为可以操作 字符串,列表,字符指针,map和其他结构的函数。

实例:

把字符串转换成大写字母

代码语言:javascript复制
void to_upper(std::string& string_);
//字符串指针
void pointer_to_upper(std::string *str)
{
    if(str) to_upper(*str);
}
//字符串向量
void vector_to_upper(std::vector<string> &strs)
{
    for(auto& str:strs)
        to_upper(str);
}
//map
void map_to_upper(std::map<int, std::string> &strs)
{
    for(auto& pair:strs)
        to_upper(pair.second);
}

函数提升应用在上述函数:创建一个高阶函数,接收操作单个字符串的任意函数,并创建一个操作字符转指针的函数。把操作某一类型的函数提升为操作包含这种类型的结构或集合的函数。

代码语言:javascript复制
//使用auto*作为类型说明符,因此该函数不仅可用于字符串指针,也可以用于任意类型的指针
template<typename Function>
auto pointer_lift(Function f)
{
    return [f](auto *item){
        if(item){
            f(*item);
        }
    };
}

//不仅可用于字符串向量,还可用于任意类型的可遍历集合
template<typename Function>
auto collection_lift(Function f)
{
    return [f](auto& items){
        for(auto& item:items){
            f(item);
        }
    };
}

键值对列表反转

代码语言:javascript复制
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>

template <typename C, typename T = typename C::value_type>
std::unordered_map<T, unsigned int> count_occurences(
        const C &collection)
{
    std::unordered_map<T, unsigned int> result;

    for (const auto &c: collection) {
        result[c]  ;
    }

    return result;
}


// Reverses all pairs in a collection (section 4.4.1)
template <
    typename C,
    typename P1 = typename std::remove_cv<
             typename C::value_type::first_type>::type,
    typename P2 = typename C::value_type::second_type
    >
std::vector<std::pair<P2, P1>> reverse_pairs(const C &items)
{
    std::vector<std::pair<P2, P1>> result(items.size());

    // For each pair (first, second) in the source collection,
    // put (second, first) into the destination collection
    std::transform(
        std::begin(items), std::end(items),
        std::begin(result),
        [] (const std::pair<const P1, P2> &p)
        {
            return std::make_pair(p.second, p.first);
        }
    );

    return result;
}


int main(int argc, char *argv[])
{
    std::string sentence = "Hello world";
    std::vector<std::string> words { std::string("The"),
        std::string("Farm"),
        std::string("from"),
        std::string("the"),
        std::string("Animal"),
        std::string("Farm")
    };

    for (const auto& item: reverse_pairs(count_occurences(sentence))) {
        std::cout << item.first << " " << item.second << std::endl;
    }

    for (const auto& item: reverse_pairs(count_occurences(words))) {
        std::cout << item.first << " " << item.second << std::endl;
    }

    return 0;
}

注:来源 《函数式编程》 阅读整理

学习使用,如有侵权,联系立删!

认识一个人就是开了一扇窗户,就能看到不一样的东西,听到不一样的声音,能让你思考,觉悟,这已经够了。其他还有很多,比如机会,帮助,我不确定。这个在一般人看来可能不重要,但是我知道这个很重要。

我是小阳哥

希望用身边的人,身边的事

让我们少走一些弯路,一点点就好

0 人点赞