编程语言
首页 > 编程语言> > c++ 可调用类型(callable type)

c++ 可调用类型(callable type)

作者:互联网

可调用类型(callable type)

目录

目录

一、概要

典型的可调用类型自然是函数类型,但函数类型不能用来定义一个函数对象,只能声明一个函数对象。

using F = void(int); // 函数类型
F f; 			// 函数类型不能用来定义一个函数对象,只能声明

除去函数类型之外,c++ 中还有其他可调用类型,这些可调用类型可以直接定义一个可调用对象。比如函数指针定义了一个指针对象,该对象可以指向任意一个同类型函数;函数引用定义了一个对象,该对象可以指向任意一个同类型函数。

(一)函数指针、函数引用

void show(int x) { cout << x << endl;}
void (*p)(int) = show; p(3);  // 函数指针
void (&r)(int) = show; r(4);	  // 函数引用

(二)成员函数指针

struct A {
	void show(const string& str)  { cout << str << endl; }
};
void (A:: * pm)(const string&) = &A::show;
A a;
(a.*pm)("hello world");	

暂时没有“成员函数引用”这种东西。

(三)仿函数(Functor)

顾名思义,就是用类来模仿函数,类通过重载 operator() 运算符,可以使类实例像函数一样被调用。

struct A { void operator()(int x) { cout << x << endl; } };
A a;  a(23);

(四)lambda 表达式

lambda 表达式本身也是一种仿函数。只不过,与一般仿函数类相比,编译器会根据 lambda 表达式生成一个匿名类;与一般仿函数类实例相比,lambda 表达式是一个匿名仿函数类实例。

[]() {return 0; };  // 一个十分简单的 lambda 表达式

/* 编译器生成的对应的内部表示 */
class __lambda_11_5 { 					// 仿函数类
public: 
	inline int operator()() const { 	// operator() 重载
      return 0;
    }
    
    using retType_11_5 = int (*)();
    inline operator retType_11_5 () const noexcept{
      return __invoke;
    };
private: 
    static inline int __invoke()
    {
      return 0;
    }
} __lambda_11_5{}; 						// 匿名实例

(五)一个可被转换为函数指针的类对象

c++ 的类可以重载类型转换运算符 operator type() ,如果类型转换转向了函数指针或函数引用类型,那么该类实例也将成为可调用对象,但这种调用是以隐式转换的方式。

void show(int x) { cout << "show:" << x << endl; }
struct A {
    using pFunc = void(*)(int);			// 函数指针类型
    operator pFunc() { return show; }   // 可以返回 show,也可以返回 &show,两者含义相同
};
struct B {
    using rFunc = void(&)(int);			// 函数引用类型
    operator rFunc() { return show; }	
};

A a;    a(4); // 打印 show:4,说明完成了隐式类型转换
B b;	b(5); // 打印 show:5,说明完成了隐式类型转换

对于 operator type() 的说明:

(1)虽然是成员函数,但该函数没有返回值;

(2)不仅没有返回值,而且也不能有形参;

(3)通常情况下带有 const,所以一般形式是 operator type() const

调用显式类型转换自然也是可以,但此时与一般函数的调用形式不符,既没必要也不方便。

static_cast<A::pFunc>(a)(4); // 打印 show:4
static_cast<B::rFunc>(b)(5); // 打印 show:5

二、函数类型

(一)概要

1、函数类型都包括哪些信息?

指定了返回值、参数表、异常规约(noexcept)可以定义函数类型。

2、函数原型、函数头、函数签名

函数声明通常称为"函数原型"或"函数头",以强调这代表函数的访问方式,而不是具体代码。术语"函数签名"指将函数名与形参列表组合在一起,但没有返回类型。

3、什么是回调函数?

在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。

image-20220416161958473

#include <iostream> // std::cout 
#include <functional> // std::function 
class A { 
    std::function<void()> callback_; // 类成员
public: 
    A(const std::function<void()>& f) : callback_(f) {} // 构造时接收一个可调用对象,然后保存在 callback_ 中
    void notify(void) { 
        callback_(); // 调用之前接收的可调用对象
	} 
};
class Foo {
public: 
    void operator()(void) { std::cout << __FUNCTION__ << std::endl; } 
};
int main(void) { 
    Foo foo; 
    A aa(foo); 
    aa.notify(); 
    return 0; 
}

4、当前函数名称

每个函数都有一个预定义的局部变量 __func__ ,其中包含当前函数的名称。这个变量的一个用途是用于日志记录:

int fun(int x, int y) {
    std::cout << "entering function:" << __func__ << endl; // __func__ 不是宏,而是预定义的局部变量
    return x + y;
}

(二)函数类型别名

noexcept 属于函数类型的一部分,但定义函数类型别名时,不允许出现 noexcept

using func = void(void);			// ok
using func = void(void) noexcept; 	//error,using别名不能有noexcept

