【STL实用技巧】函数对象(仿函数)与pair类型初探

本文涉及的产品
交互式建模 PAI-DSW,5000CU*H 3个月
简介: 【STL实用技巧】函数对象(仿函数)与pair类型初探

前言

STL容器中有一种集合容器set,set容器内部元素唯一并按照一定规则顺序排列。下面将通过set容器装入基本数据类型和类对象的实例,在程序中一步步分析函数对象的本质和使用方法,并延伸出自己在学习STL时的一些心得体会。

话题引出:set容器自动排序的实现

我们首先定义一个int类型的set容器,并放入int类型数据

1. #define _CRT_SECURE_NO_WARNINGS
2. #include <iostream>
3. using namespace std;
4. 
5. #include <set>
6. 
7. //存入基本数据类型
8. void FuncTest1()
9. {
10.   set<int, less<int>> s1;
11. 
12.   int temp;
13.   cout << "输入set的值:";
14.   cin >> temp;
15.   while (temp != EOF)
16.   {
17.     s1.insert(temp);
18.     cout << "输入set的值:";
19.     cin >> temp;
20.   }
21. 
22.   for (set<int, less<int>>::iterator it = s1.begin(); it != s1.end(); it++)
23.   {
24.     cout << *it << " ";
25.   }
26.   cout << endl;
27. 
28.   cout << "s1长度:" << s1.size() << endl;
29. 
30.   while (!s1.empty())
31.   {
32.     cout << *s1.begin() << " ";
33.     s1.erase(s1.begin());
34.   }
35.   cout << endl;
36. 
37.   s1.clear();
38.   cout << "s1长度:" << s1.size() << endl;
39. }
40. 
41. int main()
42. {
43.   FuncTest1();
44. 
45.   system("pause");
46.   return 0;
47. }

通过程序运行结果,我们可以初步了解到set容器元素唯一和自动排序的特性。那么有一个问题,set自动排序的规则是怎么确定的,这个规则是否可以由我们自己制定呢?

我们默认情况下定义一个容器

set<int> s;

它实际上是

set<int, less<int>> s;

这里的less就是一个函数对象,它规定了这个set容器内部排序规则为从小到大排序,实际上在set容器种装入元素时,他会首先进入less这个函数,满足规则才放入元素并根据规则存放,不满规则(重复数据)就放入失败,这个在后面会说。

这样,在set容器放入int型数据,实现自动排序的机制我们就了解了,他是采用了一个类似于回调函数的方式实现的。那么新的问题又出现了,当我放入一个int型数据的时候,set默认知道如何排序,那我们放入一个既有int数据又有字符串以及更复杂的数据类型时,set能否自动排序呢?我们通过下面的例子分析。

set容器装入类对象元素

首先我们定义一个类MyString

1. class MyString
2. {
3. public:
4.  MyString(const char* str)
5.  {
6.    if (str == NULL)
7.    {
8.      throw "str == NULL";
9.    }
10.     this->len = strlen(str);
11.     this->str = new char[this->len + 1];
12.     strcpy(this->str, str);
13.   }
14.   MyString(const MyString& s) //必须加const
15.   {
16.     this->len = s.len;
17.     this->str = new char[this->len + 1];
18.     strcpy(this->str, s.str);
19.   }
20.   ~MyString()
21.   {
22.     if (this->str != NULL)
23.     {
24.       delete[] this->str;
25.       this->str = NULL;
26.     }
27.     this->len = 0;
28.   }
29. public:
30.   friend ostream& operator<<(ostream& out, MyString s);
31. public:
32.   int   len;
33.   char* str;
34. };
35. 
36. ostream& operator<<(ostream& out, MyString s)
37. {
38.   out << "string: " << s.str << "\t" << "strlen: " << s.len << endl;
39.   return out;
40. }

这个类中包含两个成员,长度和字符串内容。这里需要注意一点,因为容器执行的是值拷贝,也就是把一个对象元素拷贝到容器中,所以在类中必须自定义一个拷贝构造函数,以免出现浅拷贝问题,并且装入set容器中的数据必须可以被拷贝(含有拷贝构造函数)。

定义好类后,我们在测试函数中放入类对象

1. void FuncTest2()
2. {
3.     set<MyString> s;   
4. 
5.  MyString s1("ccc");
6. 
7.  s.insert(s1);
8. }
9. 
10. int main()
11. {
12. system("pause");
13. return 0;
14. }

