effective c++ 笔记 条款41-46

条款41:了解隐式接口和编译器多态

显式接口和运行期多态: 面向对象的世界总是以显式接口和运行期多态解决问题
显式接口的构成: 函数名称,参数类型,返回类型,常量性也包括编译器产生的copy构造函数和copy assignment 操作符。( 函数的签名式 )
多态是通过虚函数发生于运行期

class Widget {
public:
    virtual void Normalize()=0;
};
class AWidget:public Widget {
public:
    virtual void Normalize() override {...}
};
class BWidget:public Widget {
public:
    virtual void Normalize() override {...}
};
void doProcessing(Widget& w) {
    w.Normalize();
}

由于doProcessing的w被声明为Widget,所以w必须支持Widget接口,可以在源码中找到接口,实际可见,是显示接口。
隐式接口和编译期多态: templates及泛型编程的世界,与面向对象的世界有根本的不同。在此世界显式接口和运行期多态仍然存在,但重要性降低
隐式接口的构成: 有效表达式
多态是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期

template<typename T>
void doProcessing(T& w) {
    if (w.size() > 10 && w != someNastyWidget) {
    ...

对w而言,必须支持什么接口,由doProcessing中的操作决定。上述例子中需要支持
提供一个名为size的成员函数,该函数的返回值可与int(10 的类型)执行operator>,或经过隐式转换后可执行operator>。
必须支持一个operator!=函数,接受T类型和someNastyWidget的类型,或其隐式转换后得到的类型。

条款42:了解typename的双重意义

声明template参数时,前缀关键字class和typename可互换
模板内出现的名称如果相依于某个模板参数,我们称之为从属名称(dependent names);如果从属名称在类内呈嵌套状,我们称之为嵌套从属名称(nested dependent name);如果一个名称并不倚赖任何模板参数的名称,我们称之为非从属名称(non-dependent names)
使用关键字typename标识嵌套从属类型名称
但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符

template<typename T>
void print(const T& container)
{
    T::const_iterator* m;
    ...
}

如果T::const_iterator它不是一个类型,而是T中的一个静态成员变量,而这时m碰巧也是一个全局变量。那么就不是定义一个指针,而是两个静态变量相乘。
因此C++ 默认假设嵌套从属名称不是类型名称
因此下面这段代码会编译报错

template<typename C>
void Print2nd(const C& container) {
    if (container.size() >= 2) {
        C::const_iterator iter(container.begin());
    }
}

因为C::const_iterator是一个指向某类型的嵌套从属类型名称,可能导致解析困难,因为在编译器知道C是什么之前,没有任何办法知道C::const_iterator是否为一个类型
需要显示指明

typename C::const_iterator iter(container.begin());

typename只被用来验明嵌套从属类型名称

template<typename C>                 //允许使用"typename"(或"class")
void f(const C&container,           //不允许使用"typename"
typename C::iterator iter)//一定要使用"typename"
template<typename T>
class Derived : public Base<T>::Nested {    // 基类列表中不允许使用 typename
public:
    explicit Derived(int x)
        : Base<T>::Nested(x) {                 // 成员初始化列表中不允许使用 typename
        typename Base<T>::Nested temp;
        ...
    }
    ...
};

当类型名过于复杂,可以使用using或typedef来进行简化

using value_type = typename std::iterator_traits<IterT>::value_type;

条款43:学习处理模板化基类内的名称

class MsgInfo { ... };
template<typename Company>
class MsgSender {
public:
    void SendClear(const MsgInfo& info) { ... }
    ...
};
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    void SendClearMsg(const MsgInfo& info) {
        SendClear(info);        // 调用基类函数,这段代码无法通过编译,SendClear找不到标识符
    }
    ...
};

如果父类是模板类,那么子类直接调用父类的成员函数无法通过编译器,因为可能会有特化版的模板类针对某个类不声明该接口函数,编译器并不知道究竟继承哪个类( T 是一个 template 参数,不到后来的具现化,是无法确切知道它是什么),即无法知道是否有SendClear函数。
如特化版 class Company (模板全特化)
template<>为特化模板标志

template<>
class MsgSender <CompanyZ>{
public:
    //void SendClear(const MsgInfo& info) { ... }
    ...
};

三种解决办法

  1. 在基类函数调用动作之前加上this->:
this->SendClear(info);  告诉编译器,假设SendClear被继承
  1. 使用 using 声明式。告诉编译器进入 base class 作用域寻找函数
using MsgSender<Company>::SendClear;
SendClear(info);

条款33不同,这里并不是base class名称被derived class名称遮掩,而是编译器不进入base class作用域内查找,于是我们通过using告诉它,请它那么做

  1. 明确指出被调用函数位于 base class 内
MsgSender<Company>::SendClear(info);

不推荐,因为如果调用的是虚函数,明确资格修饰(explicit qualification)会使“虚函数绑定行为”失效

条款44:将与参数无关的代码抽离

template class 成员依赖 template 参数值,会造成造成代码膨胀,即non-type template parameters(非类型模板参数)带来的膨胀

template <typename T, std::size_t n>
class SquareMatrix {
  public:
    void invert();
  private:
    std::array<T, n * n> data;
};
SquareMatrix<double, 5> m1;
SquareMatrix<double, 10> m2;

会具有两份几乎一样的代码,除了一个参数5,一个参数10
改进

template<typename T>
class SquareMatrixBase {
protected:
    void Invert(std::size_t matrixSize);
    ...
private:
    std::array<T, n * n> data;
};

template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {  // private 继承实现,见条款 39
    using SquareMatrixBase<T>::Invert;              // 避免掩盖基类函数,见条款 33
public:
    void Invert() { this->Invert(n); }              // 调用模板基类函数,见条款 43
    ...
};

可能不止invert函数会使用矩阵操作,每次传尺寸过于繁琐,可以考虑将数据放在子类中,在父类存储指针和矩阵尺寸

template<typename T>
class SquareMatrixBase {
protected:
    SquareMatrixBase(std::size_t n, T* pMem)
        : size(n), pData(pMem) {}
    void SetDataPtr(T* ptr) { pData = ptr; }
    ...
private:
    std::size_t size;
    T* pData;
};

template<typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
public:
    SquareMatrix() : SquareMatrixBase<T>(n, data.data()) {}
    ...
private:
    std::array<T, n * n> data;
};