typedef void func(void);			// ok
typedef void func(void) noexcept; 	//error,typedef别名不能有noexcept

函数类型别名是为了更方便使用函数类型,而函数类型在使用时只能是声明一个函数,具体函数定义还需要额外补充。

using func = void(void); // 定义函数类型别名
func f; 		// 使用别名声明一个函数,等价于
void f(void); 	//函数声明

// 函数调用前还需要在某处补充函数定义
f(); 	//函数调用,需要有对应的函数定义,否则链接错误
void f(void) {  cout << "f" << endl; } //函数定义

(三)调用规约

__cdecl

__fastcall

__stdcall

三、函数引用、函数指针

(一)概要

函数类型也有对应的指针、引用类型,这可以更方便地调用函数。

using Function = int(int, int, int);	// 函数类型 int(int,int,int),类型别名 Function
Function func;							// 声明一个函数 func
Function* pf = func;					// 函数指针	pf 绑定函数 func
Function* pf2 = &func;					// 函数指针	pf2 绑定函数 func,另一种绑定格式
Function& rf = func;					// 函数引用 rf 绑定函数 func

(二)函数引用、函数指针 与 noexcept

类型别名不能指定 noexcept,默认是 non-noexcept。但是定义函数引用、函数指针时可以绑定 noexcept。

using pfun = void(*)(void); 	//ok,non-noexcept
typedef void(*pfun)(void); 		//ok,non-noexcept
using pfun = void(*)(void) noexcept; 	//error,别名不能有noexcept
typedef void(*pfun)(void) noexcept; 	//error,别名不能有noexcept

using rfun = void(&)(void); 	//ok,non-noexcept
typedef void(&rfun)(void); 		//ok,non-noexcept
using rfun = void(&)(void) noexcept; 	//error,别名不能有noexcept
typedef void(&rfun)(void) noexcept; 	//error,别名不能有noexcept

void (*pf)(int)noexcept = foo;	//ok,函数指针,绑定noexcept函数
void (&rf)(int)noexcept = foo;	//ok,函数引用,绑定noexcept函数

(三)初始化与赋值

函数名不能给函数类型初始化或赋值。

func f = show; //error,两个函数名之间不能初始化和赋值

函数名可以初始化/赋值给函数指针,函数类型到函数指针类型默认隐式转换。

void show(void){...} 

using func = void(void); 
func* p = show;			//函数类型到函数指针类型的隐式转换;
using func = void(void); 
func* p = &show;

//格式注意:*p 需要被括号括起来,否则 p 是返回 void* 的函数类型声明的函数(且无定义,调用会报找不到符号)
void(*p)(void) = show;	//函数类型到函数指针类型的隐式转换;
void(*p)(void) = &show;	

函数引用可以初始化/赋值给函数指针;

void(&r)(void) = foo;
void(*p)(void) = r; //函数引用可以初始化/赋值给函数指针

函数指针需要解引用之后才能初始化/赋值给函数引用。

void(*p)(void) =foo;
void(&r)(void) =*p; //函数指针解引用后才能初始化/赋值给函数指针

(四)调用函数指针

通过函数引用调用函数没什么特别的,而通过函数指针调用某个函数时,却既可以解引用,也可以不解引用。

void show(int x) { cout << x << endl; }

void(&rf)(int) = show;
rf(34); 	// 打印 34

void(*pf)(int) = show;
pf(23);		// 打印 23
(*pf)(78);	// 打印 78

(五)做函数形参

函数参数是函数类型,会自动类型转换为函数指针类型(类似于数组名做函数参数,会自动退化为指向数组元素的指针类型)。所以函数类型与函数指针类型不能重载。

类似的,虽然函数引用不会转换为函数指针类型,但函数引用类型与函数类型、函数指针类型之间也不能重载。

// 以下三个函数不能重载
void func(int var, void(f)(int));		//f是函数类型,自动转为函数指针类型
void func(int var, void(*pf)(int));		//pf是函数指针类型
void func(int var, void(&rf)(int));		//rf是函数引用类型,不会转换为函数指针类型

(六)做函数返回值

返回值不能是函数类型,但可以是函数指针、函数引用类型(类似于因为数组不能拷贝,所以返回值不能是数组类型,可以是数组的指针或引用类型)。

using f = void(int);
using pf = void(*)(int);
using rf = void(&)(int);

f func(float); 	// error,不能返回函数类型
f* func(int); 	// 返回void(*)(int)类型的函数func
pf func(char);	// 返回void(*)(int)类型的函数func
f& func(int); 	// 返回void(&)(int)类型的函数func
rf func(char);	// 返回void(&)(int)类型的函数func

void (*func(long))(int);	// 返回void(*)(int)类型的函数func
void (&func(long))(int);	// 返回void(&)(int)类型的函数func

auto func1(double)->void(*)(int);	// 尾置返回类型是void(*)(int)类型的函数func
auto func2(double)->void(&)(int);	// 尾置返回类型是void(&)(int)类型的函数func
decltype(func1(3.)) func(std::string);	// decltype对func1(double)返回值类型进行推断,得到void(*)(int)的函数指针类型
decltype(func2(3.)) func(std::string);	// decltype对func2(double)返回值类型进行推断,得到void(&)(int)的函数引用类型
decltype(foo)* func(std::vector<int>);	// decltyp对函数名foo推断为函数类型,需要加'*'号表示函数指针类型
decltype(foo)& func(std::vector<int>);	//decltyp对函数名foo推断为函数类型,需要加'&'号表示函数引用类型

(七)类型转换

不同函数指针/引用类型间不存在转换规则

void (*p)(unsigned int);
auto p2 = static_cast<void(*)(int)>(p);		//类型转换无效	
void(&rf)(unsigned int) = foo;
auto rf2 = static_cast<void(&)(int)>(rf); 	//类型转换无效

(八)异常规约

考虑异常规约时,noexcept函数更具有局限性。non-noexcept函数指针/引用可以承接noexcept/non-noexcept函数地址;noexcept函数指针/引用只能承接noexcept函数的地址。

并且,non-noexcept函数指针/引用在承接noexcept函数地址之后仍然保留noexcept功能(noexcept函数抛出异常,默认不能被catch,只能调用terminate函数终止进程)。但是non-noexcept指针/引用虽然保存noexcept函数地址,但仍然是non-noexcept属性,不能将该指针/引用赋值给另一个noexcept函数指针/引用。

void f(void)noexcept;

void (*p)(void) = f;			//通过p调用f,如果抛异常,则不能捕获,进程会被终止
void (*p2)(void)noexcept = p;	//error,noexcept不能承接non-noexcept
void (&r)(void) = f;			//通过r调用f,如果抛异常,则不能捕获,进程会被终止
void (&r2)(void)noexcept = r;	//error,noexcept不能承接non-noexcept

void(*pr)(void)noexcept = r;	//error,noexcept不能承接non-noexcept
void(&rp)(void)noexcept = *p;	//error,noexcept不能承接non-noexcept

四、类型推导与函数

(一)decltype 推导

decltype 作用于函数名,推导得到函数类型(而不是函数指针/引用类型)。

decltype 作用于函数调用,推导得到函数返回值类型。

using func = int(void); // 给函数类型定义别名,即 func
func f;					// 声明一个 func 类型的函数 f
cout << typeid(decltype(f)).name() << endl; 	// int __cdecl(void)
cout << typeid(f()).name() << endl;         	// int

推导的函数类型会携带原函数名的异常规约,noexcept 函数指针/引用只能绑定 noexcept 函数,non-noexcept 函数指针/引用既可以绑定 non-noexcept 函数(此时按照 non-noexcept 规则可以捕获异常),也可以绑定 noexcept 函数(此时按照 noexcept 规则不能捕获异常)。

void fne(void)noexcept {} 
void fnne(void) {}

decltype(fne)* pfne = fne; 	// ok
pfne = fnne; 		// error,不兼容的异常规范

decltype(fnne)* pfnne = fnne; // ok
pfnne = fne;				  // ok

如果函数存在重载,即同一个函数名对应多个函数类型,此时无法使用 decltype 对函数名做推导,会编译报错。

void fun(void);
void fun(int);
void fun(int, double);

decltype(fun) f; // error,无法确定需要哪个重载函数

(二)auto 推导

auto 根据函数名推导的类型则自动转换为函数指针类型。

auto 推导的函数指针类型也会携带原函数名的异常规约,noexcept 函数指针/引用只能绑定 noexcept 函数,non-noexcept 函数指针/引用既可以绑定 non-noexcept 函数(此时按照 non-noexcept 规则可以捕获异常),也可以绑定 noexcept 函数(此时按照 noexcept 规则不能捕获异常)。

void foo(int x) noexcept { cout << x << endl;}
void fun(int x) { cout << x << endl;throw 3;}
auto pf = foo;	//auto对应void(*)(int)类型,且noexcept
pf = fun; 		//error,noexcept指针不能绑定到fun(non-noexcept)
auto pf2 = fun;	//auto对应void(*)(int)类型,且non-noexcept
pf2 = foo; 		//ok,non-noexcept指针可以绑定到foo(noexcept)

如果函数存在重载,即同一个函数名对应多个函数类型,此时无法使用 auto 对函数名做推导,会编译报错。

void fun(void);
void fun(int);
void fun(int, double);

auto p = fun; // error,无法确定需要哪个重载函数

(三)模板参数推导

函数指针/引用可以用来实例化函数模板

template<typename T> int compare(const T&, const T&);

int (*pf)(const int&, const int&) = compare; // int compare(const int&,const int&)
int (&rf)(const int&, const int&) = compare; // int compare(const int&,const int&)

但函数指针的重载导致模板参数不清晰时,会编译报错

void fun(int(*)(const string&, const string&));
void fun(int(*)(const int&, const int&));
fun(compare); //error,无法确定如何实例化函数模板

void foo(int(&)(const string&, const string&));
void foo(int(&)(const int&, const int&));
foo(compare); //error,无法确定如何实例化函数模板

此时需要显式指定模板实参消除歧义

fun(compare<int>);//ok,显式指定模板实参消除歧义
foo(compare<int>);//ok,显式指定模板实参消除歧义

五、函数重载

(一)概要

(1)函数重载前提是函数名字相同,但形参列表不同。与 noexcept、返回值等无关。

(2)main 函数不能重载

(3)函数指针/引用绑定到重载的函数,必须精确匹配。

struct A {
	A(int*) {}
	operator int*()const noexcept { return 3; }
};
void fun(unsigned int);
void fun(int*);
void(*p)(int) = fun;	//error,非精确匹配,需要算术转换
void(&r)(A) = fun;		//error,非精确匹配,需要类类型自定义的转换

(二)重载排序

(1)首先根据实参到形参所做的类型转换找到一组最优匹配;

(2)然后根据特例化程度选择最特化乃至非模板版本;

(3)否则,调用是有歧义的。

(三)函数重载中的类型转换问题

1、精确匹配

(1)实参类型和形参类型相同;

(2)实参从数组类型/函数名字转换成对应的指针类型;

(3)相比于形参,实参带有顶层 const 或没有顶层 const

2、底层 cv 限定符的隐式转换

cv 限定符与指针、引用类型之间构成顶层或底层的关系

int * const // 顶层 const
int & const // 顶层 const
int const * // 底层 const
int const & // 底层 const

带顶层 cv 限定符的引用、指针类型与无 cv 限定符的引用、指针类型之间的隐式转换被视为精确匹配;而带底层 cv 限定符的引用、指针类型与无 cv 限定符的引用、指针类型之间的隐式转换则是优先级次一等的类型转换。

具体内容为:

(1)非 cv 引用(指针)到底层 cv 引用(指针)是可行的隐式转换;反过来却不可行,需要 const_cast 显式转换;

    int* p = new int(24);
    const int* cp = p;   // ok,非 cv 到底层 cv 隐式转换
    int* p2 = cp; 		 // error,底层 cv 到非 cv 需要显式转换
	int* p3 = const_cast<int*>(cp); // ok,底层 cv 到非 cv 需要显式转换

(2)函数形参是引用(指针)类型,根据是否带有底层 cv 限定符可以构成重载。规则是:

带底层 cv 的形参可以匹配不带底层 cv 的实参;但不带底层 cv 的形参不可以匹配带底层 cv 的实参。

void show(volatile int* x) { cout << 1 << endl; }  
volatile int x = 10;  show(&x); // 打印 1
int y = 20;  show(&y);	// 打印 1
说明 show 可以匹配两种实参,这种两种实参都是指针类型,并且第一个是带有底层 volatile 的实参,第二个是不带有底层 volatile 的实参。
    
void disp(int* x) { cout << 2 << endl; }
volatile int x = 10;  disp(&x); // error
int y = 20;  disp(&y);	// 打印 2
说明 disp 不能匹配两种实参,这种两种实参都是指针类型,并且第一个是带有底层 volatile 的实参,第二个是不带有底层 volatile 的实参。

不带底层 cv 的实参优先匹配不带底层 cv 的形参的函数;而带底层 volatile 的实参只能匹配带底层 volatile 的形参。

void show(volatile int* x) { cout << 1 << endl; }
void show(int* x) { cout << 2 << endl; }
volatile int x = 10;  show(&x); // 打印 1,说明带底层 volatile 的实参只能匹配带底层 volatile 的形参。
int y = 20;  show(&y);	// 打印 2,说明不带底层 volatile 的实参优先匹配不带底层 volatile 的形参。

对于成员函数,同样如此,因为成员函数的第一个参数就是类实例的引用(指针),会涉及到是否带有底层 cv。

class Screen {
public:
	// const成员与非const成员重载
	// const对象只能调用const函数;非const对象有限调用非const函数
	Screen& display(std::ostream& os) {
		do_display(os);//this指针和os传入do_display,this指针非常量转常量ok
		return *this;//返回非常量引用
	}
	const Screen& display(std::ostream& os)const {
		do_display(os);//this指针和os传入do_display
		return *this;//返回常量引用
    }
private:
	void do_display(std::ostream& os)const {
		os << contents;
	}
	string contents;
};

3、通过类型提升实现的匹配

(1)小整数(bool、char、signed char、unsigned char、short、unsigned short)的值可以存储在int里就提升为int,否则unsigned int;

(2)对于较大的字符类型(wchar_t、char16_t、char32_t),会提升为int、unsigned int、long、unsigned long、long long、unsigned long long中最小的一种类型

(3)示例:

void foo(short);
void foo(int);
foo('a');	//实参char会直接提升为int,如果转short应算做算术转换

4、通过算术类型转换或指针转换实现的匹配

(1)算术转换优先级低于类型提升

void fun(unsigned int);
void fun(double);
void fun(int);
//char->int,类型提升
//char->unsigned int,算术转换
//char->double,算术转换
fun('a');

(2)指针的转换包括:

常量整数0或字面值nullptr能转换成任意类型的指针值,都表示空指针;

指向任意non-const的指针都能转换为void*;

指向任意对象的指针都能转换为const void*;

派生类指针可以隐式转换为基类指针;

5、通过类类型转换实现的匹配(转换构造、重载类型转换运算符)

(1)自定义的类类型转换很容易产生二义性问题,在同时包含运算符重载的类内,也会因为自定义类类型转换而产生函数重载匹配时的二义性问题:

struct A {
	A() = default;
	A(int) {}
	operator int()const noexcept { return 3; }
	A operator+(const A& other) { return A(3); }
};
A a1, a2;
A a3 = a1 + a2;//operator+
A a4 = a1 + 5;//二义性错误:A转int后,执行int+int;5转A后,执行A+A

(2)实参通过自定义的类类型转换可以同时满足多个重载函数时会产生二义性错误:

struct A { A(int) {} };
struct B { B(int) {} };
struct C { C(double) {} };
void fun(const A&);
void fun(const B&);
void fun(const C&)
fun(3);//二义性错误:int->A/B;int->double->C
fun(3.14);//二义性错误:double->int->A/B;double->C

(3)自定义多个内置类型与类类型间的转换,而内置类型之间存在隐式转换,将产生二义性错误:

struct A { 
	A() = default;
	A(int);
	A(double);
	operator int()const noexcept;
	operator double()const noexcept;
};
void fun(long) {}

A a1(3.14);//ok,精确匹配
A a2(3);//ok,精确匹配
A a3(3L);//error,long->int与long->double平级,构造函数重载的二义性
A a;
fun(a);	//error,int->long与double->long平级
		//所以operator int与operator double重载二义性

(4)自定义两个类类型之间的转换,同一个方向上只能有一个转换函数,否则重载二义性

struct A { 
	A(const B&);
};
struct B {
	operator A()const noexcept;
};
void fun(A a) {}
B b;
fun(b); //B->A方向上有两个转换函数

(四)函数重载中的特例化排序

1、考虑特例化排序之前,需要注意是否有类型转换,因为类型转换优先级更高

template<typename T>  void fun(T&);
template<typename T>  void fun(const T*);
void fun(const string&);
    	char str[] = "hello";
fun(str);//调用fun(T&)版本
/* 解析:
对于fun(const T*),T被绑定到char类型,实参到形参转换过程是:
		char[6]->const char*
对于fun(const string&),非模板函数,最为特例化,但参数存在类型转换:
			char[6]->const string临时量->const string&
		对于fun(T&),最为通用的版本,但不存在类型转换,T直接被绑定到char[6]
	按照特例化排序是:
		fun(const string&)
		fun(const T*)
		fun(T&)
	但是类型转换优先级更高,重新按照类型转换排序是(即调用优先的排序):
		fun(T&)//无类型转换
		fun(const T*)//non-const到const
		fun(const string&)//string类通过转换构造定义的类型转换
	*/
template<typename T>  void fun(T&);
template<typename T>  void fun(T*);
    	char str[] = "hello";
fun(str);//调用fun(T*)版本
/* 解析:
		对于fun(T*),虽然存在类型转换,char[6]->char*。但是该转换算作【精确匹配】
		对于fun(T&),同样是精确匹配
但T*更为特例化,T*只能接收指针类型的参数,T&可以接收指针/非指针参数
	*/

2、默认swap函数是模板函数,自定义版本将比标准库版本更特例化

std名字空间中实现了模板版本的通用swap函数,但往往需要自定义一个重载版本。因为可能不知道被swap的对象是否确定有重载的swap版本,所以在调用swap函数时,一个好的模式应该是:

using std::swap;
swap(obj1, obj2);//如果有重载版本,将调用更特例化的

实现某个类类型重载的swap函数:

struct A {
	Mem m;
	friend void swap(A&, A&);
};
inline void swap(A& lhs, A& rhs){
	using std::swap;
	swap(lhs.m, rhs.m);//如果有重载,调用更特例化的
}

六、lambda 表达式

lambda 表达式会被编译器翻译为未命名类的未命名对象,该类重载了 operator(),所以 lambda 表达式就是一个仿函数对象。

(一)形式

[ capture ] (params) opt -> ret { body; }; 

