%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% # 可调用对象 可调用对象(callable object):可直接对其使用调用运算符`()` 的对象或表达式,C++中有以下五类: - **函数** - **函数指针**: 指向函数的指针 - **函数对象 / 仿函数**:即重载了调用运算符 `operator()` 的**类**; - **`bind` 创建的函数对象**: 通过`std::bind` 创建的函数对象 - **`std::function<>` 实例对象** :可存储任何可调用对象,本身支持被直接调用。 - **lambda 表达式**(严格来说,属于一种函数对象) > [!NOTE] **==类成员函数指针==本身不属于可调用对象**,不能直接调用,必须**通过一个实例对象**来进行调用 > > 说明示例: > ```cpp > struct MyClass { > void memfunc(); > }; > > MyClass obj; > // `func_ptr`为成员函数指针 > void (MyClass::*func_ptr)() = &MyClass::memfunc; > // 需要通过对象来调用成员函数指针 > (obj.*funcptr)(); // 调用obj.memfunc; > ``` > > 同样原因,类成员函数指针也不能直接传给 `std::funtion` 包装, > **必须通过 `std::bind()` 或者 `lambda` 为其绑定一个类实例**,先得到一个可调用对象,再赋给 `std::function`。 <br><br><br> # 函数对象(仿函数) **函数对象**:行为类似函数的"对象",也称**仿函数**(**Functor**)。 函数对象是由一个提供了 `operator()` 操作符的**类**实例化得到的对象,具有**类似函数的行为**。 函数对象的优点: - 函数对象可拥有**成员变量**和**成员函数**,因此函数对象**可拥有状态(state)**。<br>在同一时间点,相同类型的两个不同的函数对象所表述的相同机能 (same functionality),可具备不同的状态。 - **每个函数对象有其自己的类型**。两个函数对象即使函数签名式相同,也可以有不同的类型。 ```cpp // 示例: 通过函数对象实现"为集合内每个元素增加一个指定值" class AddValue { private: int value; // the value to add public: AddValue(int v) : value(v) {} // 函数调用 void operator()(int &elem) const { elem += value; } } int main() { list<int> coll = {1, 3, 4, 2, 5}; // add value 10 to each element: for_each(coll.begin(), coll.end(), AddValue(10)); // add value 17 to each element: for_each(coll.begin(), coll.end(), AddValue(17)); // add value 3 to each element: for_each(coll.begin(), coll.end(), AddValue(3)); } ``` <br><br><br> # function 对象 > 位于 `<functional>` 头文件中 C++11 提供了 `std::function<>` **类模版**,实例化得到的 **function 对象**可作为 C++中**任意可调用对象**的 **==包装器==**(Function Type Wrapper), 其可**存储、复制和调用** 指定类型的任意可调用对象。 ## 用法说明 `std::function<T>` 的 **模版参数 `T`** 是其所能接收的**可调用对象**的 **==函数类型==**, 例如 `std::function<int(int, int)>` 表示一个接受两个 `int` 参数并返回一个 `int` 的可调用对象。 ![[02-开发笔记/01-cpp/类型相关/cpp-指针相关#^03j3v1]] ## 使用示例 常见使用场景: - 使用 `std::function<>` 对象**包装其它任意可调用对象**。 - 使用 `std::function<>` 对象包装 "**==成员函数指针==**",参见 [[02-开发笔记/01-cpp/类型相关/cpp-指针相关#方式一:使用function为成员函数指针直接生成一个可调用对象|cpp-指针相关#成员函数指针]] - 将 `std::function<>` 模版类作为**模版参数**,使**STL 容器可存储相同函数类型的任意函数**。 - 某个函数返回一个 lamba 时,作为函数的返回类型 示例一:使用 funciont 对象包装可调用对象:函数、函数指针、函数对象、lambda、bind 对象 ```cpp #include <functional> #include <iostream> // 普通函数 int Add(int x, int y) { return x + y; } // 函数对象; 仿函数 struct Adder { int operator() (int x, int y) const { return x + y; } }; // 类,用于演示成员函数指针 class MyClass { public: void memberFunc(int x, int y) { return x + y; } }; int main() { // 声明一个返回值为int, 接受两个int型参数的可调用对象类型,得到一个模版类。 std::function<int(int, int)> Func; // 可以被赋予普通函数 Func = Add; // 可以被赋予lambda Func = [](int x, int y) { return x+y; }; // 可以被赋予一个函数对象; Func = Adder(); // 可以被赋予函数指针 int (*fp)(int, int) = Add; Func = fp; // 可以被赋予bind创建的可调用对象 Func = std::bind(Add, 5, std::placeholders::_1); // 包装"类成员函数指针", 从而可供任何该类的实例调用 (第一参数需要声明为对该类实例的引用) std::function<void(const MyClass&, int, int)> MemFunc; MemFunc = &MyClass::memberFunc; MyClass obj; MemFunc(obj, 42, 77); } ``` 示例二:声明 vector 中的元素类型为 `std::function<>` 类型,从而**存储可调用对象**。 ```cpp void func (int x, int y); // 声明vector中的元素类型为function<void(int,int)>类型 vector<function<void(int,int)>> tasks; // 向其中添加"函数类型"相同的不同函数 tasks.push_back(func); tasks.push_back([](int x, int y) { ... }); // 调用各个函数 for (std::function<void(int, int) f : tasks) { f(69, 69); } ``` 示例三:作为函数返回类型,接收返回的 lambda 表达式 ```cpp function<int(int, int)> returnLambda() { return [](int x, int y) -> int { return x + y;}; } int main() { auto lf = returnLambda(); lf(6, 7); } ``` <br><br> ## function 的性能 `std::function` 会比简单的函数指针或直接的函数调用**有==更多开销==**。 - function 类型实例本身的大小通常是**16~32 个字节**(取决于编译器具体实现和平台),通常包括: - 一个**指向其可调用对象的指针** - 一个或多个函数指针,用于管理如何调用、复制和销毁其内部的可调用对象。 - 一个小的缓冲区,用于存储小的可调用对象。 - function 通常使用 **动态内存** 来 **存储** 其可调用对象,尤其是当这些对象超过某个大小阈值时。 - 在大多数实现上,也提供了一个称为 "small object optimization" 或 "small buffer optimization (**SBO**)" 的特性,即**对于小的可调用对象**(如小的 lambda 表达式或函数指针),function 类将直接在内部存储,而不会申请动态内存。 可通过 `sizeof` 查看 `std::function` 在当前平台和编译器上的确切大小: ```c++ #include <iostream> #include <functional> int main() { std::cout << "Size of std::functional<void()>: " << sizeof(std::function<void()>) << " bytes" << std::endl; } // 我的机器上是32字节. ``` 因此,**function 包装不适用于作为递归函数**,性能会非常低! <br><br><br> # 函数参数绑定—bind 函数 > `bind` 函数以及 `placeholder` 命名空间位于头文件 `<functional>` 中 `bind` 函数用于 "**==绑定==一个或多个参数**" 到一个可调用对象,**==生成一个新的可调用对象==**,**适配原对象的参数列表**[^1] 。 ## 用法说明 ```cpp auto newCallable = std::bind(callable, arg_list); ``` `bind` 函数返回一个可调用对象 `newCallable`,**当调用 `newCallable` 时,等价于使用参数列表 `arg_list` 来调用`callable`** 。 **`arg_list` 对应于 `callable` 参数列表**,其中可指定**两种类型的参数**: - (1)**==绑定==到 `newCallable` 的参数**:直接给出,参数默认以 "**==拷贝/副本==**" 的方式进行绑定。 - 若要以 "**==引用==**" 或 "**==const 引用==**" 形式传递,需要使用**标准库函数 `std::ref()` 与 `std::cref()`** 包裹。 - (2)**==调用== `newCallable` 时需要传递的参数**:以 "**==占位符==**" 形式声明:`std::placeholders::_n`; - 数值 `n` 表示**传递给 `newCallable` 的参数位置**, `_1` 代表传递给 `newCallable` 的第一个参数,`_2` 代表传递的第二个参数,以此类推。 > [!note] 关于 bind 生成的可调用对象中的 "绑定的参数" 与 "接收的参数" [^2] > > - bind 对象 **==存储==着传递给 `std::bind` 的 ==被绑定的实参== 的"副本"**(默认按值传递时) > - bind 对象 **==接收==的所有实参** 都是通过 "**==引用传递==**" > - (因为 **bind 对象的 `operator()` 是以 "完美转发" 实现,故形参为 `T&&` 转发引用类型**) > ![[02-开发笔记/01-cpp/类型相关/cpp-指针相关#^03j3v1]] #### 绑定到 "类的非静态成员函数" 时的用法 > [!NOTE] > > 当`callable` 是 **指向==非静态成员函数==的指针** 时, `arg_list` 中首项参数必须是一个指向 **实例对象的引用或指针**(包括智能指针,如 `std::shared_ptr` 和 `std::unique_ptr`),剩余参数则对应该成员函数的参数列表。 > > ```cpp > MyClass Obj; > auto func = std::bind(&MyClass::mem_func, &obj, param1, param2); > ``` > <br><br> ## 使用示例 使用场景:参数的重新排列、固定化(固定某些参数, 而让调用时可传递的参数减少)以及柯里化。 示例一:**参数固定化** ```cpp int add(int a, int b) { return a + b; } // 通过bind生成一个可调用对象add_five, 固定其调用add时首个参数为5, 第二个参数为传递给其的第一个参数; auto add_five = bind(add, 5, std::placeholders::_1); int result = add_five(3); // result为8 (5+3) ``` 示例二:**参数重新排序** ```cpp int divide(int a, int b) { return a / b; } auto inv_divide = std::bind(divide, std::placeholders::_2, std::placeholders::_1); int result = inv_divide(2, 10); // result为5(10/2) ``` 示例三:以 "**引用**" 的方式绑定参数:`ref`,`cref` ```cpp void increment(int &x) { ++x; } int add(int a, int b) { return a + b; } int main(void) { int value = 0; auto bi_incre = std::bind(increment, std::ref(value));// 通过ref()以"引用"的形式绑定参数; bi_incre(); // 调用后, value的值变为1; // std::cref 用于创建一个类型为 std::reference_wrapper<const T> 的对象 // 其作用是保持对给定对象的常量引用。 int value2 = 55; auto add_by_value = std::bind(add, std::cref(value), std::placeholders::_1); int res = add_by_value(3); // res为58 (55+3); value2 = 11; res = add_by_value(6); // res为17 (11+6); } ``` 示例四:**绑定成员函数** ```cpp struct Multiplier { int factor; int multiply(int x) { return x * factor; } }; // 绑定成员函数 // std::bind()的第一项参数接受成员函数指针 // 第二项参数接受一个实例对象的引用或指针, 剩余参数为成员函数的参数列表. Multiplier multiplier{5}; auto bound_multiply = std::bind(&Multiplier::multiply, &multiplier, std::placeholders::_1); int result = bound_multiply(3); // result 为 15 (3 * 5) ``` 示例五:**为各种可调用对象绑定参数** ```cpp void funct (int x, int y); auto l = [](int x, int y) { ... }; class C { public: void operator() (int x, int y) const; void memfunc(int x, int y) const; }; int main() { C c; std::shared_ptr<C> sp = std::make_shared<C>(); // bind() uses callable objects to bind arguments: std::bind(func, 77, 33)(); // call: func(77,33) std::bind(l, 77, 33)(); // call: l(77,33) std::bind(C(), 77, 33)(); // call: C::operator()(77,33) std::bind(&C::memfunc, c, 77, 33)(); // call: c.memfunc(77,33) std::bind(&C::memfunc, sp, 77, 33)(); // call: sp->memfunc(77,33) } ``` <br><br><br> # Reference Wrapper 引用包装器 > 位于 `<functional>` 头文件中 C++11 中引入的 `std::reference_wrapper<>` 类模版,用作为 **引用包装器**。 对于给定类型 `T`,`std::reference_wrapper<T>` 类提供了: - `std::ref()` 用以隐式转换为 `T&` - `std::cref()` 用以隐式转换为 `const T&` 上述两个函数将返回**一个`std::reference_wrapper<T>` 实例对象**。 要得到被引用的原对象,可通过该对象的 **`.get()` 方法**获取,**返回一个被包装对象的引用**。 ### 使用示例 使用场景: 1. 用于为以`by value`方式接受参数的**函数模版**提供 reference 类型,即让**函数模版能够接受"引用"类型**,而无需进行模版特化。 - 该这个特性被 C++标准库运用于各个地方,例如,基于该特性可实现: - `make_pair()` 创建一个` pair<> of references` - `make_tuple()` 能够创建一个 `<tuple> of references` - `Binder` 能够绑定 `(bind) reference` - `Thread` 能够以 `by reference` 形式传递实参 2. 作为**对象包装器**,用以**在 STL 容器中存储引用**。 > [!caution] STL 标准容器(如 `std::vector`,`std::list` 等)**不能"直接"存储引用类型**。 > > 因为**引用不是对象**,**没有实际的存储大小**,而是别名或已存在对象的另一个名称。试图创建例如`std::vector<int&>`的容器会导致编译错误。 使用示例一: ```cpp template <typename T> void foo(T val); // 模版参数推导中, 对于实参int&, 推导结果为T是int类型, 会忽略引用. // 对于上述函数模版, 可以直接如下传递引用参数: int x; foo(std::ref(x)); // T变成int& foo(std::cref(x)); // T变成const int& ``` 使用示例二: ```cpp // 通过reference_wrapper声明容器的元素类型为引用 // 搭配ref()或cref()向容器中传入对象的引用. // vector<int&> vec; // Error vector<reference_wrapper<int>> vec; // OK int val = 3; // ref()返回一个std::reference_wrapper<int>实例对象, 包装了被引用的原对象 vec.push_back(ref(val)); // vec中的引用也将看到更改 val = 4; cout << vec[0].get() << endl; // 通过.get()获取一个被包装对象的引用 cout << static_cast<int&>(vec[0]) << endl; // 与上语句等价 ``` <br><br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《C++ Primer》(P354) [^2]: 《Effective Morder C++》Item34