Skip to main content

模板与泛型编程

·2574 words·13 mins· loading
Raven005
Author
Raven005
A little bit about you
Table of Contents

C++的三大模块,面向过程、面向对象、模板与泛型。面向过程就是 C 语言,面向对象就是类,现在轮到模板与泛型了

介绍及实现原理
#

  • 模板能够实现一些其他语法难以实现的功能,但是理解起来会更加困难,容易导致新手摸不着头脑

  • 模板分为类模板和函数模板,函数模板又分为普通函数模板和成员函数模板

  • 模板需要编译两次,在第一次编译时仅仅检查最基本的语法,比如括号是否匹配。等函数真正被调用的时候,才会真正生成需要的类或函数,所以这导致了一个结果,就是不论是模板类还是模板函数,声明与实现都必须放在同一个文件中,因为程序在编译期就必须知道函数具体的实现过程,如果实现和声明分文件编写,需要在链接时才可以看到函数的具体实现过程,这当然会报错,于是人们发明了 .hpp 文件来存放模本这种声明与实现在同一文件的情况

// filename: MyClass.cpp
#pragma once
#include <cstddef>
template <typename T> class MyArray {
  using iterator = T *;
  using const_iterator = const T *;

private:
  T *data;

public:
  // NOTE: size_t在32位系统上定义为 unsigned
  // int,也就是32位无符号整型。在64位系统上定义为 unsigned long ,
  // 也就是64位无符号整形。

  MyArray(size_t count);

  iterator begin() const;

  const_iterator cbegin() const;

  ~MyArray();
};

template <typename T> MyArray<T>::MyArray(size_t count) {
  if (count) {
    data = new T[count]();
  } else
    data = nullptr;
}

template <typename T> MyArray<T>::~MyArray() {
  if (data)
    delete[] data;
}

// NOTE: 定义迭代器得使用typename
template <typename T> typename MyArray<T>::iterator MyArray<T>::begin() const {
  return data;
}

template <typename T>
typename MyArray<T>::const_iterator MyArray<T>::cbegin() const {
  return data;
}
#include <iostream>
#include <string>
#include <memory>
#include "MyClass.hpp"

int main() {

  MyArray<int> st(100);
  std::cout << *st.begin() << std::endl;
  return 0;
}

initializer_list
#

initializer_list 其实就是初始化列表,我们可以用初始化列表初始化各种容器,比如vector、array

#ifndef MYCLASS_H_
#define MYCLASS_H_

#include <cstddef>
#include <initializer_list>
#include <type_traits>
// NOTE:类型萃取
#include <vector>

// NOTE: 模板特化

template <typename T> struct get_type {
    using type = T;
};

template <typename T> struct get_type<T *> {
    using type = T;
};

template <typename T> class MyArray {
private:
    T *data{ nullptr };
    // NOTE:
    // 在创建的时候实际上是创建了两层指针,第一层是data,第二层是data里面的元素
    // 析构函数只删除了第一层指针,导致第二层还留着,造成内存泄露
    // 得使用智能指针来避免的这个问题
    // 如果是容器的话,直接用vector就好了
    //
    using iterator       = T *;
    using const_iterator = const T *;
    unsigned int cnt{ 0 };

public:
    MyArray( const MyArray & )            = delete;
    MyArray &operator=( const MyArray & ) = delete;
    MyArray( MyArray && )                 = delete;
    MyArray &operator=( MyArray && )      = delete;
    MyArray( std::size_t count );
    MyArray( const std::initializer_list<T> &list );
    MyArray( std::initializer_list<T> &&list );
    iterator begin() const;
    const_iterator cbegin() const;

    T &operator[]( unsigned int query ) const { return data[query]; }

    ~MyArray();
};

template <typename T> MyArray<T>::MyArray( std::size_t count ) : cnt( count ) {
    if ( count > 0 ) {
        data = new T[count]();
    }
}

template <typename T> typename MyArray<T>::iterator MyArray<T>::begin() const {
    return data;
}

template <typename T>
typename MyArray<T>::const_iterator MyArray<T>::cbegin() const {
    return data;
}

