首页 / 资讯中心 / 文章详情

C++ 类模板与成员函数模板示例解析

C++ 类模板与成员函数模板示例解析 ★ FEATURED ARTICLE
前言类模板class template把整个类参数化成员函数模板member function template则把「某一个成员函数」再参数化一层。两者经常一起出现容器类本身有元素类型T而它的emplace还要接受任意构造参数转换构造函数还要接受「元素类型可转换的另一个容器」。理解这两层模板如何嵌套、如何在类外定义、如何被约束是读懂标准库源码的门槛。常见的误解有以为成员函数模板和普通成员函数一样类外定义只写一个template头以为Bufferint能访问Bufferdouble的私有成员不能除非显式友元以为在模板派生类里可以直接用基类的成员名依赖基类需要this-或限定。这些都不是风格问题而是会导致编译失败的硬规则。本文以一个BufferT为例逐层拆解类模板的声明、类外定义、成员模板、转换构造、友元与特化最后给出八个高频坑点。示例以 C17 为基准。一、类模板的基本骨架先看一个最小但完整的类模板// 适用于 C17#include cstddef#include stdexcept#include vectortemplate class Tclass Buffer {public:Buffer() default;explicit Buffer(std::size_t n) : data_(n) {}void push(T value) {data_.push_back(std::move(value));}std::size_t size() const { return data_.size(); }T operator[](std::size_t i) { return data_[i]; }const T operator[](std::size_t i) const { return data_[i]; }T at(std::size_t i) {if (i data_.size()) {throw std::out_of_range(Buffer::at);}return data_[i];}private:std::vectorT data_;};Buffer不是类型Bufferint才是。编译器在看到Bufferint时才用int替换T实例化出一份完整的类。不同的T得到的是彼此无关的类型Bufferint和Bufferdouble之间没有继承关系也不能互相赋值——除非你显式提供转换途径。成员函数默认在类内定义时是隐式inline的。类内定义写起来简单但当函数体很长时会把类声明撑得难以阅读这时就需要类外定义。类外定义两个 template 头普通类模板的成员在类外定义只需要一个模板头// 适用于 C17template class TT BufferT::operator[](std::size_t i) {return data_[i];}注意BufferT::这个限定类名后面必须带上模板参数。写成Buffer::operator[]是不合法的。二、成员函数模板第二层参数成员函数模板是在类模板内部再声明一个template。它可以是构造函数、普通成员也可以是运算符。// 适用于 C17#include cstddef#include type_traits#include utility#include vectortemplate class Tclass Buffer {public:Buffer() default;explicit Buffer(std::size_t n) : data_(n) {}// 成员模板从元素类型可转换的 BufferU 构造template class U,class std::enable_if_tstd::is_convertible_vconst U, TBuffer(const BufferU other): data_(other.data_.begin(), other.data_.end()) {}// 成员模板原地构造参数个数与类型不限template class... ArgsT emplace(Args... args) {data_.emplace_back(std::forwardArgs(args)...);return data_.back();}void push(T value) { data_.push_back(std::move(value)); }std::size_t size() const { return data_.size(); }private:template class U friend class Buffer; // 所有 BufferU 互为友元std::vectorT data_;};这里有三个关键点。1. 类外定义需要两个 template 头成员函数模板在类外定义时模板头要按「外层类、内层函数」的顺序各写一个// 适用于 C17template class T // 外层类模板的参数template class... Args // 内层成员模板自己的参数T BufferT::emplace(Args... args) {data_.emplace_back(std::forwardArgs(args)...);return data_.back();}只写一个template class T, class... Args是错的编译器会把Args当成类模板参数而类模板声明里并没有它。2. 转换构造函数为什么要友元BufferU和BufferT是不同的类。当U ! T时BufferT的构造函数访问other.data_属于访问另一个类的私有成员必须友元。template class friend class Buffer;一次性把所有实例化都设为友元。如果只写friend class Buffer;不带模板头在某些写法下只友元当前实例化跨类型访问仍会失败。3.enable_if_t约束转换范围std::is_convertible_vconst U, T保证只有当U能转换为T时这个构造函数才参与重载。否则Bufferstd::string从Bufferint构造会在函数体内部报错错误信息晦涩。把约束放在模板参数默认值里属于 SFINAE替换失败只是让该候选退出不会让整个编译失败。注意这个构造函数不是explicit所以允许Bufferdouble b Bufferint{...};这样的复制初始化。如果不想隐式转换加explicit。三、依赖名、typename 与 template 消歧类模板内部引用「依赖模板参数的名字」时编译器在解析模板定义阶段不知道它到底是什么需要你手动标注。依赖类型要加 typename// 适用于 C17#include vectortemplate class Containervoid dump_first(const Container c) {// Container::value_type 依赖模板参数必须加 typenametypename Container::value_type first *c.begin();(void)first;}如果Container是具体类型如std::vectorintstd::vectorint::value_type默认被当作类型但在模板里Container::value_type可能是静态成员、类型别名或别的什么编译器无法判断typename是必需的前缀。C20 起在某些明显是类型的上下文里可以省略typename但为了兼容性和可读性依赖类型前缀仍然建议写全。依赖模板要加 template// 适用于 C17template class Tvoid call_get(T obj) {// obj.get0() 中的 会被当成小于号需要 template 消歧auto v obj.template get0();(void)v;}当成员名依赖模板参数且后面紧跟时必须写.template或-template、::template否则编译器按「小于」解析。依赖基类的成员要加 this-// 适用于 C17template class Tstruct Base {void helper() {}int value 0;};template class Tstruct Derived : BaseT {void run() {// helper(); // 错两阶段查找找不到依赖基类的成员this-helper(); // 对this-value 1; // 对}};模板基类的成员在定义阶段不可见因为BaseT尚未确定。用this-把名字变成依赖名推迟到实例化时查找。这是模板继承最常见的编译错误来源之一。四、成员模板的重载与特化边界成员函数模板可以重载也可以与普通成员函数共存。重载解析时非模板函数通常优先于同签名的模板特化更精确的匹配规则仍然适用。关于特化要分清三件事操作是否允许说明全特化整个类模板允许template class Bufferbool { ... };偏特化整个类模板允许如BufferT*偏特化单个成员函数不允许只能通过重载或if constexpr分支全特化单个成员函数允许需类已实例化且特化在使用前声明特化成员函数模板允许全特化写法较繁琐慎用单个成员函数不能偏特化是很多人试图「只给BufferT*换一种push」时踩的坑。替代方案是给整个类写偏特化或者在函数内部用if constexpr按类型分支。还有一个顺序规则显式特化必须出现在第一次使用该实例化之前。把特化写在头文件末尾、而被其他头文件先包含使用程序就是 ill-formed且标准不要求编译器诊断——这类问题可能在某些编译单元「碰巧能过」换一种包含顺序就炸。五、完整示例一个可转换的缓冲区把上面的片段组装起来// 适用于 C17#include cstddef#include iostream#include string#include type_traits#include utility#include vectortemplate class Tclass Buffer {public:Buffer() default;explicit Buffer(std::size_t n) : data_(n) {}template class U,class std::enable_if_tstd::is_convertible_vconst U, TBuffer(const BufferU other): data_(other.data_.begin(), other.data_.end()) {}template class... ArgsT emplace(Args... args);void push(T value) { data_.push_back(std::move(value)); }std::size_t size() const { return data_.size(); }const T operator[](std::size_t i) const { return data_[i]; }private:template class U friend class Buffer;std::vectorT data_;};template class Ttemplate class... ArgsT BufferT::emplace(Args... args) {data_.emplace_back(std::forwardArgs(args)...);return data_.back();}int main() {Bufferint ints;ints.push(1);ints.push(2);Bufferlong longs ints; // 走转换构造函数Bufferstd::string names;names.emplace(3, x); // 原地构造 std::string(3, x)names.emplace(hello); // 原地构造 std::string(hello)std::cout longs.size() names.size() \n;return 0;}names.emplace(3, x)调用的是std::vectorstd::string::emplace_back(3, x)它把参数完美转发给std::string的构造函数string(size_type, char)。emplace(hello)则匹配string(const char*)。如果写成names.push(hello)会先构造一个临时std::string再移动进去多一次构造emplace直接在容器内构造。但emplace不是无条件更优它绕过了显式构造检查某些情况下会意外匹配到你不想要的构造函数例如接受initializer_list或接受指针的构造函数。对std::vector这类容器参数含义明确时push_back反而更安全。六、编译期检查与错误信息模板报错通常很长善用static_assert可以把错误提前并说清楚// 适用于 C17#include type_traitstemplate class Tclass NumericBuffer {static_assert(std::is_arithmetic_vT,NumericBuffer requires an arithmetic type);// ...};static_assert在实例化时求值条件必须是常量表达式。把约束写在类作用域顶部使用者一看就知道哪里不符合。另一个实用手段是[[nodiscard]]标注返回新对象的成员模板避免调用方忽略结果以及用 delete明确禁止某些实例化路径。常见坑点❌ 类外定义成员模板只写一个模板头cpp template class T, class... Args T BufferT::emplace(Args... args);✅ 外层类、内层函数各一个cpp template class T template class... Args T BufferT::emplace(Args... args);❌ 访问另一个实例化的私有成员却不加友元cpp template class U Buffer(const BufferU o) : data_(o.data_) {}✅ 类内声明template class friend class Buffer;或提供公开的访问接口。❌ 依赖类型前漏写typenamecpp Container::value_type v;✅typename Container::value_type v;❌ 依赖模板成员前漏写templatecpp obj.get0();✅obj.template get0();❌ 在模板派生类中直接调用依赖基类成员cpp template class T struct D : BT { void f() { helper(); } };✅this-helper();或BT::helper();❌ 试图偏特化单个成员函数cpp template class T void BufferT*::push(T* v); // 不合法✅ 偏特化整个类或用if constexpr/重载在函数内分支。❌ 显式特化写在首次使用之后cpp Bufferint b; // 先实例化 template class Bufferint; // 再特化ill-formed ✅ 特化声明放在所有使用点之前通常紧跟主模板。❌ 认为emplace永远优于pushcpp v.emplace_back(0); // 可能匹配到接受指针的构造函数✅ 参数语义明确时用push_back需要原地构造且参数无歧义时用emplace_back。总结概念写法要点常见错误类模板成员类外定义template class TBufferT::漏模板头或漏T成员函数模板类外定义两个template头合并成一个转换构造成员模板 SFINAE 约束无约束导致深坑报错跨实例化访问template class friend class以为自动可见依赖类型typename前缀漏写导致解析失败依赖模板template消歧被当作小于号依赖基类成员this-或限定名两阶段查找找不到成员特化只可全特化且在使用前试图偏特化成员类模板与成员模板的难点集中在「编译器在不知道T的情况下能解析什么」。凡是依赖T的名字都要显式告诉编译器它是不是类型、是不是模板凡是跨实例化访问都要显式授权凡是定义都要让使用点看得见。把这四条记牢大部分模板编译错误都能定位。
阅读完成 · 觉得有帮助?
咨询建站