模板初步

函数模板#

写一个最简单的模板函数:

template<typename T1, typename T2>
T1 max(T1 a, T2 b) {
    return a < b ? b : a;
}

比较两个数的大小,然后返回最大值。存在一个问题,返回的类型将会和第一个入参的类型一致。

cout << max(4.2, 7) << endl;
cout << max(4, 7.2) << endl;

执行这两个时,返回的都将会是整型,结果都为7,这不是我们期望的。如果是一个整型和浮点型比较,浮点型比较大就返回浮点型,整型比较大就返回整型,这才是我们期待的。
当然,我们可以通过显式指定返回类型的方式来明确我们期待返回什么:

template<typename Ret, typename T1, typename T2>
Ret max(T1 a, T2 b) {
    return a < b ? b : a;
}

在模板参数最前面再加上一个参数,表示我们希望返回的类型,由于返回类型无法直接进行推导,所以在使用的时候需要显式地指定它。后面的参数可以通过函数的入参进行推导,所以不用再显式指定。

cout << max<int>(4.2, 7) << endl;
cout << max<double>(4, 7.2) << endl;

简单的模板函数倒还好,如果函数写的再复杂一点,甚至连我们自己都不确定使用的时候需要返回什么类型合适的时候,就显得不是那么“聪明”了。
更好的办法是使用C++标准库中提供的模板技巧,来获取两者的共同类型,使用std::common_type_t模板类来萃取出两个类型的通用类型。

template<typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b) {
    return a < b ? b : a;
}

还有更简洁的做法,就是用auto来代替返回类型,直接交给编译器去推导。

template<typename T1, typename T2>
auto max(T1 a, T2 b) {
    return a < b ? b : a;
}

这样也可以获得适当的返回类型。

三目运算符可以由编译器来推导出要返回的类型,如果将三目运算符改成if-else的话,编译就会报错,因为不能保证if-else的作用域中的返回值是有公共类型的。

模板函数重载#

模板函数是允许重载的,当有多个重载的模板函数时,编译器会选择匹配度更高的那一个。

template<typename T1, typename T2>
int max(T1 a, T2 b) {
    cout << "two template params\n";
    return a < b ? b : a;
}

template<typename T>
T max(T a, T b) {
    cout << "one template params\n";
    return a < b ? b : a;
}

在这里重载了比较最大值的函数,现在要比较max(7, 42),那么编译器会选择第二个函数,即一个模板参数的函数。调用这个函数并将结果输出到终端可以看到:

one template params
42

重载规则#

当调用一个函数时,可以理解为经历了以下的几个步骤:

  1. 查找函数名称,形成一个最初的重载集;
  2. 如有必要,修改这个重载集,比如模板函数的实现或者模板参数的推导等;
  3. 其中与调用不匹配的函数将会从重载集中删除,得到一个可行的候补重载集;
  4. 执行重载解析寻找最优的候选函数,如果找到唯一的最优候选,就调用它,否则会报二义性的错误;
  5. 最后还要检查一下被选中的函数,如果它是声明被删除(=delete),或者是不可访问的私有成员函数,编译器会报诊断错误。

这一切都会在编译阶段执行,所以如果是正确的调用往往是无感的,而错误的调用会导致编译失败。

函数的调用也不总是会被重载解析,例如使用函数指针来调用函数,这将完全取决于函数指针指向哪里,这是在运行期才能确定的;还有类似函数的宏,也不会被重载解析。

匹配程度由高到低排列如下:

  1. 完美匹配,参数和表达式类型完全一致或者是表达式类型的引用(忽略cv限定符)。
  2. 细节调整,比如数组退化成指针,或者入参添加const限定符。
  3. 提升的匹配,提升是一种隐式转换。比如将位数较小的整型(bool,char,short,某些枚举类型)提升为int或long long,将float提升为double。
  4. 标准类型转换,比如将整型转换为浮点型,派生类转换成明确的基类。
  5. 自定义的类型转换,允许任何用户自定义的类型隐式转换。
  6. 省略号的匹配。C++中省略号类似于一个参数包,原则上可以接受任何类型,有一种情况除外,类如果具有特殊的拷贝构造,行为将会难以预测。

看起来似乎也还可以,但在使用的时候需要万分小心。

void combine(int, double);

void combine(long, int);

int main() {
    combine(1, 2);  // error
}

在这里调用combine()编译时会报二义性的错误,int类型入参都是完美匹配的,而double和long都可以通过转换得到,尽管直观上long比double更接近我们的预期,但是编译器并不这么认为,C++不会试图去衡量多个调用参数的匹配度,所以当前场景下的两个重载函数都是可用的,所以会报二义性的错误。

类模板#

类模板和模板函数类似,在类名前加上template关键字来声明,不过模板类必须在全局中或是命名空间中声明,不能在函数或者块作用域中声明(普通的类可以)。
类名后跟随尖括号包裹的类型来实例化具体的类。在模板类内部可以不指定具体的类型,而在类外部需要明确指定。

template<typename T>
class Stack {
public:
    Stack(const Stack&);
    Stack& operator=(const Stack&);
};

template<typename T>
bool operator==(const Stack<T>& lhs, const Stack<T>& rhs);

类似于函数的重载,模板类可以有特化和偏特化版本,并且在匹配的情况下拥有比普通模板类更高的优先级。

