一、函数、函数指针及函数对象
1.1 函数
函数(function)是把一个语句序列(函数体, function body)关联到一个名字和零或更多个函数形参(function parameter)的列表的 C++ 实体,可以通过返回或者抛出异常终止。。
// 函数名:func
// 形参列表拥有一个形参,具有名字 cnt 和类型 int
// 返回类型是 void
void func(int cnt)
{ // 函数体开始
std::cout << (cnt % 2);
} // 函数体结束
函数声明可以在任何作用域中出现,但函数定义只能在命名空间作用域出现,或对于成员和友元函数,可以在类作用域中出现。在类体中声明而不带 friend 说明符的函数是类成员函数。这种函数拥有许多附加性质。
每个函数都具有一个类型,它由函数的返回类型,所有形参的类型(形参列表),函数是否为 noexcept (C++17 起),以及对于非静态成员函数的 cv 限定性和引用限定性 (C++11 起)构成。函数类型同样拥有语言链接。不存在有 cv 限定的函数类型(不要与如 int f() const; 这样的 cv 限定函数类型,或如 std::string const f(); 这样的返回 cv 限定类型的函数相混淆)。如果有任何 cv 限定符被添加到到函数类型的别名,那么它会被忽略。
1.2 函数指针
函数不能按值传递或被其他函数所返回。可以有指向/到除主函数以外的所有函数 (C++20 前)所有非标准库函数和几个标准库函数 (C++20 起)的指针和引用,它们可以用于这些函数自身无法被使用的地方。因此我们说这些函数“可取址”。
void (*pf)() = &func;
pf();
函数指针能以非成员函数或静态成员函数的地址初始化。由于存在函数到指针的隐式转换,取址运算符可以忽略。不同于函数或函数的引用,函数指针是对象,从而能存储于数组、被复制、被赋值等。
void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // 与 &f 相同
更多关于函数指针说明见本专栏关于函数指针的博文:c/c++开发,无可避免的函数指针使用案例_py_free-物联智能的博客-CSDN博客
1.3 函数对象
函数调用表达式还支持函数指针以及重载了函数调用运算符及可转换为函数指针的任何类类型的值(包括 lambda 表达式) (C++11 起)。这些类型被统称为函数对象 (FunctionObject)。
函数对象是指可以像函数一样被调用的对象,它可以重载函数调用运算符(),并且可以像函数一样接受参数和返回值。任何定义了函数调用操作符的对象都是函数对象。C++ 支持创建、操作新的函数对象,同时也提供了许多内置的函数对象。
函数对象可以作为参数传递给函数,也可以作为返回值返回给调用者。函数对象可以用于实现一些特定的算法,例如排序、查找等。在c/c++标准库中,函数对象被广泛应用于算法和容器中,例如sort()函数中可以传递一个函数对象作为排序规则,或者在map容器中可以使用一个函数对象作为比较器来进行元素的排序和查找。此外,函数对象还可以用于实现自定义的操作符重载,例如可以定义一个函数对象来实现自定义的加法操作。
函数对象和函数指针都可以作为函数的参数或返回值,但它们的实现方式不同。函数对象是一个类,它重载了函数调用运算符(),可以像函数一样被调用。而函数指针是一个指向函数的指针,可以通过指针调用函数。另外,函数对象可以保存状态,而函数指针不可以。
二、线程与函数
线程是程序中的一条执行路径,通过执行多个线程,可以让程序在同一时间执行多个任务,提高程序的并发性和效率。
函数指针是指指向函数的指针变量,可以将函数作为参数传递给其他函数,或者将函数作为返回值返回给调用者;函数对象是一个类,它重载了 () 运算符,可以像函数一样被调用。它们都是 c/c++语言中非常实用的特性,可以更好地实现复杂的功能。在多线程编程中,函数指针或函数对象可以作为线程的入口函数,让线程执行特定的任务。
2.1 线程通过函数体入口获得真正任务执行语句
下面来看一下线程与函数指针或函数对象结合,完成执行任务的例子:
//test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void func_ptr_test(void);
#endif //_TEST_1_H_
//test1.cpp
#include "test1.h"
#include <thread>
#include <atomic>
#include <iostream>
#include <functional>
std::atomic<int> cnt{0};
void func(){ cnt++;};
typedef void (*pfunc)();
using pfunc_us = void (*)();
//using pfunc_us=pfunc;
void func_ptr_test(void)
{
//函数引用
std::thread t1{func}; // OK
t1.join();
std::cout << "cnt = " << cnt << "\n";
//函数指针
void (*pf)() = &func;
std::thread t2{pf}; // OK
t2.join();
std::cout << "cnt = " << cnt << "\n";
//函数指针别名
pfunc pfc = &func;//= func;
std::thread t3{pfc}; // OK
t3.join();
std::cout << "cnt = " << cnt << "\n";
//函数指针别名
pfunc_us pfus = &func;//= func;
std::thread t4{pfus}; // OK
t4.join();
std::cout << "cnt = " << cnt << "\n";
//函数对象,Lambda 表达式,闭包
auto f = [&]{cnt++;};
std::thread t5{f}; // OK
t5.join();
std::cout << "cnt = " << cnt << "\n";
//函数包裹器
std::function<void()> f_display = [&]{cnt++;};
std::thread t6{f}; // OK
t6.join();
std::cout << "cnt = " << cnt << "\n";
//函数包裹器
std::function<void()> f_f = &func;
std::thread t7{f_f}; // OK
t7.join();
std::cout << "cnt = " << cnt << "\n";
//函数包裹器
std::function<void()> f_fb = std::bind(func);
std::thread t8{f_fb}; // OK
t8.join();
std::cout << "cnt = " << cnt << "\n";
};
//main.cpp
#include "test1.h"
int main(int argc, char* argv[])
{
func_ptr_test();
return 0;
};
在这个例子可以看到,通过函数引用、函数指针及函数指针别名、函数对象Lambda 表达式、函数对象包裹器等均可作为入口地址传递给线程类,线程运行是通过函数入口地址开始执行语句的。线程执行任务的真正实现内容是在传递进去函数体内完成。
编译g++ main.cpp test*.cpp -o test.exe -std=c++11,运行程序:
2.2 Lambda 表达式与函数
在上述例子中,有两个很新颖的应用Lambda 表达式和std::function类模板。先来看Lambda 表达式,它能够捕获作用域中的变量的无名函数对象。
标准库为其提供了一系列支持:
//语法支持
/*(1),完整声明,捕获,包含零或更多个捕获符的逗号分隔列表,可以默认捕获符(capture-default)起始
*由说明符、 异常说明、 属性和尾随返回类型 按顺序组成,每个组分均非必需
* (C++20 起)向闭包类型的 operator() 添加约束
*默认捕获符有&(以引用隐式捕获被使用的自动变量)和=(以复制隐式捕获被使用的自动变量)。
*/
[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) { 函数体 }
/*(2),省略形参列表:函数不接收实参,如同形参列表是 ()*/
[ 捕获 ] { 函数体 } //(C++23 前)
[ 捕获 ] lambda说明符 { 函数体 } //(C++23 起)
/*(3),与(1) 相同,但指定泛型 lambda 并显式提供模板形参列表
*模板形参列表不能为空(不允许 <>)。*/
[ 捕获 ] <模板形参> 约束(可选)( 形参 ) lambda说明符 约束(可选) { 函数体 } //(C++20 起)
/*(4),与 (2) 相同,但指定泛型 lambda 并显式提供模板形参列表。
*模板形参列表不能为空(不允许 <>)。*/
[ 捕获 ] <模板形参> 约束(可选) { 函数体 } // (C++20 起)(C++23 前)
[ 捕获 ] <模板形参> 约束(可选) lambda说明符 { 函数体 } //(C++23 起)
lambda 表达式是纯右值表达式,它的类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type),它(对于 ADL 而言)在含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域声明。
//闭包类型::operator()(形参)
返回类型 operator()(形参) const { 函数体 } //(未使用关键词 mutable)
返回类型 operator()(形参) { 函数体 } //(使用了关键词 mutable)
template<模板形参>返回类型 operator()(形参) const { 函数体 } //(C++14 起)(泛型 lambda)
template<模板形参>返回类型 operator()(形参) { 函数体 } //(C++14 起)(泛型 lambda,使用了关键词 mutable)
前面例子,因为采用了auto关键字,它是一个泛型 lambda(当以 auto 为形参类型或显式提供模板形参列表 (C++20 起)时,该 lambda 为泛型 lambda)。它捕获([&])一个函数的引用,传入线程是,线程会获得函数体{cnt++;},即创建线程后,将执行cnt++;语句后,线程就完成了执行任务。
// 泛型 lambda,operator() 是无形参的模板
auto f = [&]{cnt++;};
//auto f = [&](void){cnt++;};
std::thread t5{f};
更多的关于lambda 表达式的用法及格式要求不是本博文关注重点,就不在这里展开:
Lambda 捕获标识符:空、...、初始化器、&、&...、&初始化器、this、*this、...初始化器、&...初始化器
/*注,初始化器:(表达式)、=表达式、{表达式}*/
闭包类型::捕获
闭包类型::operator 返回类型(*)(形参)()
闭包类型::闭包类型()
闭包类型::operator=(const 闭包类型&)
闭包类型::~闭包类型()
2.3 函数包装器-std::function类模板
std::function类模板是定义在标准库头文件 <functional>,此头文件是函数对象库的一部分并提供标准散列函数。该头文件包含很多函数对象相关类模板及函数模板,本文就只讲述std::function和std::bind。
function (C++11) 包装具有指定函数调用签名的任意可复制构造类型的可调用对象(类模板)
bind (C++11) 绑定一或多个实参到函数对象
类模板 std::function 是通用多态函数包装器。 std::function 的实例能存储、复制及调用任何可复制构造 (CopyConstructible) 的可调用 (Callable) 目标——函数、 lambda 表达式、 bind 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。
//std::function,定义于头文件 <functional>,(C++11 起)
template< class >class function;
template< class R, class... Args > class function<R(Args...)>;
std::function 满足可复制构造 (CopyConstructible) 和可复制赋值 (CopyAssignable) 。类模板std::function的成员:
成员类型
类型 定义
result_type R
argument_type //(C++17 中弃用)(C++20 中移除) 若 sizeof...(Args)==1 且 T 是 Args... 中首个且唯一的类型,则为 T
first_argument_type//(C++17 中弃用)(C++20 中移除) 若 sizeof...(Args)==2 且 T1 是 Args... 中二个类型的第一个,则为 T1
second_argument_type//(C++17 中弃用)(C++20 中移除) 若 sizeof...(Args)==2 且 T2 是 Args... 中二个类型的第二个,则为 T2
成员函数
(构造函数) 构造新的 std::function 实例(公开成员函数)
(析构函数) 析构 std::function 实例(公开成员函数)
operator= 为内容赋值(公开成员函数)
swap 交换内容(公开成员函数)
assign (C++17 中移除) 为内容赋值一个新的目标(公开成员函数)
operator bool 检查是否包含了有效的目标(公开成员函数)
operator() 调用其目标(公开成员函数)
目标访问
target_type 获得 std::function 所存储的目标的typeid(公开成员函数)
target 获得指向 std::function 所存储的目标的指针(公开成员函数)
非成员函数
std::swap(std::function) (C++11)特化 std::swap 算法(函数模板)
operator== 比较 std::function 和 nullptr(函数模板)
operator!= (C++20 中移除)比较 std::function 和 nullptr(函数模板)
辅助类
std::uses_allocator<std::function> (C++11) (C++17 前)特化std::uses_allocator类型特性(类模板特化)
类模板 std::function的声明定义:
namespace std {
template<class> class function; // 不定义
template<class R, class... ArgTypes>
class function<R(ArgTypes...)> {
public:
using result_type = R;
// 构造/复制/销毁
function() noexcept;
function(nullptr_t) noexcept;
function(const function&);
function(function&&) noexcept;
template<class F> function(F);
function& operator=(const function&);
function& operator=(function&&);
function& operator=(nullptr_t) noexcept;
template<class F> function& operator=(F&&);
template<class F> function& operator=(reference_wrapper<F>) noexcept;
~function();
// function 修改器
void swap(function&) noexcept;
// function 容量
explicit operator bool() const noexcept;
// function 调用
R operator()(ArgTypes...) const;
// function 目标访问
const type_info& target_type() const noexcept;
template<class T> T* target() noexcept;
template<class T> const T* target() const noexcept;
};
template<class R, class... ArgTypes>
function(R(*)(ArgTypes...)) -> function<R(ArgTypes...)>;
template<class F> function(F) -> function</* see description */>;
// 空指针比较函数
template<class R, class... ArgTypes>
bool operator==(const function<R(ArgTypes...)>&, nullptr_t) noexcept;
// 特化的算法
template<class R, class... ArgTypes>
void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&) noexcept;
}
从std::function类名板声明可以看出,其模板参数是由一个泛型返回类型R和多个泛型参数类型ArgTypes...组成,最终构成类似于lambda表达式的指向R(*)(ArgTypes...){};函数体。
三、线程与成员函数
3.1 函数转发包装器- std::bind
讲述std::function类模板时,涉及到一个重要的函数模板 std::bind ,它可以生成 函数f 的转发调用包装器。调用此包装器等价于以一些绑定到 args 的参数调用函数 f上。
/*std::bind,定义于头文件 <functional>
*参数:
*1)f - 可调用 (Callable) 对象(函数对象、指向函数指针、到函数引用、指向成员函数指针或指向数据成员指针)
*2)args - 要绑定的参数列表,未绑定参数为命名空间 std::placeholders 的占位符 _1, _2, _3... 所替换
*返回值:
*未指定类型 T 的函数对象,满足 std::is_bind_expression<T>::value == true 。
*/
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args ); (C++11 起)(C++20 前)
template< class F, class... Args >
constexpr /*unspecified*/ bind( F&& f, Args&&... args ); (C++20 起)
template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args ); (C++11 起)(C++20 前)
template< class R, class F, class... Args >
constexpr /*unspecified*/ bind( F&& f, Args&&... args ); (C++20 起)
简单来说,函数模板 std::bind就是将函数(包括操作符)、成员函数(包括操作符)进行函数转换包装给调用者使用,相当于使用函数对象指针一样方便。调用指向非静态成员函数指针或指向非静态数据成员指针时,首参数必须是引用或指针(可以包含智能指针,如 std::shared_ptr 与 std::unique_ptr),指向将访问其成员的对象。因此可以将std::bind指向的对象作为函数入口地址传递给线程,和将函数引用或指针传递是一样效果。
3.2 std::bind与线程
//test2.h
#ifndef _TEST_2_H_
#define _TEST_2_H_
void cfunc_ptr_test(void);
#endif //_TEST_2_H_
//test2.cpp
#include "test2.h"
#include <thread>
#include <iostream>
#include <functional>
#include <memory>
struct ATest {
void print1(int i) const {
std::cout << "ATest::print1 i = " << i << '\n';
};
};
struct CTest {
CTest(int num) : num_(num) {};
void operator()(int i) const
{
std::cout << "CTest::operator i = " << i << '\n';
};
void print_add(int i) const {
std::cout << "CTest::print_add num_+i = " << num_+i << '\n';
};
static void print2(int i)
{
std::cout << "CTest::print2 2*i = " << 2*i << '\n';
};
struct BTest
{
void print1(int i) const {
std::cout << "CTest::BTest::print1 i = " << i << '\n';
};
};
int num_;
ATest at;
BTest bt;
};
void cfunc_ptr_test(void)
{
//绑定到成员函数
CTest ct(1);
//f1类型:void (CTest::*(CTest*,int))(int i) const
auto f1 = std::bind(&CTest::print_add, &ct, 10);//10为函数传入参数,&ct为传入地址
std::thread t1{f1}; // OK
t1.join();
//绑定到成员函数,符号索引参数
using std::placeholders::_1;
auto f2 = std::bind(&CTest::print_add, &ct, _1);
std::thread t2{f2,2 }; // OK,2为函数传入参数
t2.join();
//绑定到成员操作符
auto f3 = std::bind(&ct, &ct, _1); //CTest::operator()
// auto f3 = std::bind(&CTest::operator(), &ct, _1);
std::thread t3{f2,3 }; // OK,3为函数传入参数
t3.join();
//绑定智能指针指向的成员函数
std::unique_ptr<CTest> pct(new CTest(1));
auto f4 = std::bind(&CTest::print_add, &*pct.get(), _1);
std::thread t4{f4,4 }; // OK,4为函数传入参数
t4.join();
//绑定智能指针指向的成员函数
auto f5 = std::bind(&CTest::operator(), &*pct.get(), _1);//CTest::operator()
std::thread t5{f5,5 }; // OK,5为函数传入参数
t5.join();
//静态成员函数作为线程入口
// auto f6 = &CTest::print2;
// std::thread t6{f6,6 }; // OK,6为函数传入参数
std::thread t6{&CTest::print2,6 }; // OK,6为函数传入参数
t6.join();
//
auto f7 = std::bind(&ATest::print1, &ct.at, _1);
// f7(7);
std::thread t7{f7,7 }; // OK,7为函数传入参数
t7.join();
//
auto f8 = std::bind(&CTest::BTest::print1, &ct.bt, _1);
// f8(8);
std::thread t8{f8,8 }; // OK,8为函数传入参数
t8.join();
//绑定到成员函数,直接在bind内初始化一个对象传入
auto f9 = std::bind(&CTest::print_add, new CTest(1), _1);//10为函数传入参数,&ct为传入地址
std::thread t9{f9,9}; // OK
t9.join();
};
//main.cpp
#include "test2.h"
int main(int argc, char* argv[])
{
cfunc_ptr_test();
return 0;
};
另外,标准库还有和std::bind相差不大的函数转发调用转发调用包装:std::bind_front,这是C++20的标准。调用此包装等价于绑定首 sizeof...(Args) 个参数到 args 再调用函数 f,这里就不细说了,有兴趣的自行查看C++20资料。
编译g++ main.cpp test2.cpp -o test.exe -std=c++11,运行生产程序: