『 C++11 』模板可变参数包,Lambda表达式与 function 包装器

文章目录


模板可变参数模板

可变参数模板是C++11引入的一个特性,允许模板接收任意数量的参数;

该特性增加了C++的泛型编程能力;

可变参数模板引入可使用...来表示模板参数包;

可在模板参数和函数参数中使用;

cpp 复制代码
template<class ...Args>
void func(Args ...args)

其中class ...Args表示其可变参数包的类型名为Args;

func()函数中的Args ...args表示传入一个该可变参数包;

cpp 复制代码
template <class ...Args>
void func1(Args ...args){
 // 代码
}

int main() {
  func1(1, "666", 'a', 2.2);
  return 0;
}
  • 可变参数包的参数类型个数

    可通过sizeof()来查看当前传入的可变参数包的参数类型个数;

    cpp 复制代码
    template <class... Args>
    void func1(Args... args) {
      cout << sizeof...(args) << endl;
    }
    
    int main() {
      func1(1, "666", 'a', 2.2);
      return 0;
    }

    通常在使用sizeof()对可变参数包的参数个数进行查看时语法规定必须使用sizeof...(可变参数包)进行查看;

    运行结果为:

    bash 复制代码
    $ ./test 
    4

可变参数包的展开

可变参数包通常使用...展开参数包,如:

cpp 复制代码
func(args...);

当编译器遇到args...时会将参数包中的每个参数都展开为独立的参数;

假设传入func()函数的参数为'c',10,2.2时,在函数func中将优先获取第一个参数c;

其参数展开的顺序按照它们在参数包中的顺序;

通常展开参数包的方法以递归,或是利用初始化数组使其进行展开;

  • 递归方式展开

    cpp 复制代码
    void _ShowList() { cout << endl; }
    
    template <class T, class... Args>
    void _ShowList(const T& t1, Args... args) {
      cout << typeid(t1).name() << " : " << t1 << endl; // 打印参数类型与参数值
      _ShowList(args...);
    }
    
    template <class... Args>
    void ShowList(Args... args) {
      _ShowList(args...);
    }
    
    int main() {
      ShowList(1, "666", 'a', 2.2);
      return 0;
    }

    在这个例子中ShowList()函数是一个使用模板可变参数包的函数模板;

    _ShowList()函数则是该函数的子函数,该函数将一个可变参数包衍生为一个T类型的模板参数与一个可变参数包class ...Args,并且该函数存在一个返回值与参数都为空的函数;

    调用ShowList()函数并传入1, "666", 'a', 2.2为参数;

    根据可变参数包的展开原理,_ShowList()中的const T&类型将优先获取可变参数包中的第一个参数,并将剩余的可变参数包以递归的形式调用逐个进行展开,当可变参数包中所有参数被展开完毕后将对应的调用无参的_ShowList()函数重载;

    运行结果为:

    bash 复制代码
    $ ./test 
    i : 1
    PKc : 666
    c : a
    d : 2.2
  • 初始化数组形式展开

    cpp 复制代码
    template <class T>
    int _Analysis(T t) {
      cout << t << endl;
      return 0;
    }
    
    template <class... Args>
    void Analysis(Args... args) {
      int arr[] = {_Analysis(args)...};
    }
    
    int main() {
      Analysis(1, "666", 'a', 2.2);
      return 0;
    }

    在这个例子中定义了一个可变模板参数包函数模板Analysis(),并定义了一个模板参数类型为T的子函数模板_Analysis();

    传入参数并调用Analysis()函数,其将接收到这个可变参数包,并将其对数组进行初始化;

    此处的初始化采用列表初始化对数组进行初始化,在列表初始化中将根据可变参数包的数据个数依次调用_Analysis()函数并传入对应的可变参数包,最终实现可变参数包的展开;

    对应其展开时将会把int arr[] = {_Analysis(args)...}展开为:

    cpp 复制代码
    int arr[] = {_Analysis(1), _Analysis("666"), _Analysis('a'), _Analysis(2.2)};

    运行结果为:

    bash 复制代码
    $ ./test 
    1
    666
    a
    2.2

可变参数包与STL容器中的emplace函数关系

在 C++11 中对容器更新了对应的emplace版本的函数;

std::vector<>std::list<>中的emplace_back:

cpp 复制代码
// vector
template <class... Args>  void emplace_back (Args&&... args);

// list
template <class... Args>  void emplace_back (Args&&... args);

其中可变参数包允许函数接收任意数量和类型的参数,使得emplace_back可以直接接收构造新元素所需的所有参数;

  • 万能引用

    其中函数调用中使用了万能引用Args&&... args使得函数既可以接收左值夜可以接收右值;

其中该函数的实现依靠了完美转发,使得其可以通过std::forward<Args>(args)...保持可变参数包原有的属性将其转发至真正构造的部分,即直接在分配的内存上构造函数从而不需要再使用移动或拷贝构造,而是直接构造;

该函数的实现对于传递右值时与普通插入函数没有太大的差异,对于右值而言普通插入函数可通过移动构造的方式,即 构造+移动 ;

移动语义的开销本身就不是特别大,故emplace版本插入函数与传右值时的普通插入函数效率大差不差,但在使用普通插入函数传递左值进行构造时无法进行移动语义,如移动构造或是移动赋值,此时必将调用对应的拷贝构造函数进行深拷贝;

emplace版本插入函数可通过直接传递其可变参数包使得直接将参数在分配的内存空间上直接构造;

从而避免了拷贝构造的开销;


Lambda 表达式

lambda表达式是C++ 11 引入的一个新的特性,允许创建匿名函数对象;

Lambda表达式提供一种较为简洁的方式定义行内函数,通常适用于需要短小函数的场景,如算法库中的回调函数;

cpp 复制代码
[capture](parameters) mutable -> return_type { body }
  • [capture]捕获列表

    捕获列表定义了Lambda表达式可以访问的外部作用域中的变量,其中该外部作用域指的是Lambda表达式外部的作用域;

    其语法为:

    • []

      表示不捕获任何外部变量;

    • [=]

      表示以传值的方式捕获所有外部变量;

      通常以传值的方式捕获的变量在Lambda表达式中不允许被修改(与mutable有关,mutable为可选项,不使用mutableLambda中的参数带const属性);

      当该Lambda为类内成员函数中的行内函数时,该方式可以传值的方式获取该类的this指针(表示可直接以传值的方式访问该类内成员);

    • [&]

      表示以引用方式捕获所有外部变量;

      通常以传引用的方式捕获的变量可在Lambda表达式中被修改,且与引用相同,同时在使用[&]时可不需要声明mutable;

      当该Lambda为类内成员函数中的行内函数时,使用[&]为默认捕获时对this指针依旧是传值捕获,这是一个特殊的捕获处理;

    • [x,&y]

      捕获列表可同时以不同的捕获方式或相同的捕获方式捕获多个特定的外部变量,但在捕获时不可重复以同一种方式捕获,如[x,&x],[=,x][=,&]等;

    • [this]

      表示以传值捕获的方式捕获当前类的this指针;

    • [=,&x]

      表示以传值方式捕获所有外部变量,但对x以传引用捕获方式捕获;

  • (parameters)参数列表

    参数列表与普通函数类似,可指定参数的类型与名称;

    参数列表定义了Lambda表达式接收的输入参数;

    对应的如果Lambda没有参数可以省略()或使用()或在参数列表中传入void;

  • mutable关键字

    该关键字允许在Lambda表达式内部修改通过传值捕获的变量;

    通常传值捕获的变量具有const属性,具有const属性的变量无法被修改,而使用mutable关键字进行修饰后则可以修改通过传值捕获的参数;

  • -> return_type

    Lambda表达式可以显示指定该表达式返回的类型;

    通常该类型可直接省略,让编译器自动推导;

  • { body }

    Lambda表达式的函数体是包含在花括号{}内的代码块,用来定义Lambda表达式的行为;

    该函数体与普通函数的函数体非常相似;

    在函数体内可以使用捕获列表捕获的变量,当该Lambda表达式不存在返回值时可不使用returnLamdba表达式进行返回;

Lambda表达式本质上是一个仿函数,其底层的实现就是依靠仿函数,将先为该函数实例化出一个对象,再调用其对应的operator()()运算符重载;

通常使用auto接收Lambda表达式所实例化的匿名函数对象,在WindowsLambda表达式其函数名是一个以lambda_uuid进行命名的一个仿函数(不同的系统对应命名的方式也不同);

cpp 复制代码
int main() {
  auto f1 = []() { cout << "hello world" << endl; };
  f1();
  cout << typeid(f1).name() << endl;
  return 0;
}

在这个例子中使用Lambda表达式创建了一个匿名函数对象,并用f1,进行接收,同时打印出对应Lambda表达式的类型名,省略了返回值类型与mutable关键字,运行结果为;

bash 复制代码
# 此处以 Linux 做测试

$ ./test 
hello world
Z4mainEUlvE_
  • Lambda 表达式的使用

    cpp 复制代码
    int main() {
      int a = 10;
      int b = 20;
    
      // f1: 值捕获 a 和 b,只读访问
      auto f1 = [=]() { cout << a << " : " << b << endl; };
      f1();  // 输出: 10 : 20
    
      // f2: 引用捕获所有变量,可以修改 a 和 b
      auto f2 = [&]() {
        int tmp = a;
        a = b;
        b = tmp;
      };
      f2();
      cout << a << " : " << b << endl;  // 输出: 20 : 10 (a 和 b 的值被交换)
    
      // f3: 值捕获 a 和 b,使用 mutable 允许修改捕获的副本,但不影响原始变量
      auto f3 = [a, b]() mutable {
        int tmp = a;
        a = b;
        b = tmp;
      };
      f3();
      cout << a << " : " << b << endl;  // 输出: 20 : 10 (原始 a 和 b 不变)
    
      // f4: 值捕获 a,引用捕获 b,mutable 允许修改 a 的副本和 b 的原始值
      auto f4 = [a, &b]() mutable {
        int tmp = a;
        a = b;
        b = tmp;
      };
      f4();
      cout << a << " : " << b << endl;  // 输出: 20 : 20 (只有 b 被修改为 a 的初始值)
    
      b = 5;  // 修改 b 的值
      // f5: 引用捕获 a 和 b,可以直接修改原始变量
      auto f5 = [&a, &b]() {
        int tmp = a;
        a = b;
        b = tmp;
      };
      f5();
      cout << a << " : " << b << endl;  // 输出: 5 : 20 (a 和 b 的值再次交换)
    
      // f6: 通过参数引用直接操作传入的变量
      auto f6 = [](int& x, int& y) {
        int tmp = x;
        x = y;
        y = tmp;
      };
      f6(a, b);
    
      cout << a << " : " << b << endl;  // 输出: 20 : 5 (a 和 b 的值再次交换)
    
      return 0;
    }

    这个例子展示了Lambda表达式的不同捕获方式和他们对变量的影响;

    其运行结果为(参考代码与注释):

    bash 复制代码
    $ ./test 
    10 : 20
    20 : 10
    20 : 10
    20 : 20
    5 : 20
    20 : 5

    同时Lambda表达式可不使用auto接收并直接使用,如:

    cpp 复制代码
    int main() {
      int a = 10;
      int b = 20;
    
      [=]() { cout << a << " : " << b << endl; }(); /* 实例化匿名函数对象后直接使用()进行调用 */
      
      int i = [=]() { cout << a << " : " << b << endl;return 10; }(); /* 也可在实例化匿名函数对象使用()调用后对其返回值进行接收 */
    
      return 0;
    }

Lambda表达式的捕获列表[]只可捕获其父作用域中的变量;

  • Lambda表达式之间不可相互赋值

    Lambda表达式之间不可相互赋值,本质原因是虽然其为一个匿名函数对象,但其在底层有其相应的命名方式,每个Lambda表达式的命名在底层是不同的,故其类型也不相同,无法相互赋值;


function 包装器

std::function函数包装器是C++11引入的一个用于存储,复制和调用任何可调用目标的一个包装器;

其可以存储,复制和调用的对象包括:

  • 普通函数
  • 函数指针
  • Lambda表达式
  • 绑定表达式
  • 函数对象(实现了operator()的类的对象,即仿函数)

基本语法为:

cpp 复制代码
std::function<返回类型(参数类型列表)> 函数对象名;

以下列代码为例:

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

// 定义一个普通的全局函数
void Print() { cout << "Hello Print" << endl; }

// 定义一个函数对象(仿函数)类
class Functor {
 public:
  // 重载 operator() 使得该类的对象可以像函数一样被调用
  void operator()() { cout << "Hello Functor" << endl; }
};

int main() {
  // 定义一个 lambda 表达式
  auto func1 = []() { cout << "Hello Func1" << endl; };

  // 使用 std::function 创建函数包装器
  // f1 包装普通函数 Print
  function<void()> f1 = Print;
  
  // f2 包装 Functor 类的实例(函数对象)
  function<void()> f2 = Functor();
  
  // f3 包装 lambda 表达式 func1
  function<void()> f3 = func1;

  // 通过统一的接口调用这些不同类型的可调用对象
  f1();  // 输出: Hello Print
  f2();  // 输出: Hello Functor
  f3();  // 输出: Hello Func1

  return 0;
}

其运行结果为:

bash 复制代码
$ ./test 
Hello Print
Hello Functor
Hello Func1

function 包装器对成员函数的包装

function包装器对成员函数的包装与普通函数的包装不同;

成员函数隐含了一个this指针,通常在使用function包装器对成员函数包装时通常要为成员函数加上 域作用限定符::&;

  • 对静态成员函数

    使用function包装器对静态成员函数而言其不存在对应的this指针,可直接进行包装;

    但为了与普通函数或Lambda表达式的包装进行区分也应使用&进行区分;

    cpp 复制代码
    #include <iostream>
    #include <functional>
    
    // 定义一个测试类 TestClass
    class TestClass {
     public:
      // 定义一个静态成员函数 func1
      // 静态成员函数不需要类的实例就可以调用
      // 参数: 两个整数 x 和 y
      // 返回值: 整数 (x + y)
      static int func1(int x, int y) {
        printf("func 1 ,x: %d , y: %d \n", x, y);  // 打印输入的参数
        return x + y;  // 返回两个参数的和
      }
    };
    
    int main() {
      // 创建一个 std::function 对象 f1
      // 它可以存储任何返回 int 并接受两个 int 参数的可调用对象
      // 这里我们将静态成员函数 TestClass::func1 的地址赋值给 f1
      std::function<int(int, int)> f1 = &TestClass::func1;
    
      // 调用 f1,传入参数 10 和 20
      // f1 会调用 TestClass::func1(10, 20)
      // 然后打印返回值
      std::cout << f1(10, 20) << std::endl;
    
      return 0;  // 程序正常结束
    }
  • 对非静态成员函数的包装

    非静态成员函数,即普通成员函数中默认存在一个隐含的this指针;

    对于静态成员函数的包装而言相同,都需要加上&与域作用限定符::;

    同时其需要传入一个该类类型的指针,否则参数将不匹配;

    cpp 复制代码
    #include <iostream>
    #include <functional>
    
    // 定义一个测试类 TestClass
    class TestClass {
     public:
      // 定义一个非静态成员函数 func2
      // 非静态成员函数需要通过类的实例来调用
      // 参数: 两个双精度浮点数 x 和 y
      // 返回值: 双精度浮点数 (x + y)
      double func2(double x, double y) {
        printf("func 2 ,x: %.1f , y: %.1f \n", x, y);  // 打印输入的参数,保留一位小数
        return x + y;  // 返回两个参数的和
      }
    };
    
    int main() {
      // 创建一个 std::function 对象 f2
      // 它可以存储任何返回 double 并接受 TestClass* 和两个 double 参数的可调用对象
      // 这里我们将非静态成员函数 TestClass::func2 的地址赋值给 f2
      std::function<double(TestClass*, double, double)> f2 = &TestClass::func2;
    
      // 创建 TestClass 的一个实例 t
      TestClass t;
    
      // 调用 f2,传入 &t(TestClass 实例的地址)和参数 1.1, 2.2
      // f2 会调用 t.func2(1.1, 2.2)
      // 然后打印返回值
      std::cout << f2(&t, 1.1, 2.2) << std::endl;
    
      return 0;  // 程序正常结束
    }

    此处不能直接实例化匿名对象进行传入,匿名对象是一个临时对象,为右值,右值无法被取地址&;

两段代码结合测试并运行:

cpp 复制代码
#include <iostream>
#include <functional>

// 定义一个测试类 TestClass
class TestClass {
 public:
  // 定义一个静态成员函数 func1
  // 静态成员函数不需要类的实例就可以调用
  // 参数: 两个整数 x 和 y
  // 返回值: 整数 (x + y)
  static int func1(int x, int y) {
    printf("func 1 ,x: %d , y: %d \n", x, y);  // 打印输入的参数
    return x + y;  // 返回两个参数的和
  }

  // 定义一个非静态成员函数 func2
  // 非静态成员函数需要通过类的实例来调用
  // 参数: 两个双精度浮点数 x 和 y
  // 返回值: 双精度浮点数 (x + y)
  double func2(double x, double y) {
    printf("func 2 ,x: %.1f , y: %.1f \n", x, y);  // 打印输入的参数,保留一位小数
    return x + y;  // 返回两个参数的和
  }
};

int main() {
  // 创建一个 std::function 对象 f1
  // 它可以存储任何返回 int 并接受两个 int 参数的可调用对象
  // 这里我们将静态成员函数 TestClass::func1 的地址赋值给 f1
  std::function<int(int, int)> f1 = &TestClass::func1;

  // 调用 f1,传入参数 10 和 20
  // f1 会调用 TestClass::func1(10, 20)
  // 然后打印返回值
  std::cout << f1(10, 20) << std::endl;

  // 创建一个 std::function 对象 f2
  // 它可以存储任何返回 double 并接受 TestClass* 和两个 double 参数的可调用对象
  // 这里我们将非静态成员函数 TestClass::func2 的地址赋值给 f2
  std::function<double(TestClass*, double, double)> f2 = &TestClass::func2;

  // 创建 TestClass 的一个实例 t
  TestClass t;

  // 调用 f2,传入 &t(TestClass 实例的地址)和参数 1.1, 2.2
  // f2 会调用 t.func2(1.1, 2.2)
  // 然后打印返回值
  std::cout << f2(&t, 1.1, 2.2) << std::endl;

  return 0;  // 程序正常结束
}

运行结果为:

bash 复制代码
$ ./test 
func 2 ,x: 10 , y: 20 
30
func 1 ,x: 1.1 , y: 2.2 
3.3

bind 绑定

bind是C++11引入的一个函数模板;

该函数模板用于将函数和某些参数进行绑定创建一个新的可调用对象;

这个函数的对象可以不立即调用,同时其可以将部分或全部参数与函数进行绑定;

其基本语法为:

cpp 复制代码
auto newCallable = std::bind(callable, arg1, arg2, ...);
  • callable

    该参数为任何可调用对象,如函数指针,成员函数指针,Lambda表达式,function包装后的可调用对象,仿函数等;

其可以绑定任意数量的参数,通常使用std::placeholders:: _1 , _2 , _3来表示未绑定的参数;

同时bind可改变原函数参数的顺序;

cpp 复制代码
// 定义一个减法函数
int Sub(int a, int b) { return a - b; }

// 定义一个除法结构体,使用函数调用运算符
struct Div {
  int operator()(int a, int b) { return a / b; }
};

int main() {
  // 定义一个取模的 lambda 函数
  auto Mod = [](int a, int b) { return a % b; };

  // 绑定 Sub 函数,保持参数顺序不变
  auto sub1 = bind(Sub, placeholders::_1, placeholders::_2);
  cout << sub1(10, 20) << endl;  // 输出 -10 (10 - 20)

  // 绑定 Sub 函数,交换参数顺序
  auto sub2 = bind(Sub, placeholders::_2, placeholders::_1);
  cout << sub2(10, 20) << endl;  // 输出 10 (20 - 10)

  // 绑定 Div 函数对象,固定第二个参数为 2
  auto div1 = bind(Div(), placeholders::_1, 2);
  cout << div1(100) << endl;  // 输出 50 (100 / 2)

  // 绑定 Div 函数对象,固定第一个参数为 10
  auto div2 = bind(Div(), 10, placeholders ::_1);
  cout << div2(2) << endl;  // 输出 5 (10 / 2)

  // 绑定 Mod lambda 函数,固定两个参数
  auto mod1 = bind(Mod, 11, 10);
  cout << mod1() << endl;  // 输出 1 (11 % 10)

  return 0;
}