编译程序,发现编译器报错

我们双击错误提示定义到错误位置

我们发现错误被定位到了xstddef文件的第127行,这根本就不是我们自己的cpp文件,我们仔细看这段代码可以发现,原来这就是我们上面提到的

set<int, less<int>> s;

中的函数对象less的实现,我们可以猜测这应该STL的源码,分析这段源码可以看到,less函数有两个参数left和right,返回值是一个bool类型,该函数返回两个参数的比较结果,由此可以看出,这个比较规则并不能对我们的MyString类对象进行比较,因为他不知道是比较对象的len成员还是str成员。那么我们就可以自己定义一个函数对象来实现MyString类对象的比较规则,首先介绍函数对象的定义。

函数对象(仿函数)

在C语言中,我们可以通过函数指针做函数参数来实现C语言风格的回调函数,在回调函数中实现自己需要的回调行为,详细可见本人博客

image.png

以及

image.png

在C++中还提供了另一种实现回调函数的方法,就是函数对象,也叫仿函数。

函数对象就是一个重载了函数调用操作符的类所定义的对象 ,该类对象表现出类似于函数调用的一个行为,通过函数对象可以实现自定义的回调行为。总之,函数对象类似于函数的功能,其实质就是一个回调函数,会在适当时机进行调用,并执行预定义的规则。下面将通过自定义一个函数对象,一步一步的分析函数对象及调用时机。

自定义函数对象实现MyString对象的排序规则

STL提供的默认排序机制less并不能对我们的MyString类对象进行排序,也就是函数对象less提供的排序机制不知道我们自定义的数据该如何排序。初次接触,我们可能并不知道如何定义一个函数对象的接口,它的返回值,参数等。这时我们可以通过VS的转到定义功能来查看源码,源码是最好的书籍和实例。less函数对象的源码如下:

1. /*此为源码*/
2. // STRUCT TEMPLATE less
3. template <class _Ty = void>
4. struct less {
5.     _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
6.     _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
7.     _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;
8. 
9. constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const {
10. return _Left < _Right;
11.     }
12. };

我们可以模仿less实现一个简单的函数对象

1. struct StringFunc //可参考源码模仿
2. {
3.  //重载函数调用操作符,实现string类的排序功能
4.  constexpr bool operator()(const MyString& s1, const MyString& s2) const
5.  {
6.    if (s1.len < s2.len)
7.    {
8.      return true;
9.    }
10.     else
11.     {
12.       return false;
13.     }
14.   }
15. };

我们的规则是按照字符串的长度进行排序,使用我们自定义的函数对象定义一个容器

1. void FuncTest2()
2. {
3.  set<MyString, StringFunc> s;
4. 
5.  MyString s1("ccc");
6.  MyString s2("a");
7.  MyString s3("dddd");
8.  MyString s4("bb");
9.  MyString s5("eeeee");
10.   MyString s6("e");
11. 
12.   s.insert(s1);
13.     s.insert(s2);
14.   s.insert(s3);
15.   s.insert(s4);
16.   s.insert(s5);
17.   s.insert(s6);
18. 
19.   for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++)
20.   {
21.     cout << *it;
22.   }
23. }

编译运行

我们看到打印结果,字符串按照长度进行了顺序排列,且长度相同的元素s6没有插入到容器中。这时候,我们又发现一个新问题,s6是没有插入还是插入后被覆盖了,又或者是其他什么情况导致他没有进入容器,那么我们定义的StringFunc是在什么时候调用的呢?

insert函数返回值、pair类型、以及函数对象调用时机

点击insert,转到定义,可以看到insert函数的原型

1. /*此为源码*/
2. template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0>
3.     pair<iterator, bool> insert(const value_type& _Val) {
4. const auto _Result = _Emplace(_Val);
5. return {iterator(_Result.first, _Get_scary()), _Result.second};
6.     }

我们看到insert这个函数是有返回值的,它返回一个pair<iterator, bool>。pair是一个对组,它将两个值放在一起当作一个单位使用,两个值可以是相同类型,也可以是不同类型。这里insert函数返回的一个是迭代器,一个是bool类型。我们可以通过pair的第二个值来判断插入是否成功。

我们把代码升级一下,通过一个pair变量来判断插入是否成功

