Effective C++ 条款45、46

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

所谓智能指针是“行为像指针”的对象,并提供指针没有的机能。STL容器的迭代器几乎总是智能指针;很明显的点是我们不会奢望使用“++”将一内置指针从linked list的某个节点移到另一个节点,但这在list::iterator身上办得到。

真实指针做得很好的一件事是,支持隐式转换。Derived class指针可以隐式转换为base class指针,“指向non-const对象”的指针可以换为“指向对象”......等等。具体见下面三层继承体系的转换:

class Top{};
class Middle:public Top{};
class Bottom:public Middle{};
Top* pt1 = new Middle;	//将Middle*转换为Top*
Top* pt2 = new Bottom;  //将Bottom*转换为Top*
const Top* pct2 = pt1;  //将Top*转换为const Top*
但是在用户自定的智能指针中模拟上述转换,则会出现稍许麻烦,如下所示,
template<typename T>
class SmartPtr {
public:
	explicit SmartPtr(T* realPtr);//智能指针通常以内置指针完成初始化
	...
};
SmartPtr<Top> pt1 =
	SmartPtr<Middle>(new Middle);//将SmartPtr<Middle>转换为SmartPtr<Top>
SmartPtr<Top> pt2 =
	SmartPtr<Bottom>(new Bottom);//将SmartPtr<Bottom>转换为SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1;//将SmartPtr<Top>转换为SmartPtr<const Top>

上述转换编译通不过,是因为通过template具现的B,D类型,并不具有B-D关系!于是上述转换存在问题。

还有一个问题是,根据一个SmartPtr<Middle>或一个SmartPtr<Bottom>构造出一个SmartPrr<Top>,但如果这个继承体系未来有所扩充,SmartPtr<Top>对象又必须能够根据其他智能指针构造自己。如添加如下,

class BelowBottom:public Bottom{};
难道又必须令SmartPtr<BelowBottom>对象得以生成SmartPtr<Top>对象,有点不太可能吧。

解决办法是利用template和泛型编程,即运用成员函数模板接受所有兼容类型哈。因为一个template可被无限量具现化,以致生成无限量函数。具体构造如下,

template<typename T>
class SmartPtr {
public:
	template<typename U>//这是被泛化的copy构造函数,对任何类型T和U,可以根据SmartPtr<U>生成一个SmartPtr<T>
	SmartPtr(const SmartPtr<U>& other);//member template,为了生成copy构造函数
	...
};

上述构造还需要注意的一点是,我们希望根据一个SmartPtr<Bottom>创建一个SmartPtr<Top>,却不希望根据一个SmartPtr<Top>创建SmartPtr<Bottom>。也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>,因为现实中并没有“将int*转换为double*”对应隐式转换行为。

上述问题,解决办法是也提供一个get成员函数,返回智能指针对象所持有的那个原始指针的副本,那么就可以在“构造模板”实现代码中约束转换行为,使它符合正常的期望:

template<typename T>
class SmartPtr {
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other)//以other的heldPtr初始化this的heldPtr
		:heldPtr(other.get()){...}//此处存在某个隐式转换,将一个U*指针转为一个T*指针
	T* get() const { return heldPtr; }
	...
private:
	T* heldPtr;//这个SmartPtr持有的内置(原始)指针
};

上述构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译。

member function templates(成员函数模板)的效用不限于构造函数,也常扮演的另一个角争是支持赋值操作。如TR1的shared_ptr支持所有“来自兼容的内置指针、tr1::shared_ptr、auto_ptr和tr1::weak_ptr”的构造行为,以及所有来自上述各物(tr1::weak_ptr)的赋值操作。以下是举例代码,

template<clas T>
class shared_ptr {
public:
	template<class Y>
	explicit shared_ptr(Y* p);

	template<class Y>
	shared_ptr(shared_ptr<T> const& r);//此处没用explicit意味着从某个shared_ptr类型隐匿转换到另一个shared_ptr类型是被允许的

	template<class Y>
	explicit shared_ptr(weak_ptr<Y> const& r);

	template<class Y>
	explicit shared_ptr(auto_ptr<Y>& r);

	template<class Y>
	shared_ptr& operator=(shared_ptr<Y> const& r);

	template<class Y>
	shared_ptr& operator=(auto_ptr<Y>& r);//此处没用const,是因为当复制一个auto_ptr对象时,其实是被改动过的,所以无法用const
};
最后还需要强调一点的是,编译器可能产生四个成员函数,其中两个copy构造函数,两个是copy assignment操作符。 如果需要一个copy构造函数,却没有声明它,编译器暗自生成一个。在class内声明泛化构造函数并不会阻止编译器生成它们自己的的copy构造函数

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

此条款类比条款24(将所有参数需隐式类型转换的函数定义为non-member函数)学习。

针对条款24的讨论进行扩充,此处将Rational和operator*模版化:

template<typename T>
class Rational {
public:
	Rational(const T& numerator = 0, const T& denominator = 1);
	const T numerator()const;
	const T denominator()const;
	...
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){...}
我们希望以下调用能像条款24一样通过编译,
Rational<int> oneHalf(1, 2);//这个例子来自条款24,唯一不同是Rational改为template
Rational<int> result = oneHalf * 2;//错误!无法通过编译

针对上述第二行代码无法通过编译的原因,我们从编译器的角度出发去理解为什么通不过编译。编译器首先思考该选用什么函数具现化,为此必须先推算出T是什么,可它做不到。因为operator*第一个参数可推算出T为int,第二个参数却不行

为了缓和编译器在template实参推导方面受到的挑战:template class内的friend声明式可以免指涉某个特定函数。即,class Rational<T>可以声明operator*是它的一个friend函数。Class templates并不倚赖template实参推导(因为使用class template会声明具现类型,而template实参推导只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T。样例代码如下:

template<typename T>
class Rational {
public:
	...
		friend const Rational operator*(const Rational& lhs, const Rational& rhs);//声明operator*函数
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){...}//定义operator*函数

上述对operator*混合式调用可以通编译。总结流程:当对象oneHalf被声明为一个Rational<int>,class Rational<int>被具现出来,同时friend函数operator*也被自动声明出来。

述friend修饰的operator*函数声明在class内部,定义在class外部,虽然可以通过编译,但无法连接(在连接阶段,连接找不到它),因为如果我们自己声明了一个函数(那正是Rational template内的作为),就有责任定义那个函数(没太理解,此处为会声明与定义分离,能通过编译,却会在连接阶段出现问题)。修复这个小BUG,将operator*函数本体合并至声明式内:

template<typename T>
class Rational {
public:
	...
	friend const Rational operator*(const Rational& lhs, const Rational& rhs) {//此处声明与定义合并!
		return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
	}
};

综上总结:为了让类型转换可能发生于所有实参身上,需要一个non-member函数(条款24);为了令这个函数被自动具现化,需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。


以下做法是让inline声明带来的冲击最小化:

“Rational是个template”这一事实意味上述的辅助函数通常也是个template,所以定义了Rational的头文件代码,如下所示:

template<typename T> class Rational;//声明Rational template
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Tational<T>& rhs);//辅助模版

template<typename T>
class Rational {
public:
	...
	friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
		return doMultiply(lhs, rhs);//令friend调用helper
	}
	...
};

//于是doMultiply定义如下所示
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,doMultiply当然不支持混合式乘法,但其实它不需要。因为它只被operator*调用,而operator*是支持了混合式操作。

以上内容均来自Scott Meyers大师所著Effective C++ version3,如有错误地方,欢迎指正!相互学习,促进!!

猜你喜欢

转载自blog.csdn.net/tt_love9527/article/details/80882768