capture:捕获列表,可以认为是未命名类的构造函数的参数列表
params:参数列表,可以认为是未命名类operator()函数的参数列表
opt:函数选项,修饰operator()函数,主要是 mutable 和 noexcept
ret:operator()函数的返回值类型,必须采用尾置返回的形式
body:operator()函数的函数体

参数列表、opt 选项和返回值类型可以省略,但捕获列表和函数体不能省略。省略参数列表意味着参数是 void,省略返回值则需要根据函数体推断返回值类型。

auto f = [] {return 10; };
cout << f() << endl;  // 打印 10

/* 编译器生成的内部表示 */
class __lambda_10_10{
public: 
    inline int operator()() const
    {
      return 10;
    }
    
    using retType_10_10 = auto (*)() -> int;
    inline operator retType_10_10 () const noexcept
    {
      return __invoke;
    };
    
private: 
    static inline int __invoke()
    {
      return 10;
    }
    
public: 
    // inline /*constexpr */ __lambda_10_10(__lambda_10_10 &&) noexcept = default;
    
};
__lambda_10_10 f = __lambda_10_10(__lambda_10_10{});

如果有 opt 选项,则参数列表也不能省略(即使为空)

auto f = []noexcept {return 10; }; // error,参数列表不能省略
auto f = []()noexcept {return 10; }; // ok
cout << f() << endl;  // 打印 10

(二)捕获列表

1、捕获列表的原理

lambda 表达式会生成匿名类的匿名对象,捕获列表是该匿名类的构造函数的参数列表,捕获的对象也就是给匿名类的构造函数传递的实参。

int x = 10;
auto f = [x] {return x; }; // 捕获变量 x

/* 编译器生成的内部表示为 */
int x = 10;
class __lambda_11_10{
public: 
	inline int operator()() const{
		return x;
	}
private: 
	int x;
public: 
	// inline /*constexpr */ __lambda_11_10(__lambda_11_10 &&) noexcept = default;
	__lambda_11_10(int & _x) : x{_x} {} // 构造函数
};
__lambda_11_10 f = __lambda_11_10(__lambda_11_10{x});

显然,局部静态变量、全局变量是不需要捕获就可以使用的。

static int s = 10;
auto f = []() noexcept { cout << s << endl; };
f();	// 打印 10

/*
s 是静态局部变量,不需要捕获即可使用
cout 是全局变量,不需要捕获即可使用
*/

2、捕获列表的使用

[]		不捕获任何变量。 
[&]		捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。 
[=]		捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。 
[=,&foo]	按值捕获外部作用域中所有变量,并按引用捕获foo变量。 
[bar]		按值捕获bar变量,同时不捕获其他变量。 
[this]		捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

/* 示例 */
struct A {
    int x{ 10 };
    void fun(int y) {
        cout << [&]() {return this->x * y; }() << endl;
        // 以引用的方式捕获外部作用域中的所有变量
	        // 因为成员函数默认接收 this 指针作为实参,所以可以捕获 this
    	    // 因为形参 y 是 fun 作用域中的局部变量,所以也可以被捕获
    }
	static void foo(int y) {
        cout << [&]() {return this->x * y; }() << endl; // error
        // 因为静态函数不接收this指针作为实参,所以lambda无法捕获this指针,所以编译报错
    }
};
/* 示例 */
int x = 10;
double y = 3.1400000000000001;
auto f = [x, &y] {return x * y; };

class __lambda_12_14{
public: 
inline double operator()() const{
      return static_cast<double>(x) * y;
}
private: 
    int x;			// 按值捕获时,匿名类中用值成员承接捕获量
    double & y;		// 按引用捕获时,匿名类中用引用成员承接捕获量
public: 
    // inline /*constexpr */ __lambda_12_14(__lambda_12_14 &&) noexcept = default;
    __lambda_12_14(int & _x, double & _y) : x{_x}, y{_y}{}
};
__lambda_12_14 f = __lambda_12_14(__lambda_12_14{x, y});

3、显式捕获与隐式捕获

捕获列表如果不指定捕获哪个具体变量,就是隐式捕获,反之是显式捕获。

[=]		// 隐式按值捕获
[&]		// 隐式按引用捕获
[x,y]	// 显式按值捕获
[&x,&y] // 显式按引用捕获

如果要混合使用隐式捕获和显式捕获时,捕获列表的第一个元素必须是隐式捕获,且显式捕获的方式必须和隐式捕获方式不同。

[=,&x,&y]	// 混合使用,优先列出隐式捕获,后续的显式捕获的捕获方式必须与隐式捕获方式不同
[&,x,y] 	// 混合使用,优先列出隐式捕获,后续的显式捕获的捕获方式必须与隐式捕获方式不同

那么隐式捕获在匿名类中是什么情形呢?

隐式捕获不会一上来就在 lambda 匿名类内部生成所在作用域内所有局部非静态变量对应的成员,而是在 lambda 函数体内用到的时候才生成对应的成员。