1. void FuncTest2()
2. {
3.  set<MyString, StringFunc> s;
4. 
5.  MyString s1("ccc");
6.  MyString s2("a");
7.  MyString s3("dddd");
8.  MyString s4("bb");
9.  MyString s5("eeeee");
10.   MyString s6("e");
11. 
12.   s.insert(s1);
13.   pair<set<MyString, StringFunc>::iterator, bool> my_pair;
14.   my_pair = s.insert(s2); 
15.   if (my_pair.second == true)
16.   {
17.     cout << "insert success" << "\t" << *(my_pair.first);
18.   }
19.   else
20.   {
21.     cout << "insert failed" << endl;
22.   }
23.   s.insert(s3);
24.   s.insert(s4);
25.   s.insert(s5);
26.   my_pair = s.insert(s6);
27.   if (my_pair.second == true)
28.   {
29.     cout << "insert success" << "\t" << *(my_pair.first);
30.   }
31.   else
32.   {
33.     cout << "insert failed" << endl;
34.   }
35. 
36.   for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++)
37.   {
38.     cout << *it;
39.   }
40. }

我们在

my_pair = s.insert(s2);

处加一个断点,开始调试。通过单步调试可以看到,在插入元素前会先进入函数对象StringFunc所重载的()操作符函数中,如果插入成功,再去执行MyString的拷贝构造函数,把元素拷贝到容器中;如果返回false即容器中已经有同样长度(不符合规则)的元素,那么就不再执行拷贝构造函数复制元素了。

总结

学习STL遇到不认识的类型,不会的语法,可以通过VS的转到定义功能去查看源码。另外一定要多实践,把程序加断点一步一步的调试,观察程序每一步的行为,这样才能深刻理解C++程序和编译器的每一步行为。学习最重要的不是知识点本身,而是学习的方法和对待问题形成自己的理解。最后贴上完整程序:

1. #define _CRT_SECURE_NO_WARNINGS
2. #include <iostream>
3. using namespace std;
4. 
5. #include <set>
6. 
7. //存入基本数据类型
8. void FuncTest1()
9. {
10.   set<int, less<int>> s1;
11. 
12.   int temp;
13.   cout << "输入set的值:";
14.   cin >> temp;
15.   while (temp != EOF)
16.   {
17.     s1.insert(temp);
18.     cout << "输入set的值:";
19.     cin >> temp;
20.   }
21. 
22.   for (set<int, less<int>>::iterator it = s1.begin(); it != s1.end(); it++)
23.   {
24.     cout << *it << " ";
25.   }
26.   cout << endl;
27. 
28.   cout << "s1长度:" << s1.size() << endl;
29. 
30.   while (!s1.empty())
31.   {
32.     cout << *s1.begin() << " ";
33.     s1.erase(s1.begin());
34.   }
35.   cout << endl;
36. 
37.   s1.clear();
38.   cout << "s1长度:" << s1.size() << endl;
39. }
40. 
41. class MyString
42. {
43. public:
44.   MyString(const char* str)
45.   {
46.     if (str == NULL)
47.     {
48.       throw "str == NULL";
49.     }
50.     this->len = strlen(str);
51.     this->str = new char[this->len + 1];
52.     strcpy(this->str, str);
53.   }
54.   MyString(const MyString& s) //必须加const
55.   {
56.     this->len = s.len;
57.     this->str = new char[this->len + 1];
58.     strcpy(this->str, s.str);
59.   }
60.   ~MyString()
61.   {
62.     if (this->str != NULL)
63.     {
64.       delete[] this->str;
65.       this->str = NULL;
66.     }
67.     this->len = 0;
68.   }
69. public:
70.   friend ostream& operator<<(ostream& out, MyString s);
71. public:
72.   int   len;
73.   char* str;
74. };
75. 
76. ostream& operator<<(ostream& out, MyString s)
77. {
78.   out << "string: " << s.str << "\t" << "strlen: " << s.len << endl;
79.   return out;
80. }
81. 
82. //定义一个仿函数
83. /*
84. template <class _Ty = void>
85. struct less {
86.   _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
87.   _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
88.   _CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;
89. 
90.   constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const {
91.     return _Left < _Right;
92.   }
93. };
94. */
95. struct StringFunc //可参考源码模仿
96. {
97.   //重载函数调用操作符,实现string类的排序功能
98.   constexpr bool operator()(const MyString& s1, const MyString& s2) const
99.   {
100.    if (s1.len < s2.len)
101.    {
102.      return true;
103.    }
104.    else
105.    {
106.      return false;
107.    }
108.  }
109. };
110. 
111. void FuncTest2()
112. {
113.  set<MyString, StringFunc> s;
114.  //set<MyString> s; //不知道该如何排序
115.  //错误  C2676 二进制“ < ”:“const _Ty”不定义该运算符或到预定义运算符可接收的类型的转换  
116. 
117.  MyString s1("ccc");
118.  MyString s2("a");
119.  MyString s3("dddd");
120.  MyString s4("bb");
121.  MyString s5("eeeee");
122.  MyString s6("e");
123. 
124.  s.insert(s1);
125.  /*
126.  template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0>
127.  pair<iterator, bool> insert(const value_type& _Val) {
128.    const auto _Result = _Emplace(_Val);
129.    return {iterator(_Result.first, _Get_scary()), _Result.second};
130.  }
131.  */
132.  pair<set<MyString, StringFunc>::iterator, bool> my_pair;
133.  my_pair = s.insert(s2); //先调用(),再调用拷贝构造函数
134.  if (my_pair.second == true)
135.  {
136.    cout << "insert success" << "\t" << *(my_pair.first);
137.  }
138.  else
139.  {
140.    cout << "insert failed" << endl;
141.  }
142.  s.insert(s3);
143.  s.insert(s4);
144.  s.insert(s5);
145.  my_pair = s.insert(s6);
146.  if (my_pair.second == true)
147.  {
148.    cout << "insert success" << "\t" << *(my_pair.first);
149.  }
150.  else
151.  {
152.    cout << "insert failed" << endl;
153.  }
154. 
155.  for (set<MyString, StringFunc>::iterator it = s.begin(); it != s.end(); it++)
156.  {
157.    cout << *it;
158.  }
159. }
160. 
161. int main()
162. {
163.  //FuncTest1();
164.  FuncTest2();
165. 
166.  system("pause");
167.  return 0;
168. }


相关实践学习
使用PAI-EAS一键部署ChatGLM及LangChain应用
本场景中主要介绍如何使用模型在线服务(PAI-EAS)部署ChatGLM的AI-Web应用以及启动WebUI进行模型推理,并通过LangChain集成自己的业务数据。
机器学习概览及常见算法
机器学习(Machine Learning, ML)是人工智能的核心,专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能,它是使计算机具有智能的根本途径,其应用遍及人工智能的各个领域。 本课程将带你入门机器学习,掌握机器学习的概念和常用的算法。
相关文章
|
1月前
|
存储 算法 编译器
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
237 0
|
29天前
|
算法 编译器 C++
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits 判断 Lambda表达式类型?
【C/C++ 泛型编程 应用篇】C++ 如何通过Type traits 判断 Lambda表达式类型?
36 4
|
2月前
|
存储 算法 C++
【C++】STL的基本用法
【C++】STL的基本用法
37 0
|
3月前
|
C++ 容器
|
2月前
|
C++
C++模板与STL【函数对象】
C++模板与STL【函数对象】
|
8月前
|
存储 C++ 索引
1.1 C++ STL 字符串构造函数
String 字符串操作容器是C++标准中实现的重要容器,其主要用于对字符串的高效处理,它和C风格中的`string.h`并不是同一个库,两个库有极大的差距,C库中的`string.h`主要面向过程提供一些处理函数,而C++库中的`string`则是基于类实现的更高效的一种字符串处理方法集,类中提供了非常方便的成员函数供我们使用.
33 0
|
4月前
|
存储 算法 Linux
【STL】:vector用法详解
【STL】:vector用法详解
48 1
|
4月前
|
编译器 C++ 容器
STL中的string类的用法以及模拟实现
STL中的string类的用法以及模拟实现
29 0
|
10月前
|
C++
STL-函数对象(仿函数)
STL-函数对象(仿函数)
39 0
|
C++
【C++函数对象】STL基础语法学习 | 仿函数&谓词&内建仿函数
重载函数调用操作符的类,其对象常称为函数对象。函数对象使用重载的()时,行为类似函数的调用,所以也叫仿函数。它的本质为一个类,而不是一个函数。
82 0
【C++函数对象】STL基础语法学习 | 仿函数&谓词&内建仿函数

热门文章

最新文章