template<typename T1, typename T2>
class MyClass {
    // ...
};

/*特化*/
class MyClass<int, int> {
    // ...
};

/*偏特化*/
template<typename T>
class MyClass<T, T> {
    // ...
};

/*偏特化*/
template<typename T>
class MyClass<int, T> {
    // ...
};

另外,模板类的模板参数也可以像函数的入参一样拥有一个默认值,如果不显式指定的话,就会使用默认的类型作为模板参数。

非类型模板参数#

有时候我们想传入一个值作为模板的参数,这是允许的。

template<typename T, std::size_t sz>
class Array {
    // ...
};

比如我们定义一个数组类,数组类需要明确的大小,于是可以将希望的数组大小作为模板参数传给模板类。
但这个非类型模板参数不是想穿什么都行的,比如浮点型double就不可以,只能是整型常量、指向对象\函数\成员的指针,或者对象\函数的左值引用,以及nullptr类型。
另外,还可以使用auto作为占位符,进一步增加灵活性,这样可以传入所有允许的非类型模板参数。

变参模板#

变参模板可以接受任意多的参数作为模板的参数。

void print() {
    
}

template<typename T, typename... Types>
void print(T first_args, Types... args) {
    std::cout << first_args << '\n';
    print(args...);
}

例如这样一个打印函数,将会依次打印对应模板类型参数。这样定义的args实际上是一个函数参数包,而Types是一个类型参数包。

std::string str{ "world" };
print(7.5, "hello", str);

这里将会顺序将入参打印出来。print会首先被展开成print<double, const char*, std::string>(7.5, "hello", str),然后依次再往下递归调用。

扩展一下,可变模板参数可分为三类:类型模板参数包,非类型模板参数包和模板模板参数包:

// 类型模板参数包
template<typename... Types>     

// 非类型模板参数包
template<int... Args>     

// 模板模板参数包
template<typename T, template<T> typename... Types>

大体上可以理解为都是使用省略号来表征为变参参数,类型模板参数包就可以接受任意的能识别的类型;非类型模板参数包就和非类型模板参数一样,只能是固定的类型常量值,但可以接受任意数量;模板模板形参包的类型是模板类型。
其中,类型模板形参包和模板模板形参包是可以作为函数形参包的,但非类型形参包不可以,很好理解,以为前两种是类型,而非类型形参包是具体的常量值。

sizeof... 运算符#

C++11以后引入了sizeof...运算符用来计算参数包中的元素数量。

void print() { }

template<typename T, typename... Types>
void print(T first, Types... args) {
    cout << sizeof...(args) << endl;
    return print(args...);
}

int main() {
    string str{ "world" };
    print(7.5, "hello", str);
}

这次,将会依次打印参数包中的参数数量2,1,0

折叠表达式#

C++17后,为参数包引入了更多的特性,其中包括可以对参数包中的所有参数进行二元运算。

折叠表达式 计算顺序
... op pack ((pack1 op pack2) op pack3) ... op packN
pack op ... pack1 op (... op (packN-1 op packN))
init op ... op pack ((init op pack1) op ...) op packN
pack op ... op init pack1 op (... op (packN op init))

需要注意参数包可能是空的情况,当参数包是空的时候,二元算法可能会报错,不过有几个特殊情况:&&计算结果为真,||计算结果为假,逗号运算符的空参数包对应值为void()

变参基类#

class Customer {
public:
    Customer(std::string_view str)
        : name(str) { }  

    std::string getName() const {
        return name;
    }
    
private:
    std::string name{};
};

struct CustomEq {
    bool operator()(const Customer& c1, const Customer& c2) const {
        return c1.getName() == c2.getName();
    }
};

struct CustomerHash {
    std::size_t operator()(const Customer& c) const {
        return std::hash<std::string>()(c.getName());
    }
};

// 变参作为基类
template<typename... Bases>
struct Overloader : Bases... {
    using Bases::operator()...;     // 引入变参基类的运算符重载函数
};


int main() {
    using CustomOP = Overloader<CustomerEq, CustomerHash>;
    std::unordered_set<Customer, CustomOP, CustomOP> coll;

    return 0;
}

变参参数包有一种很变态的用法,可以作为派生类的基类。正如上面展示的,CustomOP是一个派生类,将传入的类型参数包中的类型都作为自己的基类,并在自己内部引入了所有基类的小括号运算符的重载函数。这样传入到unordered_set中时,既可以进行相等比较,又可以计算哈希值。

模板使用“寄”巧#

  • 使用typename关键字来说明标识符是一种类型。
template<typename T>
class MyClass {
public:
    void foo() {
        typename T::SubType* ptr;
    }
};

使用typename来说明T::SubType*是对应的指针类型,如果不加上这个关键字的话,可能会被解析成T中的静态成员SubTy与ptr的乘积,这在某些实例化中可能是对的,但在这里我们希望ptr是个指针,所以加上typename来明确。

  • 使用初始化列表对变量进行初始化。
  • 使用this->
  • 小心处理原始数组和字符串字面量的模板,在模板参数声明为引用时,实参类型不会退化,例如传递实参"hello"会被推导为const char[6]类型,如果是在按值传递的函数中,会退化成const char*类型。

作者:cwtxx

出处:https://www.cnblogs.com/cwtxx/p/18718200

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   cwtxx  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示