Effective C++ 改善程序与设计的55个做法,总结笔记(上):
https://blog.csdn.net/afei__/article/details/83478161
继承是 is-a 关系,指 “是一个”,即父类的每条属性和方法都应该适用于子类。
using
声明或使用 转交函数(forwarding functions)。如:class Base {
public:
void fun1() {
std::cout << "Base fun1" << std::endl;
}
};
class Derived : public Base {
public:
// 1. 使用 using 声明
using Base::fun1;
// 2. 使用转交函数
void fun1() {
Base::fun1();
}
// 同名函数会遮掩掉父类的 fun1()
void fun1(int a) {
std::cout << "Derived fun1: " << a << std::endl;
}
};
当我们为了解决问题而寻找某个设计方法时,可以考虑一些 virtual 函数的替代方案,如:
tr1::function
。任何情况下都不该重新定义一个继承而来的 non-virtual 函数。
// 下面两种写法对编译器来说完全相同
template<class T> class Widget;
template<typename T> class Widget;
这一点我们慢点说,首先解释 “使用 template 标识嵌套从属类型名称”,看下面这个例子:
template<typename T>
void print(const T& container) {
// 这样是不行的
// T::const_iterator iter(container.begin());
// 必须明确告诉 C++ 这个 T::const_iterator 是个类型而不是变量
typename T::const_iterator iter(container.begin());
}
也许我们初衷是获取到集合的迭代器,但是由于泛型 T 可能并不是一个集合,它里面没有一个叫做 const_iterator 类型的嵌套类,编译器也可以把 const_iterator 当作一个类的静态变量,所以这里会存在歧义。因此我们必须使用 typename 告诉 C++ 这是一个类型而不是静态变量。
typename 被用来标识一个嵌套从属类型名称,然而还有一个例外,即不能在基类列或成员初始列内以它作为基类的修饰符。如:
// Base 是一个基类,Nested 是基类的一个嵌套类
template<typename T>
class Derive
: public Base<T>::Nested { // base class list 中不允许 typename
public:
explicit Derive(int x)
: Base<T>::Nested(x) // member initialization list 中不允许 typename
{
typename Base<T>::Nested temp; // 其余情况需要加上 typename
}
};
编译器往往会拒绝在模板化基类中寻找继承而来的名称,因为基类的模板化可能被特化而那个特化版本也许并不会提供某一接口。所以我们可以使用 “this->” 指定,或使用 “using” 告诉编译器假设它存在。
template<typename T>
class Derived : public Base {
public:
// 方式1,告诉编译器 func 位于基类中
using Base<T>::func();
void test() {
// 方式2,假设 func 将被继承
this->func();
}
};
Templates 会生成多个 classes 和函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
// 子类 Y 的智能指针指向父类 T 时就不会出问题了
template<class T>
class shared_ptr {
public:
// copy 构造函数
shared_ptr(shared_ptr const& rhs);
// 泛化 copy 构造函数
template<class Y>
shared_ptr(shared_ptr<Y> const& rhs);
// copy assignment
shared_ptr& operator=(shared_ptr const& rhs);
// 泛化 copy assignment
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& rhs);
};
为了让类型转换可能发生于所有实参上,我们需要一个非成员函数(条款 24)。在模板中,为了令这个函数被自动具现化,我们需要将它声明在类内部。为了在类内部声明非成员函数,唯一的办法是令它成为一个 friend。如:
template<typename T>
class Rational {
public:
// 定义在class内的函数都暗自inline,对于复杂函数而言,我们可以令friend函数调用一个辅助函数避免
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Rational(int numerator = 0, int denominator = 1)
: mNumerator(numerator), mDenominator(denominator) {}
int numerator() const { return mNumerator; }
int denominator() const { return mDenominator; }
private:
int mNumerator; // 分子
int mDenominator; // 分母
};
int main() {
Rational<int> oneFourth(1, 4);
Rational<int> result;
result = 2 * oneFourth; // 如果不是 non-member 的形式将不支持这种写法
cout << "numerator: " << result.numerator() << endl;
cout << "denominator: " << result.denominator() << endl;
}
例子:
以 iterator_traits 为例介绍如何实现和使用 traits classes。STL 提供了很多的容器、迭代器和算法,其中的 advance 便是一个通用的算法,可以让一个迭代器移动给定距离:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // d < 0 就向后移动
STL 迭代器回顾:
struct input_iterator_tag {}; // 输入迭代器,只能向前移动
struct output_iterator_tag {}; // 输出迭代器,只能向前移动
struct forward_iterator_tag: public input_iterator_tag {}; // 稍强的是前向迭代器,可以多次读写它的当前位置
struct bidirectional_iterator_tag: public forward_iterator_tag {}; // 双向迭代器,支持前后移动
struct random_access_iterator_tag: public bidirectional_iterator_tag {}; // 随机访问迭代器,可以支持 +=, -= 等移动操作
回到 advance 上,它的实现取决于 Iter 类型:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
// 针对 random access 迭代器使用这种方式实现
if (iter is a random access iterator) {
iter += d;
} else {
// 其它迭代器使用 ++ 或 -- 实现
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}
接下来就是怎么判断 Iter 的类型是否是 random access 迭代器了,也就是需要知道它的类型。这真是需要使用到 Traits classes 的地方。
实现 Traits classes:
用户自定义类型:
template<typename IterT>
struct iterator_traits {
// 类型 IterT 的 iterator_category 就是用来标识迭代器的类别
typedef typename IterT::iterator_category iterator_category;
};
指针类型:
指针本身就是可以支持随机访问(random access)的,所以我们对指针类型提供一个偏特化版本即可:
template<typename IterT> // template偏特化
struct iterator_traits<IterT*> { // 针对内置指针
typedef random_access_iterator_tag iterator_category;
};
advance 实现:
不好的实现方式:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag))
// ...
}
IterT
和 iterator_traits
都是可以在编译期间确定的,而 if 判断却要在运行期间核定,这样不仅浪费时间,也会导致代码膨胀。
建议做法是建立一组重载函数(doAdvance),接受不同的类型,原函数(advance)调用这些重载函数。
// 原函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
// 调用不同版本的重载函数
doAdvance(iter, d, typename std::iterator_traits<IterT>::iterator_category());
}
// 下面是一系列重载函数
// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
iter += d;
}
// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
if (d < 0 ) {
throw std::out_of_range("Negative distance");
}
while (d--) ++iter;
}
set_new_handler
允许客户指定一个函数,在内存分配无法获得满足时被调用。new (std::nothrow)
是一个颇为局限的工具,它只适用于内存分配,后续的构造函数调用还是可能抛出异常。让更多内存可以使用。既然内存不够那么我们就想办法弄到更大的内存。
安装另一个 new-handle。例如本 new-handler 无法取得更多内存,那么就找一个可以取得更多内存的 new-handler 替代自己(只要调用 set_new_handler
即可)。
卸除 new-handle。即将 null 传给 set_new_handler
,然后 new 操作会在内存分配失败后抛出异常。
抛出 bad_alloc 异常。
不返回。通常调用 abort 或者 exit。
了解 new 和 delete 的合理替换时机
有许多理由需要写个自定义 new 和 delete。如:
检测运行错误。
收集和统计 heap 使用信息。
改善性能。例如定制化的分配器来增加分配和归还速度,减少内存管理器带来的额外开销,弥补分配器中的非最佳对齐位等。
编写 new 和 delete 时需固守常规
operator_new 应该内含一个无限循环,并在其中尝试分配内存,如果无法满足内存需求,就该调用 new-handle。它也应该有能力处理 0 bytes 申请,还应该能处理 “比正确大小更大的(错误)申请” 。
operator_delete 应该在收到 null 指针时不做任何事。同样也应该能处理 “比正确大小更大的(错误)申请” 。
写了 placement new 也要写 placement delete
当 operator new 接受的参数除了那个 size_t 外还有其它,这称之为 placement new。placement delete 同理。
当你写一个 placement operator new 时,也务必写出对应的 placement operator delete。否则可能发生内存泄漏。
当你声明 placement new 和 placement delete,请确定不要无意识地遮掩了它们的正常版本。
请严肃对待编译器的警告,努力在编译器最高警告级别下争取 “无警告”。
std::tr1::
命名空间下,以下是组件实例:tr1::function
,常用于实现回调函数。tr1::bind
,能够做 STL 绑定器 bind1st 和 bind2nd 所做的每一件事,而又更多。tr1::tuple
可持有任意个数对象。tr1::array
,本质是个 STL 化的数组,即一个支持成员函数 begin 和 end 的数组。不过它大小固定,并不使用动态内存。tr1::mem_fn
,这是一个语句上构造与成员函数指针(member function pointers)一致的东西。同样容纳并扩充了 C++98 的 mem_fun
和 mem_fun_ref
的能力。tr1::reference_wrapper
,一个让引用的行为更像对象的设施。tr1::result_of
,这是一个用来推导函数调用的返回值类型的模版。