type parameters (类型参数)的膨胀:
很多平台上,int和long有相同的二进制表述,所以vector< int>和vector< long>的成员函数可能完全相同。
多数平台上,所有指针类型都有相同的二进制表述,因此凡模板持有指针者(比如list< int>、list< const int >等)往往应该对每一个成员使用唯一一份底层实现。
也就是说,如果你实现成员函数而它们操作强类型指针(T),你应该令它们调用另一个无类型指针(void )的函数,由后者完成实际工作。

  1. Templates 生成多个 classes 和多个 functions,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
  2. 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数或 class 成员变量替换 template 参数。
  3. 因类型参数(type parameters)而造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。

条款45:运用成员函数模板接受所有兼容类型

原始指针支持隐式转换,如子类指针可以隐式转化为父类指针,非const指针可以转为const指针。
对于template具现的类,无法像原始指针一样隐式转化。因为不存在子类-父类一样的关系。是完全不同的类。唯一的方式是明确编写模板拷贝构造函数

template<typename T>
class SmartPtr {
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)
        : heldPtr(other.get()) { ... }
    T* get() const { return heldPtr; }
    ...
private:
    T* heldPtr;
};

使用get获取原始指针,用原始指针进行类型转换,如果原始指针之间不能隐式转换,那么其对应的智能指针之间的隐式转换会造成编译错误。
模板构造函数并不会阻止编译器暗自生成默认的构造函数,所以如果你想要控制拷贝构造的方方面面,你必须同时声明泛化拷贝构造函数和普通拷贝构造函数,相同规则也适用于赋值运算符

template<typename T>
class shared_ptr {
public:
    shared_ptr(shared_ptr const& r);                // 拷贝构造函数

    template<typename Y>
    shared_ptr(shared_ptr<Y> const& r);             // 泛化拷贝构造函数

    shared_ptr& operator=(shared_ptr const& r);     // 拷贝赋值运算符

    template<typename Y>
    shared_ptr& operator=(shared_ptr<Y> const& r);  // 泛化拷贝赋值运算符
};

条款46:需要类型转换时请为模板定义非成员函数

条款24讨论过为什么唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”
模版类中的模板函数不支持隐式类型转换,如果在调用时传了一个其他类型的变量,编译器无法帮你做类型转换,从而报错

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
   return Rational<T>(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;     // 无法通过编译!

以oneHalf推导出Rational类型可行,但是将int类型隐式转换为Rational是会失败。2是一个int,编译器无法从int推算出Rational的T是什么类型
由于模板类不依赖模板实参推导,所以编译器能够在Rational具现化时得知T,因此可以使用友元声明式在模板类内指涉特定函数

template<typename T>
class Rational {
public:
    friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

当对象oneHalf被声明为一个Rational时,Rational类也被具现化出来,友元函数operator*也被声明出来,作为一个普通函数,可以在接受参数时隐时转换,但是此时类外部并没有实际定义,类外函数模版仍不知道,会链接失败
因此直接将定义合并至声明处

friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}

类内的函数会暗自成为内联函数,因此可以使operator*调用类外的辅助模板函数:

template<typename T> class Rational;

template<typename T>
const Rational<T> DoMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {
    return Rational<T>(lhs.Numerator() * rhs.Numerator(), lhs.Denominator() * rhs.Denominator());
}
template<typename T>
class Rational {
public:
    ...
    friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
        return DoMultiply(lhs, rhs);
    }
    ...
};

当我们编写一个 class template,而它所提供之与此 template 相关的函数支持所有参数隐式类型转换时,请将那些函数定义为 class template 内部的 friend 函数。

条款47:请使用traits classes表现类型信息

条款48:认识 template 元编程

没看懂,之后去研究下traits和模版

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值