template <typename T>
MyArray<T>::MyArray( const std::initializer_list<T> &list ) {
    if ( list.size() ) {
        data = new T[list.size()]();
        if constexpr ( std::is_pointer<T>::value ) {
            for ( auto elem : list )
                data[cnt++] = new typename get_type<T>::type( *elem );

        } else {
            for ( const auto &elem : list )
                data[cnt++] = elem;
        }

    } else
        data = nullptr;
}

template <typename T> MyArray<T>::MyArray( std::initializer_list<T> &&list ) {
    if ( list.size() ) {
        data = new T[list.size()]();

        for ( auto &elem : list )
            data[cnt++] = elem;
    } else
        data = nullptr;
}

template <typename T> MyArray<T>::~MyArray() {
    if ( data ) {
        if constexpr ( std::is_pointer<T>::value ) {
            for ( size_t i = 0; i < cnt; ++i ) {
                delete data[i];
            }
        }
    }
    delete[] data;
}

#endif // MYCLASS_H_
#include "MyClass.hpp"
#include <iostream>

int main( int argc, char *argv[] ) {

    std::initializer_list<int> iList{ 1, 2, 3, 4, 5, 6 };

    int i1 = 10, i2 = 20, i3 = 30, i4 = 40;

    std::initializer_list<int *> iList1{ &i1, &i2, &i3, &i4 };
    MyArray<int *> arrayP( iList1 );
    for ( std::size_t i = 0; i < iList1.size(); i++ )
        std::cout << *arrayP[i] << std::endl;

    return 0;
}

typename
#

  1. 在定义模板时表示这个一个待定的类型

  2. 在类外表明自定义类型时使用

在 C++的早起版本,为了减少关键字的数量,用class来表示模板的参数,但是后来因为第二个原因,不得不引入typename关键字

函数模板、成员函数模板
#

namespace mystd {
// NOTE: 函数模板
template <typename iter_type, typename func_type>
void for_each(iter_type first, iter_type last, func_type func) {
  for (auto iter = first; iter != last; iter++) {
    func(*iter);
  }
}

template <typename T> class Myvector {
public:
  template <typename T2> void OutPut(const T2 &elem);
};
// NOTE:类外定义函数

template <typename T>
template <typename T2>

void Myvector<T>::OutPut(const T2 &elem) {
  std::cout << elem << std::endl;
}

} // namespace mystd

int main() {
  std::vector<int> ivec{1, 2, 3, 4, 5};
  mystd::for_each(ivec.begin(), ivec.end(), [](int &elem) { ++elem; });

  for (auto &elem : ivec) {
    std::cout << elem << std::endl;
  }
  mystd::Myvector<int> myvec;

  for (const auto &elem : ivec) {
    myvec.OutPut(elem);
  }

  return 0;
}

默认模板参数
#

  1. 默认模板参数是一个经常使用的特性,比如在定义 vector 对象时,我们就可以使用默认分配器

  2. 模板默认参数和普通函数的默认参数一样,一旦一个参数有了默认参数,它之后的参数都必须有默认参数

  3. 函数模板使用默认模板参数

#include <functional>
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>

namespace mystd {
// NOTE: 函数模板

using void_int_func_type = std::function<void(int&)>;

template <typename iter_type, typename func_type = void_int_func_type>
void for_each(iter_type first , iter_type last , func_type func = [](int &elem) {++elem;}) {
  for(auto iter = first; iter != last; iter ++)
    func(*iter);
}



} // namespace mystd

int main() {
  std::vector<int> ivec{1, 2, 3, 4, 5};
  mystd::for_each(ivec.begin(), ivec.end());

  for (auto &elem : ivec) {
    std::cout << elem << std::endl;
  }

  return 0;
}
  1. 类模板使用默认模板参数
#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <vector>

namespace mystd {

template <typename T, typename allocator_type = std::allocator<T>> class Myvector {
public:
  template <typename T2> void OutPut(const T2 &elem);
};

template <typename T, typename allocator_type>
template <typename T2>
void Myvector<T, allocator_type>::OutPut(const T2 &elem) {
  std::cout << elem << std::endl;
}

} // namespace mystd

int main() {
  mystd::Myvector<int> ivec;
  ivec.OutPut(29);

  return 0;
}

模板重载与偏特化、全特化
#

模板重载
#

函数模板是可以重载的(类模板不能重载),通过重载可以应对更加复杂的情况。比如在处理 char*string 对象时,虽然都可以代表字符串,但 char* 在复制时直接拷贝内存效率明显更高, string 就不得不调用构造函数了,所以在一些比较追求效率的程序中对不同类型进行不同的处理还是非常有意义的

#include <iostream>
#include <string>
#include <vector>

template <typename T> void test(const T &param) {
  std::cout << "void test(const T &param)" << std::endl;
}

template <typename T> void test(T *param) {
  std::cout << "void test(T *param)" << std::endl;
}

void test(double param) {
  std::cout << "void test(double param)" << std::endl;
}

int main() {
  test(100);
  int i = 100;
  test(&i);
  test(2.2);
  int a = 20 , b = 30;
  return 0;
}

其实函数模板的重载和普通函数的重载没有什么区别,在讲完类模板特化就能知道重载和特化的区别了,这一点暂时不用在意了

模板特化
#

  • 模板特化的意义函数模板可以重载以应对更加精细的情况,类模板不能重载,但可以特化来实现类似的功能

  • 模板的特化可以分为两种:全特化和偏特化

    1. 模板的全特化:就是指模板的实参列表与相应的模板参数列表一一对应

    2. 模板的偏特化:偏特化就是介于普通模板和全特化之间,只存在部分类型明确化,而非将模板唯一化

  • 其实对于函数模板来说,特化与重载可以理解为一个东西

#include <iostream>
#include <string>
#include <vector>


template<typename T1 , typename T2>
class Test {
public:
  Test() {
    std::cout << "common template" << std::endl;
  }
};

template<typename T1 , typename T2>
class Test<T1* , T2*> {
public:
  Test() {
    std::cout << "point template" << std::endl;
  }
};

// NOTE:部分特化
template<typename T2>
class Test<int, T2> {
public:
  Test() {
    std::cout << "int -semi-special" << std::endl;
  }
};

// NOTE: 全特化
template<>
class Test<int , int> {
public:
  Test() {
    std::cout << "int, int complete special" << std::endl;
  }
};

int main() {

  Test<int * , int *> test1;
  Test<int , double> test2;
  Test<int , int> test3;
  Test<int* , int> test4;
  return 0;
}

模板的递归
#

如何提高程序运行效率?在编译时就把需要计算的结果算出来,避免运行时占用CPU时间去做耗时计算,这就是模板递归的原理

#include <iostream>

using i64 = long long;

template<int m , int n>
class C {
public:
    static const i64 result = C<m , n - 1>::result * m;
};

template<int m>
class C<m , 0>{ // NOTE: 递归终止条件
public:
   static const i64 result = 1;
};
int main() {
    std::cout << C<2,4>::result << std::endl;
    return 0;
}
  • 缺点

    1. 不能计算变量的值,也就是对于运行时缺点能够的数据不能通过模板递归出来,因为编译时需要确定运行结果
    2. 不能进行异常处理,也就是模板传入一个错误的数字后可能导致崩溃
  • 优点编译时就把数据计算出来了,对于运行时的大数据递归计算效率大大提高了

可变参数、折叠表达式
#

这玩意对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数

实现方式:

  • 需要有一个只有一个参数的函数作为递归终止函数
  • 递归调用
template<typename T>
void print(T&& x) {
    std::cout << x << " ";
}

template<typename T , typename... Args>
void print(T&& first , Args&&... args) {
    std::cout << first << " ";
    print(std::forward<Args>(args)...);
}

这样写有时候又有点麻烦,有没有一种方法可以只需要写一个函数就可以了,不需要写递归终止函数,c++17就提出了折叠表达式

template<typename... Args>
void print2(Args&&... args) {
    ((std::cout << args << ' ') , ...); //NOTE: 这里为了让每个参数输出后,都得带一个空格,就使用了逗号表达式
    std::cout << std::endl;
}

折叠分类
#

分为左折叠和右折叠

  1. 左折叠
template<typename... Args>
auto sum(Args&&... args) {
    return (... + args);
}

// int sum(1 , 2 , 3 , 4 , 6) -> 1 + (2 + (3 + (4 + 6)))
  1. 右折叠
template<typename... Args>
auto sum(Args&&... args) {
    return (args + ...);
}

// int sum(1 , 2 , 3 , 4 , 6) -> ((((1 + 2) + 3) + 4) + 5) + 6
  • 总结:只需要看 ... 在操作符的左侧还是右侧即可,如果在左侧就是左折叠,如果在右侧就是右折叠。
  • 注意:在折叠表达式中,左折叠和右折叠的减法操作和除法操作由于操作顺序不一样,结果也会不一样

空参数包问题
#

空参数包就是没有参数,对于大多数操作符而言,空参数包将会引发编译错误。针对上面的例子,我们可以加上一个0,这个0相当于缺省值,这就变成了二元折叠。

二元折叠

template<typename... Args>
auto sum(Args&&... args) {
    return (args + ... + 0);
}
// int sum(1 , 2 , 3 , 4 , 6 , 0) -> (((((1 + 2) + 3) + 4) + 5) + 6) + 0

一些例子
#

  1. 计算指定区间内包含指定数值的个数
#include <iostream>
#include <algorithm>
template<typename T , typename... Args>
size_t count(T&& range , Args&&... args) {
    return (std::count(std::begin(range) , std::end(range) , args) + ...);
}

void test_variadic_template() {
    std::string s = "aabcdef";
    std::cout << count(s , 'X' , 'c' , 'v', 'a') << std::endl;
}

int main() {
    test_variadic_template();
}

auto关键字
#

关于输出 auto 推断的变量类型 – boost 库
#

#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
    auto i = 100;
    std::cout << type_id_with_cvr<decltype(i)>().pretty_name() << std::endl;

    return 0;
}

需要注意的点
#

  1. auto 只能推断出类型,引用不是类型,所以 auto 无法推断出引用,要使用引用只能自己加引用符号
  2. auto关键字在推断引用的类型时会直接将引用替换为引用指向的对象。其实引用一直是这样的,引用不是对象,任何使用引用的地方都可以直接替换成引用指向的对象。
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{
    const int i = 100;
    const int& refi = i;
    auto i2 = refi;// 其实i2直接指向i
    std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;

    return 0;
}
  1. auto 关键字在推断类型时,如果没有引用符号,会忽略值类型的 const 修饰,而保留修饰指向对象的 const,典型的就是指针。
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{

    int i = 100;
    int a = 120;
    const int* const pi = &i;
    auto i2 = pi;// const int*
    i2 = &a;
    std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;
    std::cout << *i2 << std::endl;
    // const int i = 100;
    // auto i2 = i;
    // std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;
    return 0;
}
  1. auto 关键字在推断类型时,如果有了引用符号,那么值类型的 const 和修饰指向对象的 const 都会保留。
#include<iostream>
#include<boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;

int main()
{

    int i = 100;
    const int* const pi = &i;
    auto& i2 = pi;
    std::cout << type_id_with_cvr<decltype(i2)>().pretty_name() << std::endl;
    return 0;
}
  1. 当然,我们可以在 auto 前面加上 const ,这样永远都有 const 的含义。

  2. auto 不会影响编译速度,甚至会加快编译速度。因为编译器在处理 XX a = b 时,当 XX 是传统类型时,编译期需要检查 b 的类型是否可以转化为 XX。当 XX 为 auto 时,编译期可以按照 b 的类型直接给定变量 a 的类型,所以效率相差不大,甚至反而还有提升。

  3. 最重要的一点,就是 auto 不要滥用,对于一些自己不明确的地方不要乱用 auto,否则很可能出现事与愿违的结果,使用类型应该安全为先。

  4. auto 主要用在与模板相关的代码中,一些简单的变量使用模板常常导致可读性下降,经验不足还会导致安全性问题。

#include <iostream>
#include <vector>
using namespace std;

vector<int> A;

int main() {
  int n = 5;
  while (n--) {
    int x;
    cin >> x;
    A.push_back(x);
  }

  for (vector<int>::iterator i = A.begin(); i != A.end(); i++)
    cout << *i << endl;// 可以用auto来代替vector<int>::iterator

  return 0;
}
  1. auto在推断数组时为推断出std::initializer_list
#include <iostream>

template<typename T>
class TypeDisplayer;

template<typename T>
void func1(T t) {
    TypeDisplayer<decltype(t)> test;
}

int main() {

    auto x = {1 , 2 , 3 , 4}; // NOTE: std::initializer_list<int>
    func1(x);


    return 0;
}
  1. 无法推断返回类型,要使用decltype

template<typename T>
class TypeDisplayer;

template<typename T>
// BUG: template deduction
auto func(T t) {
    return {1 , 2 , 3};
}

int main() {
    int a = 10;
    func(10);

    return 0;
}
  1. 不要将auto用在返回类型为bool的函数上

decltype关键字
#

  • auto,用于通过一个表达式在编译时确定待定义的变量类型,auto所修饰的变量必须被初始化,编译器需要通过初始化来确定auto所代表的类型,即必须要定义变量。
  • 若仅希望得到类型,而不需要(或不能)定于变量的时候那应该怎么办?C++11新增了 decltype 关键字,用来在编译时推导出一个表达式的类型。

使用编译器诊断
#

template<typename T>
class TypeDisplayer; // NOTE: just defination no declartion

TypeDisplayer<decltype(expression)> test;

例子
#

  1. 参数类型是指针/引用
namespace {
template<typename ParamType>
void func(ParamType& t) {

}
};

// NOTE:
// 1. if has reference, ignore the reference part
// 2. regular match


int x = 10;
const int cx = x;
const int& rx = x;

func(x); // NOTE: T-> int  ParamType int&
func(cx); // T-> const int   ParamType const int&
func(rx); // T-> const int  ParamType const int&
  1. 参数类型是万能引用
namespace {
template<typename ParamType>
void func(ParamType&& t) {

}
};

// NOTE:
// 1. if l-value -> T&
// 2. if r-value -> normal rules applies


int x = 10;
const int cx = x;
const int& rx = x;

func(x); // NOTE: T-> int  ParamType int& -> Reference Collapse(引用折叠)
func(cx); // NOTE: T-> const int   ParamType const int&
func(rx); // NOTE: T-> const int  ParamType const int&
func(27); // NOTE: T-> int
  1. 参数类型不是指针/引用
#include <iostream>

template<typename T>
class TypeDisplayer;
template<typename T>
void func(T param) {
    TypeDisplayer<decltype(param)> test;
    param = 100;
}

// NOTE: ignore the reference and ocnst

int main() {
    int x = 10;
    const int cx = x;
    const int& rx = x;

    func(x); // NOTE: T-> int  ParamType int& -> Reference Collapse(引用折叠)
    func(cx); // NOTE: T-> const int   ParamType const int&
    func(rx); // NOTE: T-> const int  ParamType const int&
    func(27); // NOTE: T-> int
    std::cout << rx << std::endl;

    return 0;
}
  1. 数组退化
#include <iostream>

template<typename T>
class TypeDisplayer;
template<typename T>
void func(T& param) {
    // TypeDisplayer<decltype(param)> test;
    // NOTE: if has no reference, is a pointer
    // else is a array
    std::cout << sizeof param << std::endl;
}


int main() {
    const char hello[13] = "nihao"; // NOTE: const char* Array Decay(数组退化成指针)
    func(hello);
    return 0;
}

确认返回值类型
#

decltype通常用于表面一个函数模板的返回值的类型

#include <iostream>
#include <vector>
#include <cassert>

template <typename T> class TypeDisplayer;

template <typename Container, typename Index>
decltype(auto) func(Container&& c, const Index& i ) {
    return std::forward<Container>(c)[i];
    // NOTE: 不使用decltype之前,函数返回值是int,使用之后,函数返回值是int&
}

int main() {
    std::vector<int> a{1 , 4 , 48 , 489 , 382};
    int& res = func(std::vector<int>{1 , 3, 48 , 890} , 1);

    return 0;
}

decltype(auto) f1() {
    /*
     expression
    */
    retunr x // NOTE: decltype(x) is int , so f1 returns int
}

decltype(auto) f2() {
    /*
     expression
    */
    retunr (x) // NOTE: decltype(x) is int& , so f2 returns int
}

后置推导
#

template <typename F, typename... Args>
auto commit(F&& func, Args&&... args) -> std::future<decltype(func(args...))> {}

using RetType = decltype( func( args... ) );

这段代码的意思是,在给线程池做任务提交的时候,去推断任务(也就是函数)的返回值类型,然后保存到一个std::future对象中,供我们异步调用该任务

左值、右值以及引用
#

左值、右值
#

C++任何一个对象要么是左值要么是右值

例如 int i = 10 , i和10都是对象

左值:拥有地址属性的对象就叫左值,左值来源于 C 语言的说法,能放在"=“左边的就是左值

左值也可以放在”=“右边

右值:不是左值的对象就是右值。或者说无法操作地址的对象就叫做右值。一般来收,判断一个对象是否为右值,就看它是不是左值,有没有地址属性,不是左值,那就是右值

比如临时对象,就都是右值,临时对象的地址属性无法使用

注意:左值也可以放在”\=“右边,但右值绝对不可以放在”=“左边

#include <iostream>

int main() {

  int i = 10;
  int i2 = (i + 1);// i + 1 是一个临时对象
  ++i = 200;//++i 是先给i加1,然后返回i
 // i++ = 200;//先返回一个临时变量,这个临时变量的值等于i的值,而临时变量没有地址属性

  return 0;
}

引用的分类
#

  1. 普通左值引用:就是一个对象的别名,只能绑定左值,无法绑定常量对象
#include <iostream>

int main() {

  int i = 10;
  int& re = i;

  std::cout << re << std::endl;
  return 0;
}
  1. const 左值引用:可以对常量起别名,可以绑定左值和右值

C++规定const T&这种方式的左值引用可以接受右值(即亡值、纯右值)

#include <iostream>

int main() {

  const int i = 10;
  const int& re = (i + 1);

  std::cout << re << std::endl;
  return 0;
}
  1. 右值引用:只能绑定右值的引用
#include <iostream>
int main() {

  int i = 10;
  int&& rrefi = 200;
  int&& r1 = i ++;
  std::cout << r1 << std::endl;
  return 0;
}

move函数、临时对象
#

move函数
#

  1. 右值看重对象的值而不考虑其地址属性,move 函数可以对一个左值使用,是操作系统不再在意其地址属性,将其完全视作为一个右值

  2. move 函数让操作的对象失去了地址属性,所以我们有义务保证以后不再使用该变量的地址属性,简单来说就是不再使用该变量,因为左值对象的地址是其使用时无法绕过的属性

#include <iostream>
int main()
{
  int i = 10;
  // FIX: int&& rrefi = i; i是一个左值
  int&& rrefI = std::move(i);
  // NOTE: i依旧是一个左值,move(i)是将i的地址属性忽略掉,是一个右值
  return 0;
}

临时对象
#

  1. 右值都是不体现地址的对象,那么,还有什么能比临时对象更加没有地址属性呢?右值引用主要负责处理的就是临时对象

  2. 程序执行时生成的中间对象就是临时对象,注意所有的临时对象都是右值对象,因为临时对象昌盛后很快就可能被销毁,使用的是它的值属性

#include <iostream>

int getI()
{
  return 10;// NOTE:该函数返回的值就是临时对象
}


int main()
{
  int i = 10;
  int&& rrefI = getI();
  return 0;
}

万能引用和引用折叠
#

万能引用
#

  • 只用两种引用的形式,左值引用和右值引用,万能引用不是一种引用类型,它存在于模板的引用折叠情况,但能够接受左值和右值
  • 一个右值一旦有名字那么就变成了左值
  • 区分左值和右值的一个简单方式就是能不能取到地址
#include <iostream>

void f(int& t) {
    std::cout << "lvalue" << '\n';
}

void f(int&& t) {
    std::cout << "rvalue\n";
}
template<typename T>
void test(T&& v) {
    f(std::forward<T>(v));
}

int main() {
    int i = 0;
    test(i);
    test(1);
    test(std::move(i));

    return 0;
}

引用折叠
#

声明引用的引用是非法的,但编译器却可以在模板实例化过程产生引用的引用

int&&&& j = 1

模板实例化过程中出现这种情况就会发生引用折叠,如果任一引用为左值引用,则结果为左值引用,若两个皆为右值引用结果为右值引用。

但凡有任何一种引用是左值引用,那么就是左值引用,否则就是右值引用。

#include <iostream>
#include <type_traits>

void f(int& t) {
    std::cout << "lvalue" << '\n';
}

void f(int&& t) {
    std::cout << "rvalue\n";
}
template<typename T>
void test(T&& v) {
    std::cout << "is int& -> " << std::is_same_v<T , int&> << std::endl;
    std::cout << "is int  -> " << std::is_same_v<T , int> << std::endl;
}

int main() {
    std::cout << std::boolalpha;
    int i = 0;
    test(i);
    test(1);

    return 0;
}
void f(Widget&& param) // rvalue reference

Widget&& var1 = Widget() // rvalue reference

auto&& var2 = var1 // universal reference

template<typename T>
void f(std::vector<T>&& param) // rvalue reference

template<typename T>
void f(T&& param) // universal reference

一些方法
#

  • std::move -> unconditional cast(不管传入的参数是左值还是右值,都会转化为右值)
  • std::forward -> conditional cast when the param is rvalue(保留传入参数的属性,左值->左值 右值->右值)

如何区别右值引用和万能引用
#

只需要判断是否有整体类型推断即可

#include <iostream>
namespace {

template<typename T>
class TypeDisplayer;

class A{
public:
    int a_ = 10;
};
A MakeAObject() {
    return A(10);
}

template<typename T>
// NOTE:
// universal reference
// reference collapse
// A&&& -> A&
void f(T&& param) {
    TypeDisplayer<decltype(param)> test;
}

};

int main() {

    // NOTE:
    // 1. 表明转换为右值的可行性
    // 2. 绑定一个临时对象
    A&& a = MakeAObject();
    a.a_ = 100;
    auto&& r = a; // universal reference -> lvalue reference
    r.a_ = 200;
    std::cout << a.a_ << std::endl;
    return 0;
}
  • 总结:
    • 在一个函数模板中,如果参数的类型是 T&& 去推断一个类型 T 或者如果一个对象是使用了 auto&& 来表示,那么这个参数或对象就是一个万能引用
    • 对于万能引用来说,如果是一个右值来初始化,那么万能引用就是右值,如果是一个左值来初始化,那么万能引用就是左值

避免重载万能引用
#

#include <iostream>
#include <set>

namespace {
std::multiset<std::string> names;

// NOTE:
// exact match
// T -> short&
template<typename T>
void Func(T&& str) {
    std::cout << "in template" << std::endl;
    names.emplace(std::forward<T>(str));
}


// NOTE:
// conversion from short to index
void Func(int Index) {
    // str->map.get(Index)
    // names.emplace(str)
}

};

int main() {

    std::string s1 = "hello";
    Func(s1);
    Func("hello");
    Func(std::string("hello"));

    short a = 1;
    Func(a);

    return 0;
}

上面代码中的Func有两个重载,一个是万能引用,另一个是有实例化的重载,在传入参数 short a 的时候,由于实例化好的函数需要一个转换,而万能引用可以直接匹配上,所以会调用万能引用的函数。

关于std::forward的实现
#

template<typename T>
T&& forward(typename remove_reference<T>::type& param) {
    return static_cast<T&&>(param);
}