在这个例子中:

  • sub1

    简单绑定了一个函数,保持参数顺序不变;

  • sub2

    绑定了一个函数,但交换了参数的顺序;

  • div1

    绑定了一个函数对象(Div仿函数)并固定第二个参数;

  • div2

    绑定了一个仿函数并固定第一个参数;

  • mod1

    绑定了一个Lambda表达式实例化的函数,并固定所有参数;

运行结果如下:

bash 复制代码
$ ./test 
-10
10
50
5
1

在使用function包装器对类内非静态成员函数包装时需要传入一个该类类型的指针类型(隐含this指针);

同时在调用该function包装后的非静态成员函数时需要传入一个对应的该类对象指针,在这种情况下可使用bind绑定第一个this指针从而减少调用参数;

cpp 复制代码
class TestClass {
 public:
  // 定义一个成员函数,接受两个double参数并返回它们的和
  double func(double x, double y) {
    printf("func ,x: %.1f , y: %.1f \n", x, y);
    return x + y;
  }
};

int main() {
  // 声明一个 std::function 对象 f2
  // 它表示一个接受 TestClass* 和两个 double 参数,返回 double 的函数
  // 这里绑定了 TestClass::func 成员函数
  function<double(TestClass*, double, double)> f2 = &TestClass::func;

  // 创建 TestClass 的实例
  TestClass t;

  // 使用 std::bind 创建一个新的可调用对象 testfunc
  // 绑定 f2(即 TestClass::func)到 TestClass 实例 t
  // placeholders::_1 和 _2 表示 testfunc 将接受两个参数
  auto testfunc = bind(f2, &t, placeholders::_1, placeholders::_2);

  // 调用 testfunc,传入 1.1 和 2.2 作为参数
  // 这相当于调用 t.func(1.1, 2.2)
  cout << testfunc(1.1, 2.2) << endl;

  return 0;
}

在这个例子中:

cpp 复制代码
function<double(TestClass*, double, double)> f2 = &TestClass::func;

创建了一个function对象以存储TestClass::func的指针;

其中第一个参数必须为TestClass*(该类的this指针类型);

cpp 复制代码
auto testfunc = bind(f2, &t, placeholders::_1, placeholders::_2);

使用了bind创建一个新的可调用对象名为testfunc;

并创建了一个TestClass对象t,并将该对象取地址&绑定在新的可调用对象testfunc的第一个参数上;

默认placeholders::_1, placeholders::_2传递两个参数;

最后调用绑定后的函数并传入1.12.2作为参数;

执行结果为:

bash 复制代码
$ ./test 
func ,x: 1.1 , y: 2.2 
3.3
相关推荐
拾木2002 分钟前
常见的限流算法
java·开发语言
处处清欢6 分钟前
MaintenanceController
java·开发语言
牵牛老人13 分钟前
Qt技巧(三)编辑框嵌入按钮,系统位数判断,判断某对象是否属于某种类,控件取句柄,支持4K,巧用QEventLoop,QWidget的窗体样式
开发语言·qt
C13 分钟前
C++_map_set详解
c++·stl
Niu_brave17 分钟前
Python基础知识学习(2)
开发语言·python·学习
geekrabbit25 分钟前
Ubuntu 22.04上安装Python 3.10.x
linux·python·ubuntu
神仙别闹33 分钟前
基于C#+Mysql实现(界面)企业的设备管理系统
开发语言·mysql·c#
大柏怎么被偷了40 分钟前
【C++算法】位运算
开发语言·c++·算法
程序猿方梓燚42 分钟前
C/C++实现植物大战僵尸(PVZ)(打地鼠版)
c语言·开发语言·c++·算法·游戏
CPP_ZhouXuyang42 分钟前
C语言——模拟实现strcpy
c语言·开发语言·数据结构·算法·程序员创富