尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

13. C++代码重用

13. C++代码重用 C代码重用包含对象成员的类组合Composition在类里面直接定义另一个类的对象作为成员变量。外部类只能通过成员的公有接口操作它无法直接访问私有成员。相比于继承属于低耦合。构造顺序先成员后自己。析构顺序先自己后成员。组合成员对象整体控制部件的生命周期构造时创建析构时销毁。聚合指针/引用整体使用部件但不负责部件的创建和销毁由外部管理。valarray 类valarray 是由头文件 valarray 支持的模板类template class专门处理数值或具有类似特性的类型支持对数组所有元素求和、找出最大最小元素等操作。因为是模板类声明对象时必须指定具体数据类型如 valarrayint、valarraydouble。其构造函数支持长度为零的空数组、指定长度的空数组、所有元素初始化为指定值的数组、用常规数组初始化的数组explicit 关键字防止单参数构造函数的隐式转换避免意外行为explicit Student(const std::string s) : name(s), scores() {}成员初始化顺序初始化列表含多个项目时成员被初始化的顺序是它们在类定义中被声明的顺序而不是它们在初始化列表中的书写顺序。私有继承私有继承中基类的公有成员和保护成员都成为派生类的私有成员。派生类继承实现但不继承接口。方式初始化语法示例包含使用成员名: name(str), scores(pd, n)私有继承使用类名: std::string(str), ArrayDb(pd, n)“无论什么继承基类子对象都是无名的。公有继承只是允许编译器自动把基类成员‘合并’到派生类的名字查找范围内。”“私有继承切断了这种自动合并迫使程序员必须手动使用 基类名:: 来访问那个原本就无名的基类子对象。”但对于组合中的成员对象天生是“有名”的因为必须写变量名。继承中的访问控制基类私有成员private无论何种继承方式public、protected、private派生类内部和外部永远无法直接访问。只能通过基类提供的公有public或保护protected成员函数间接操作。私有继承class Derived : private Base的效果把基类的 public 和 protected 成员全部降级为派生类的 private 成员。即派生类内部还能用因为降级后成了自己的私有成员但派生类的子类下一级和外部对象都访问不到了。私有继承中调用基类友元函数友元函数不属于类不能用类名显式限定函数名但可以通过显式转换为基类来调用正确的基类版本。例如 operator(ostream, const Student) 内部写 os (const string)stu;显式把 stu 转换为 string 引用从而调用 operator(ostream, const string)。引用不会自动转换。在私有继承中不进行显式类型转换就不能把指向派生类的引用或指针赋给基类引用或指针。规则限制即使使用公有继承也必须显式转换原因有二其一不转换时 os stu 会匹配友元函数自身原型导致递归调用其二多重继承下编译器无法确定应转换到哪个基类若两个基类都提供 operator。// 私有继承中调用基类友元显式转换为基类 std::ostream operator(std::ostream os, const Student stu) { os (const std::string)stu; // 显式转换后调用 string 的 operator return os; }使用 using 重新定义访问权限使用 using 声明类似命名空间中的用法指出派生类可以使用特定的基类成员即使采用的是私有派生。例如在 Student 的公有部分写 using std::valarraydouble::min; 与 using std::valarraydouble::max;使它们像 Student 的公有方法一样可用写 using std::valarraydouble::operator[]; 可使 const 与非 const 两个版本都可用从而删除手写的 operator[] 原型与定义。私有/保护派生把基类公有方法降级后外部无法调用若需要把这些方法重新暴露为公有除“包装成新方法”外using 声明是更简洁的机制。规则限制using 声明只使用成员名——不带圆括号、函数特征标和返回类型。 using 声明只适用于继承不适用于包含。 老式做法在派生类公有部分重新声明方法名如 std::valarraydouble::operator[];已被摒弃编译器支持时应改用 using 声明。// using 声明把私有继承来的基类方法重新暴露为公有 class Student : private std::valarraydouble { public: using std::valarraydouble::min; // 使 min 成为公有方法 using std::valarraydouble::max; // 使 max 成为公有方法 using std::valarraydouble::operator[]; // 使 const/非 const 两版可用 };对象切片Object Slicing什么是切片当把派生类对象赋值/拷贝/按值传递/按值返回给基类类型时编译器只拷贝基类的那部分成员派生类新增的成员变量被直接“切掉”丢弃。原对象会不会被截断。 切片发生时原对象derived完好无损。被截断的是新生成的副本比如赋值的目标变量 Base a或者函数形参。指针Base*和引用Base。它们只是“指向原对象不会产生新对象所以永远不会有数据丢失。什么时候发生切片赋值Base b derived;函数值传递void func(Base b); func(derived);函数值返回Base func() { Derived d; return d; }显式强转成值类型Base a static_castBase(derived);这会创建一个截断的临时对象。函数与虚表VTable非虚函数普通函数函数代码在代码段不占对象内存。切片后新对象调用的非虚函数由编译时类型决定即 Base 版本。重写函数和重定义函数都会被恢复到基类版本这不算“截断”只是作用域隐藏因为编译器只看指针/引用的类型。虚函数多态行为对象内存里虽没有函数代码但有一个虚表指针vptr。切片发生时新生成的 Base 对象其 vptr 会被重置为指向 Base 类的虚函数表。因此派生类的虚函数行为被截断了即使数据没丢但多态性丢失了。传递方式 (形参类型)实参传入方式调用的虚函数(func1)调用的普通函数(func2)基类指针void test(Base* p)test(derived)派生类版本动态绑定基类版本静态绑定基类引用void test(Base p)test(derived)派生类版本动态绑定基类版本静态绑定基类值传递void test(Base p)test(derived)基类版本切片重置 vptr基类版本类型降级赋值运算符Base b; b derived;将派生类赋给已有的基类对象基类版本b本身就是基类vptr 指向基类表基类版本类型降级基类指针 → 强转回派生类指针向下转型Derived* d static_castDerived*(b);必须确保b实际指向派生类派生类版本原对象的 vptr 未变派生类版本静态绑定看强转后的类型基类引用 → 强转回派生类引用向下转型Derived d static_castDerived(b);派生类版本原对象的 vptr 未变派生类版本静态绑定看强转后的类型基类对象-强转赋值给派生类对象未定义未定义dynamic_cast 运行时去查虚表vtable比较类名字符串有轻微性能开销。向下转型时使用dynamic_cast 才安全。基类中必须至少有一个虚函数通常是虚析构函数dynamic_cast 才能工作。多重继承MI多重继承Multiple InheritanceMI描述的是有多个直接基类的类。语法class SingingWaiter : public Waiter, public Singer { // ... };此时Waiter和 Singer都是公有继承。class SingingWaiter : public Waiter, Singer { // Singer 是私有继承 // ... };Waiter是公有继承, Singer是私有继承。默认是私有继承。如果 Waiter 和 Singer 都继承自 Worker则 SingingWaiter 包含两个 Worker 子对象class Worker { ... }; class Waiter : public Worker { ... }; class Singer : public Worker { ... }; class SingingWaiter : public Waiter, public Singer { ... };此时使用Worker对象会产生二义性。SingingWaiter ed; Worker * pw ed; // ❌ 二义性哪个 Worker 子对象此时通过显示类型转换可以解决Worker * pw1 (Waiter *) ed; // Waiter 中的 Worker Worker * pw2 (Singer *) ed; // Singer 中的 Worker两个 Worker 子对象导致多态性复杂化以及数据冗余姓名和ID各存两份。虚基类Virtual Base Class将Worker声明为Waiter和Singer的虚基类。class Waiter : virtual public Worker { ... }; class Singer : public virtual Worker { ... }; class SingingWaiter : public Waiter, public Singer { ... };除非调用虚基类默认构造函数否则要显式调用虚基类的构造函数。// ✅ 正确的构造函数 SingingWaiter(const Worker wk, int p 0, int v Singer::other) : Worker(wk), Waiter(wk, p), Singer(wk, v) { }如果 Waiter 和 Singer 都有 Show() 方法调用 SingingWaiter::Show() 会产生二义性解决方法1作用域解析newhire.Singer::Show(); // 明确使用 Singer 版本解决方法2在派生类中重新定义void SingingWaiter::Show() { Singer::Show(); // 只显示 Singer 部分 }混合虚/非虚继承的内存模型虚途径共享一份非虚途径各管各的。class B { /* ... */ }; ​ // C 和 D 通过 虚继承 派生自 B class C : virtual public B { /* ... */ }; class D : virtual public B { /* ... */ }; ​ // X 和 Y 通过 非虚普通继承 派生自 B class X : public B { /* ... */ }; class Y : public B { /* ... */ }; ​ // M 同时继承了这四条路径 class M : public C, public D, public X, public Y { /* ... */ };当创建 M 对象时内存中包含的 B 子对象个数为来自虚途径C 和 DC 保证虚基类在整个继承树中只有一份。因为 C 和 D 都是虚继承自 B所以它们共享同一个 B 子对象。 → 1 份来自非虚途径X 和 Y非虚继承各自独立。X 包含一个独立的 BY 包含另一个独立的 B。 → 2 份总数1 (虚共享) 2 (非虚独立) 3 个 B 子对象。虚继承下的支配规则Dominance Rule在虚继承中如果派生类较近的类中有同名的成员它会“遮蔽”基类中的同名成员即使派生类的这个成员是私有的它依然优先于基类的公有成员。但非派生类的兄弟类之间的同名会冲突二义性。class C { public: int omg(); }; class E { public: int omg(); }; // E 和 C 平级 class F : public C, public E { /*...*/ };在F中调用omg()报错二义性。class B { public: int val; }; // B 的 val 是公有 class C : virtual public B { private: int val; }; // C 的 val 是私有 class F : public C { /*...*/ };编译器先查支配规则C::val派生自B离F更近 →C::val支配B::val。编译器锁定C::val作为目标。编译器检查访问权限发现C::val是private。最终结果编译报错但报错是“C::val是私有的无法访问”而不是“存在二义性”类模板类模板是编译器用于生成类的编译期指令。它本身不产生符号表条目也不产生机器码。模板类示例// 模板定义必须放头文件 template typename T class Stack { T items[10]; public: Stack(); bool push(const T); }; ​ template typename T StackT::Stack() { /* 实现 */ } ​ // 使用必须显式指定类型 Stackint intStack; Stackstd::string strStack;语法规则类型参数声明template typename T 或 template class T。T 是占位符代表任意类型。在类定义内部T 可以直接用于声明成员变量、成员函数参数和返回值。注模板声明使用template class Type 或 template typename Type类限定符使用 StackType:: 而非 Stack::使用场景模板定义位置是否可行单文件所有代码在同一个.cpp任意位置调用前可见即可✅ 可行多文件声明与定义分离定义在.cpp声明在.h❌链接报错多文件共享标准做法完整定义放在.h中✅ 推荐多文件 显式实例化定义在.cpp并在末尾显式实例化✅ 可行但不灵活在类声明中定义的方法可省略模板前缀模板定义的位置编译期可见性核心原因C 编译器采用独立编译模型。编译 .cpp 时编译器只处理当前翻译单元。它必须在当前文件中看到完整的模板定义而不仅仅是声明才能针对具体类型如 int生成机器码。如果把定义放在 .cpp 中编译主文件时只看到声明链接器会因找不到 Stackint::push 的实现而报错未定义引用。实例化隐式实例化当代码中定义对象如 Stackint st;时发生。显式实例化在 .cpp 中写 template class Stackint;强制在当前位置生成代码用于减少编译时间或处理库封装但不推荐常规使用。类模板与函数模板的区别函数模板编译器可以根据实参类型自动推导模板参数如 max(1, 2) 推导为 int。类模板不能自动推导类型。必须在对象声明时用尖括号明确指定如Stackstd::string st;除非使用 C17 的类模板参数推导CTAD。模板实例化类模板被实例化时编译器只是将模板参数替换为具体类型然后编译生成的类。如果生成类型不支持代码中的操作则编译失败如果支持但语义不符合预期如指针只复制地址不复制对象则运行时逻辑错误。模板对赋值操作的要求类模板中常出现 item items[--top] 这样的语句。当 Type 是数组类型如 char[40]时赋值操作不合法数组不能作为左值被赋值编译失败。非类型模板参数与数组模板模板的主要动机之一是为容器类container class提供可复用代码类型参数type parameter非常适合把相同的存储方案复用到不同类型上。除了类型参数模板还能携带“非类型参数”用于在编译期把数组大小等常量并入类型本身。非类型表达式模板参数模板参数除了类型参数外还可以是非类型参数non-type parameter也称表达式参数expression parameter。它指定的不是泛型名而是一个具体的类型例如 template class T, int n 中T 是类型参数n 是类型为 int 的非类型参数。实例化instantiation时编译器用具体值替换 n从而在类内部直接声明定长数组。声明 ArrayTPdouble, 12 eggweights; 会让编译器定义出名为 ArrayTPdouble, 12 的具体类并以 double 替换 T、以 12 替换 n随后创建该类型的 eggweights 对象。C11 标准库的 array 容器正是采用这种方式在编译期指定数组大小。// 数组模板类型参数 T 非类型参数 n数组大小 template class T, int n // T 为类型参数n 为 int 型非类型参数 class ArrayTP { private: T ar[n]; // 用非类型参数 n 声明编译期定长的内置数组 public: explicit ArrayTP(const T v); // 构造函数以 v 作为每个元素的初值 virtual T operator[](int i); // 非 const 版本返回引用以便修改元素 virtual T operator[](int i) const; // const 版本只读访问 };把大小直接并入类型使数组的存储布局在编译期完全确定既不需要运行时内存管理也让“不同大小即不同类型”成为语言层面的保证。非类型参数的类型只能是整型、枚举、引用或指针。因此 double m 不合法而 double* rmdouble 型引用和 double* pmdouble 型指针合法。 模板代码不能修改非类型参数的值也不能取它的地址所以 n、n 这类表达式非法。 实例化时用作非类型实参的值必须是常量表达式constant expression即编译期可求值。栈内存方式与堆内存方式的差异改变数组大小有两种实现方式。非类型参数法直接以内置数组作为类成员内存来自为自动变量automatic variable维护的栈构造函数参数法如 Stack 模板则通过 new/delete 管理堆内存大小作为构造参数传入。原因栈内存由编译器自动分配与回收不存在堆分配的内存管理开销因此执行速度更快尤其在需要大量小型数组时差异明显。规则限制栈内存在作用域结束时自动销毁生命周期由作用域决定堆内存的分配与释放需显式管理多出 new/delete 的开销和出错风险每种数组大小生成一个独立模板类非类型实参属于类型的一部分因此下面的声明生成两个独立的类ArrayTPdouble, 12 eggweights; 与 ArrayTPdouble, 13 donuts; 对应 ArrayTPdouble, 12 和 ArrayTPdouble, 13 两个互不相干的类型。而构造函数方式只生成一个 Stackint 类数组大小以参数形式传给构造函数Stackint eggg(12); 与 Stackint dunkers(13); 只是同一类型的不同对象。原因编译器按模板实参组合逐一实例化非类型实参一旦不同生成的类就不同。规则限制使用很多不同尺寸会生成大量类造成代码体积膨胀使用构造函数方式则没有这一问题。构造函数方式更具通用性构造函数方式把数组大小作为类成员存储而不是硬编码进类型定义中。原因大小一旦成为普通成员就不再约束对象之间的赋值关系——可以把一种尺寸的数组对象赋给另一种尺寸的数组对象也能构造出数组大小在运行时可变的类。这是硬编码大小的非类型参数法无法做到的。规则限制ArrayTPdouble, 12 与 ArrayTPdouble, 13 属于不同类两者之间不能互相赋值构造函数方式则需要额外的内存管理代码来维护动态数组。模板的多功能性用于常规类的技术同样适用于模板类。模板类可以用作基类base class、组件类component class也可以用作其他模板的类型参数。例如一个模板可以继承另一个模板GrowArray 继承 Array一个模板可以持有另一个模板的实例作为成员Stack 内含 ArrayTP还可以写出“栈的数组”这种嵌套结构。原因模板之间可以相互组合、层层复用把可复用代码的粒度从“单个类”提升到“一类结构”这是容器类可组合性的基础。规则限制在 C98 中嵌套模板的相邻右尖括号之间必须至少有一个空格写成 Array Stackint 以免与右移运算符 混淆C11 起不再有此要求可直接写 ArrayStackint。template typename T class Array { // 通用数组模板 private: T entry; // 单个元素 public: T operator[](int i); // 元素访问声明 }; ​ template typename Type class GrowArray : public ArrayType { // 继承模板用作基类 // 扩展成员 }; ​ template typename TP class Stack { private: ArrayTP ar; // 组件模板实例用作成员 }; ​ Array Stackint asi; // 嵌套类型实参是另一个模板模板的递归使用构造多维数组模板可以递归使用——把一个模板的实例作为另一个模板实例的类型实参从而构造多维结构。ArrayTP ArrayTPint, 5, 10 twodee; 表示一个含 10 个元素、每个元素都是含 5 个 int 的数组与常规声明 int twodee10; 等价。原因为什么解决了“用模板表达多维数组”的问题使任意维度都可声明为编译期定长的模板类型。规则限制模板语法中维度的顺序与等价的常规多维数组相反——外层实参10对应第二维内层实参5对应第一维即ArrayTPArrayTPint,5,10对应int[10][5]而非int[5][10]。// 递归使用模板外层实参是内层模板的实例 ArrayTP ArrayTPint, 5, 10 twodee; // 10 个元素每个是含 5 个 int 的数组 // 等价于int twodee[10][5];维度顺序相反 twodee[i][j] value; // 与二维数组相同的下标访问方式多个类型参数模板可以包含多个类型参数让一个类同时保存两种不同类型的值。典型例子是 PairT1, T2 模板标准模板库提供了类似的 pair。原因解决“一个类需要同时存储两种不同类型”的场景例如把“名称”与“数值”成对保存。规则限制实例化后的类名是完整实参组合 PairT1, T2而不是 PairPairstring, int 与 Pairchar*, double 是彼此完全不同的类。返回数据成员引用的 first()、second() 既能读取也能通过赋值修改值。// 多类型参数模板Pair 保存两个不同类型的值 template class T1, class T2 class Pair { private: T1 a; // 第一个值 T2 b; // 第二个值 public: Pair(const T1 aval, const T2 bval) : a(aval), b(bval) {} T1 first() { return a; } // 返回引用可读取也可修改 T2 second() { return b; } T1 first() const { return a; } // const 版本只读 T2 second() const { return b; } };默认模板参数可以为类模板的类型参数提供默认值例如 template class T1, class T2 int class Topo {...};。实例化时若省略该实参编译器使用默认类型Topodouble m2; 中 T1 为 double、T2 取默认值 int。原因减少实例化时反复书写常用类型的重复劳动标准模板库常把默认类型设为某个类简化用户代码。规则限制默认值按“省略则取默认”生效Topodouble, double m1; 显式给出两个类型时默认值不起作用。非类型参数也可以提供默认值这对类模板和函数模板都适用函数模板的类型参数默认值在 C98 中不允许C11 起才允许提供。// 默认类型模板参数省略 T2 时使用 int template class T1, class T2 int class Topo { private: T1 first; // 第一个成员 T2 second; // 第二个成员默认 int public: void set(const T1 f, const T2 s); // 成员函数声明 }; ​ Topodouble, double m1; // 显式给出两个类型 Topodouble m2; // 省略 T2使用默认值 int模板的具体化specialization概述模板以泛型generic方式描述类具体化则是用具体类型生成类声明的过程。类模板与函数模板一样存在隐式实例化implicit instantiation、显式实例化explicit instantiation和显式具体化explicit specialization它们统称为具体化。原因同一份泛型代码需要按不同实参生成不同版本具体化机制决定了“何时生成”以及“生成哪个版本”。规则限制四种形式的选择遵循优先级规则——泛型模板是兜底具体化程度越高越优先隐式实例化声明一个或多个对象并指明所需类型编译器按通用模板提供的“处方”生成具体的类定义。例如 ArrayTPint, 100 stuff; 即触发隐式实例化。原因为什么编译器按“用到了才生成”的原则工作避免生成无用代码。规则限制编译器在需要对象之前不会生成隐式实例化。仅声明指针不会触发类定义生成——ArrayTPdouble, 30* pt; 只是声明指针pt new ArrayTPdouble, 30; 因需要对象此刻才生成类定义并创建对象。ArrayTPint, 100 stuff; // 隐式实例化生成类定义并创建对象 ArrayTPdouble, 30* pt; // 仅声明指针暂不生成类定义 pt new ArrayTPdouble, 30; // 需要对象此刻才生成类定义显式实例化使用关键字 template 并指明所需类型来声明类编译器直接生成类声明包括方法定义即使没有创建或提及任何类对象。原因主动强制生成指定版本可用于控制类定义在何处、何时生成便于跨编译单元管理代码生成。规则限制该声明必须位于模板定义所在的命名空间中与隐式实例化一样显式实例化仍依据通用模板生成具体化。// 显式具体化为 const char* 提供专用定义 template class SortedArrayconst char* { // 实现中使用 strcmp() 而非 比较字符串 }; ​ SortedArrayint scores; // 使用通用模板 SortedArrayconst char* dates; // 使用显式具体化版本部分具体化部分具体化partial specialization是部分限制模板的通用性——例如给多个类型参数之一指定具体类型template class T1 class PairT1, int { ... };。template 后尖括号内声明的是未被具体化的参数若所有类型参数都被指定尖括号内为空即退化为显式具体化。也可为指针实参提供专用版本template class T class FeebT* { ... };。原因针对“某一类特殊情形”某参数为 int、或实参是指针等提供不同实现同时其余参数仍保持泛型兼顾定制与复用。规则限制有多个模板可供选择时编译器使用具体化程度最高的版本通用模板 部分具体化 显式具体化。 指针部分具体化只在实参是指针时命中Feebchar 用通用版Feebchar 用指针专用版若未提供指针具体化后者也会按通用模板处理。 可以对多个参数组合施加限制如 TrioT1, T2, T2第三个参数固定为第二个的类型、TrioT1, T1, T1*后两个参数固定为第一个的指针。// 部分具体化T2 固定为 intT1 保持泛型 template class T1 class PairT1, int { // 为第二参数为 int 的情形定制实现 // ... }; ​ // 指针部分具体化实参是指针时命中 template class T class FeebT* { // 指针专用实现 // ... }; ​ Pairdouble, double p1; // 使用通用模板 Pairdouble, int p2; // 使用部分具体化版本 Feebchar fb1; // 使用通用版本 Feebchar* fb2; // 使用指针部分具体化版本成员模板模板可以用作结构、类或模板类的成员这样的模板成员称为成员模板member template。一个模板类可以把另一个嵌套模板类和模板成员函数都作为自己的成员。完全实现 STL 的设计必须使用这项特性。例如外层模板 beta 在私有部分声明了嵌套模板类 hold并用它声明两个数据成员 holdT q; 和 holdint n;——前者基于外层模板参数 T后者基于固定类型 int同时声明了模板成员函数 blab。原因成员模板让“外层类的类型参数”与“内部方法/成员的类型参数”相互独立T 由对象实例化时的实参决定而成员模板方法 blab 的 U 由该方法被调用时的实参决定。betadouble guy(3.5, 3); 使 T 为 double、q 为 holddoubleguy.blab(10, 2.3); 则因第一个实参 10 把 U 确定为 int。规则限制嵌套模板类声明在私有部分时只能在宿主类内部访问。 返回值类型是 U 而非计算结果的类型guy.blab(10, 2.3) 中混合类型使计算以 double 进行但返回类型 U 为 int结果被截断为 28改用 guy.blab(10.0, 2.3) 后 U 为 double输出 28.2608。 第二个参数的类型由对象声明时的 T 决定不由函数调用决定guy.blab(10, 3); 仍实例化为 blab(int, double)并按常规函数原型规则把 3 转换为 double。// 成员模板外层模板 beta 包含嵌套模板类 hold 与模板方法 blab template typename T class beta { private: template typename V // 嵌套成员模板类 class hold { V val; public: hold(V v) : val(v) {} V value() const { return val; } }; holdT q; // 基于外层参数 T 的成员 holdint n; // 基于固定类型 int 的成员 public: template typename U // 成员模板方法声明 U blab(U u, T t); // 返回类型由调用实参决定 };模板用作模板参数template template parameter模板参数本身可以是一个模板称为模板模板参数template template parameter。语法为 template template typename T class Thing class Crab其中 template typename T class 是参数的类型Thing 是参数名。实例化 CrabStack nebula; 时实参 Stack 必须是一个模板类且其声明与 Thing 的声明匹配如 template typename T class Stack。Crab 内部的 Thingint s1;、Thingdouble s2; 会被分别实例化为 Stackint 与 Stackdouble。模板新增的这项特性用于实现 STL——让一个模板类能接收另一个模板类作为参数再基于该模板类实例化自己的成员。规则限制宿主模板会对参数模板的接口做假设。例如 Crab 要求 Thing 表示的类具备 push()、pop() 方法且接口匹配任何声明匹配且具备这些成员的模板类都可用作实参。若不满足假设编译将失败。// 模板模板参数Thing 本身是模板类参数 template template typename T class Thing class Crab { private: Thingint s1; // 实参替换 Thing 后实例化 Thingdouble s2; public: bool push(int a, double x) { // 假设 Thing 提供 push 方法 return s1.push(a) s2.push(x); } bool pop(int a, double x) { // 假设 Thing 提供 pop 方法 return s1.pop(a) s2.pop(x); } };模板参数与常规类型参数混合模板模板参数可以与普通类型参数混合使用。template template typename T class Thing, typename U, typename V class Crab 中Thing 是模板参数U、V 是普通类型参数成员声明为 ThingU s1;、ThingV s2;其存储的数据类型由 U、V 决定而不是硬编码。把成员的存储类型也参数化使模板模板参数与类型参数各司其职——前者决定容器结构后者决定元素类型。规则限制实例化时必须同时给出模板实参与类型实参如 CrabStack, int, double nebula; 对应 ThingStack、Uint、Vdouble。// 模板模板参数与类型参数混合使用 template template typename T class Thing, typename U, typename V class Crab { private: ThingU s1; // 成员类型由类型参数 U 决定 ThingV s2; // 成员类型由类型参数 V 决定 public: bool push(U a, V x) { // 接口类型对应 U、V return s1.push(a) s2.push(x); } };模板类友元的三种类型模板类声明也可以有友元模板的友元分三类非模板友元non-template friend一个普通函数是所有模板实例化的友元约束模板友元bound template friend友元的具体化类型取决于类被实例化时的类型类的每个具体化只获得与之匹配的函数具体化非约束模板友元unbound template friend友元模板的所有具体化都是类的每一个具体化的友元。 原因为什么三类友元对应三种访问控制粒度——不加区分地授予全部实例、只授予同型实例、或跨类型自由授予满足不同的“谁能访问私有成员”需求。规则限制三类友元的声明语法、实例化时机和匹配方式各不相同详见后续知识点。非模板友元函数在模板类中把一个常规函数声明为友元如 friend void counts();该声明使 counts() 成为模板所有实例化的友元——它既是 HasFriendint 的友元也是 HasFriendstring 的友元。该函数不是成员函数也没有对象参数访问对象的方式包括访问全局对象、用全局指针访问非全局对象、创建自己的对象、访问独立于对象的模板类静态数据成员每个具体化拥有各自独立的静态成员。某些操作如统计所有实例的个数与具体对象无关只需访问静态成员或全局状态用一个共享的非模板友元即可完成。规则限制不能用模板类名本身作参数friend void report(HasFriend); 不合法因为不存在 HasFriend 这样的类型只有具体化提供模板类参数必须指明具体化写为 friend void report(HasFriendT);。 report 本身不是模板函数只是使用模板作参数因此必须为要用到的每个具体化单独定义重载版本显式具体化。// 非模板友元一个函数是所有实例化的友元 template typename T class HasFriend { private: T item; // 数据成员 static int ct; // 静态成员每个具体化独立一份 public: HasFriend(const T i) : item(i) { ct; } ~HasFriend() { ct--; } friend void counts(); // 非模板友元所有实例化的友元 friend void report(HasFriendT); // 带模板具体化参数的友元声明 }; ​ template typename T int HasFriendT::ct 0; // 静态成员定义约束模板友元函数让友元函数本身成为模板并使类的每一个具体化都获得与它匹配的函数具体化——HasFriendTint 获得 int 版本的函数友元依此类推。需要三个步骤在类定义之前声明模板函数原型如 template typename T void counts(); 与 template typename T void report(T);在类内部按类模板参数再次声明模板为友元指明具体化为友元提供模板定义。当友元函数需要访问具体化实例的私有成员且不同具体化的处理方式可能不同如统计每个具体化各自的静态计数时让函数模板按类的类型参数生成专属版本。规则限制声明中的 表示这是模板具体化friend void countsTT(); 必须用显式模板参数语法 TT 指明具体化因为 counts 没有可推导具体化的函数参数。 friend void report(HasFriendTTT); 中 可以为空因为编译器可从函数参数 HasFriendTTT 推断出模板类型参数也可显式写为 reportHasFriendTTT(HasFriendTTT)。 调用无参数的具体化必须显式给出模板实参如 countsint()有参数的则可从实参推导如 report(hfi1)。 每种 T 类型都有自己独立的友元函数具体化表现为各自报告不同的 sizeof。// 步骤1类定义前声明模板函数原型 template typename T void counts(); template typename T void report(T); ​ // 步骤2在类内按类型参数声明具体化为友元 template typename TT class HasFriendT { private: TT item; // 数据成员 static int ct; public: HasFriendT(const TT i) : item(i) { ct; } ~HasFriendT() { ct--; } friend void countsTT(); // 无参数必须用 TT 指明具体化 friend void report(HasFriendTTT); // 可从参数推导具体化 }; ​ // 步骤3为友元提供模板定义 template typename T void report(T hf) { // 可访问 hf 的私有成员 item // 使用 hf.item }非约束模板友元函数在类内部声明模板可创建非约束友元函数——每个函数具体化都是每个类具体化的友元。非约束友元的模板类型参数与模板类的类型参数相互独立例如 ManyFriendT 中声明 template typename C, typename D friend void show2(C, D);show2 的 C、D 与类的 T 无关。当某个函数需要同时访问两个不同具体化的私有成员时如打印一个 ManyFriendint 和一个 ManyFrienddouble 的元素约束友元无法胜任——它只能匹配同型对象非约束友元因对所有具体化开放而可以跨类型访问。规则限制show2(hfi1, hfi2) 匹配具体化 show2ManyFriendint, ManyFriendint作为所有 ManyFriend 具体化的友元它有权访问所有具体化的 item 成员但实际只访问所传对象。 show2(hfdb, hfi2) 匹配 show2ManyFrienddouble, ManyFriendint可同时访问 ManyFrienddouble 与 ManyFriendint 的 item 成员。 非约束友元放宽了访问范围跨类型代价是任何具体化组合都能被授权访问需权衡封装性。// 非约束模板友元函数类型参数与类类型参数相互独立 template typename T class ManyFriend { private: T item; // 数据成员 public: ManyFriend(const T i) : item(i) {} // 模板声明在类内部每个函数具体化是每个类具体化的友元 template typename C, typename D friend void show2(C c, D d); // C、D 与类的 T 无关 }; ​ template typename C, typename D void show2(C c, D d) { // 可访问任意 ManyFriend 具体化的 item // 使用 c.item 与 d.item }总结知识点描述模板友元三类非模板友元全实例共享、约束模板友元同型匹配、非约束模板友元全部具体化互相开放。非模板友元常规函数成为所有实例化的友元参数须用具体化类型HasFriendT不能写HasFriend须为每个用到的具体化提供重载定义。约束模板友元三步先声明模板原型、类内用TT指明具体化声明友元、再提供模板定义无参函数须显式指定具体化有参函数可推导。非约束模板友元在类内部声明模板友元函数类型参数与类参数独立每个函数具体化是每个类具体化的友元可跨类型访问私有成员。
返回列表