int x = 10;
double y = 3.14;
[=]{ cout<<x<<endl;}();

/* 编译器生成的内部表示为 */
class __lambda_12_2{
public: 
	inline void operator()() const{
        std::cout.operator<<(x).operator<<(std::endl);
    }
private: 
    int x;
public:
    __lambda_12_2(int & _x): x{_x}{} // 虽然是隐式捕获,但只用到了 x,所以只捕获 x
} __lambda_12_2{x};

4、mutable

lambda 生成的匿名类,重载 operator() 函数,该函数默认是 const 函数。所以按值捕获的变量在 lambda 函数体内部虽然与原变量没有任何关系,但仍然无法被改动。

int x = 10;
double y = 3.14;
[=]{ x=20; }();		// error,operator() 是const函数,无法改变类成员

/* 编译器生成的内部表示为 */
class __lambda_12_2{
public: 
	inline void operator()() const{...} // 此函数无法改动类成员 x
private: 
    int x;
public:
    __lambda_12_2(int & _x): x{_x}{} 
} __lambda_12_2{x};

而 lambda 表示式可以通过使用 mutable 选项删掉 operator() 函数的 const 属性

int x = 10;
double y = 3.14;
[=]()mutable{ x=20; }(); 	

/* 编译器生成的内部表示为 */
class __lambda_13_1{
public: 
	inline /*constexpr */ void operator()(){  // const 属性被去掉了
        x = 20;
    }
private: 
	int x;
public:
	__lambda_13_1(int & _x): x{_x} {}
} __lambda_13_1{x};
__lambda_13_1.operator()();

当然,如果按引用捕获,不需要加 mutable 也能改变捕获量,这是 const 成员函数的特性。

int x = 10;
double y = 3.14;
[&](){ x=20; }();

/* 编译器生成的内部表示为 */
class __lambda_13_1{
public: 
	inline /*constexpr */ void operator()() const{	// const 函数可以改变引用类型成员变量的值
		x = 20;
	}
private: 
	int & x;
public:
    __lambda_13_1(int & _x): x{_x}{}
} __lambda_13_1{x};
__lambda_13_1.operator()();

5、与函数指针之间的转换

如果 lambda 表达式捕获列表是空(没有任何显式捕获和隐式捕获),那么生成的匿名类会重载类型转换运算符,转换到函数指针类型。所以,lambda 在捕获列表为空的情况下,可以转换为函数指针。

using PFunc = int(*)(void);
PFunc f = []() {return 1; }; //调用重载的类型转换函数
f();

/* 编译器生成的内部表示为 */
class __lambda_12_11{
public: 
    inline /*constexpr */ int operator()() const{
        return 1;
    }
	using retType_12_11 = int (*)();
    inline /*constexpr */ operator retType_12_11 () const noexcept{
        return __invoke;
    }
private: 
    static inline int __invoke() {
        return 1;
    }
} __lambda_12_11{};

using FuncPtr_12 = PFunc;
FuncPtr_12 f = static_cast<int (*)()>(__lambda_12_11.operator __lambda_12_11::retType_12_11());
f();

七、函数适配器

(一)std::bind

头文件 <functional>

std::bind 接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表:

auto new_callable = std::bind(old_callable, arg_list);

当调用 new_callable 时,new_callable 会调用 old_callable,并传递给它 arg_list 中的参数。

void show(int x) { cout << x << endl; }
auto disp = std::bind(show, 10);
disp();

arg_list 中的参数可以是占位符,占位符的序号代表 new_callable 的第几个参数。

using namespace std::placeholders;
auto new_callable = std::bind(old_callable,_2, _1);
new_callable(_1, _2); // 相当于 old_callable(_2, _1);

/* 示例 */
void show(int x,double d) { cout << x << ':' << d << endl; }
using namespace std::placeholders;
auto disp = std::bind(show, _2, _1);
disp(3.14, 53); // 打印 53:3.14

arg_list 中非占位符参数默认是按值传递,如果希望按引用传递,需要使用标准库函数:

std::ref(arg);		//获取arg的引用
std::cref(arg);		//获取arg的const引用

/* 示例 */
std::ostream& print(std::ostream& os, const string& s, char c) { 
    return os << s << c;
}
auto newCallable = std::bind(print, std::ref(cout), "hello", '\n'); // cout不能按值传递
newCallable();		// 打印 hello

std::bind 之间可以互相组合与嵌套来实现更强大的功能

auto f = std::bind(std::logical_and<bool>(), 
                   std::bind(std::greater<int>(), _1, 5), 
                   std::bind(std::less<int>(), _1, 10) );
int count=std::count_if(vec.begin(), vec.end(), f); // 查找集合中大于 5 小于 10 的元素的个数

(二)std::function

std::function 是模板类,接受一个模板参数,该模板参数是可调用对象的调用形式。所以可以用来做可调用对象的包装器,可以容纳所有可调用对象,甚至经过处理也可以容纳成员函数指针。

std::function 的转换构造允许隐式转换,可以直接将可调用对象赋值/初始化给 std::function 的实例

std::function<T> f = Callable;
std::function<T> f(Callable);

可以用空指针(nullptr)赋值/初始化 std::function 实例;

std::function<T> f(nullptr);
std::function<T> f = nullptr;

std::function 可以拷贝构造。

std::function<T> f;//默认构造
std::function<T> f(f2);//拷贝构造

std::function 定义了向 bool 类型的转换,如果内含可调用对象,返回 true,否则 false

int fun(int){...}
std::function<int(int)> f = fun;
if (f) { f(3); }

std::function 模板类内部定义了一些有用的类型:

std::function<int(int)>::result_type   x;		//可调用对象返回类型
std::function<int(int)>::argument_type y;		//可调用对象只有一个参数时参数的类型
std::function<int(int, int)>::first_argument_type z1;	//可调用对象有两个参数时,第一个参数的类型
std::function<int(int, int)>::second_argument_type z2;	//可调用对象有两个参数时,第二个参数的类型

存在重载的函数不能直接赋值给 std::function 模板类

void fun(int);
void fun(string);
std::function<void(int)> f = fun; // error
// 虽然调用形式明确,但仍然不能将存在重载的函数赋值给 std::function,因为重载的函数的参数会被添加额外信息,与 std::function 的模板参数 void(int) 是不一样的

// 但是此时还可以通过函数指针间接传递给 std::function
void (*pf)(int) = fun;
std::function<void(int)> f = pf;

// 或者直接使用 lambda 表达式避免函数重载
std::function<void(int)> f = [](int) {...}

(三)将成员函数指针转换为可调用对象

1、使用 std::function

std::function 是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。

vector<string> vs = { "hello","world","", "member function" };
bool (string::*fp)(void)const = &string::empty;		//成员函数指针
auto aim = find_if(vs.begin(), vs.end(), fp);		//error
// 因为成员函数指针调用的时候需要有类实例,但 find_if 的调用方式为 fp(*it),这无法调用成员函数指针

// 这时候就需要 std::function 来解决问题
function<bool(const string&)> fcnr=&string::empty;	// 需要把隐式传递的this显式列在参数表中
auto aim = find_if(vs.begin(), vs.end(), fcnr);	//相当于调用 fcnr(*it),it是迭代器,逐个处理每个元素

// 需要注意,形参列表不能为指针,这与 this 无关,而是受限于 find_if 调用形式的结果
function<bool(const string*)> fcnp=&string::empty;	// 需要把隐式传递的this显式列在参数表中
cout << fcnp(&vs[0]) << endl; // 打印 0,说明形参 const string* 是可以传递this的
auto aim = find_if(vs.begin(), vs.end(), fcnp);	//error,fcnp(*it),实参是类实例,而形参是实例指针

// 由此可知,单纯使用std::function需要明确算法(此例是find_if)如何传参给可调用对象
// 而使用标准库中的std::mem_fn则不用考虑参数是传引用还是传指针

2、使用 std::mem_fn

可调用对象中,成员函数指针与其他相比,调用方式不一致,但可以通过 std::mem_fn 转换成一致的调用方式。

bool (string::*fp)(void)const = &string::empty;//成员函数指针
auto c = std::mem_fn(fp);  // 将成员函数指针转换成一致的调用方式
//或者
auto c = std::mem_fn(&string::empty);
auto aim = find_if(vs.begin(), vs.end(), c); // 相当于调用 c(*it),it是迭代器,逐个处理每个元素

3、使用 std::bind

std::bind 也可以达到与 std::mem_fn 相同的效果,转换成的可调用对象既可以接收指针类型参数,也可以接收引用类型参数。

using namespace std::placeholders;
vector<string> vs = { "hello","world","", "member function" };
bool (string::*fp)(void)const = &string::empty;//成员函数指针
auto callable = std::bind(fp, _1);
//或者
auto callable = std::bind(&string::empty, _1);
callable(*vs.begin());	//ok,按引用传参
callable(&vs[0]);		//ok,按指针传参
auto aim = find_if(vs.begin(), vs.end(), std::bind(&string::empty, _1));

4、std::functionstd::bind 的组合使用示例

struct A {
	void fun(int);
	int x{ 10 };
};
using namespace std::placeholders;
void (A:: * pm)(int) = &A::fun;
A a;
std::function<void(int)> f = std::bind(pm, &a, _1);
std::function<int& (void)> f2 = std::bind(&A::x, &a);
f2() = 20;
cout << a.x << endl;	//20

标签:std,const,函数,int,void,c++,callable,type,noexcept
来源: https://www.cnblogs.com/BohanHwang/p/16187503.html