effective c++ 老笔记(五)

条款四十二

使用typename

当模板中使用T类型中的某个类型名称的时候需要加上typename,因为系统默认T::name是在调用某个成员函数/变量,typename T::name 才是使用T的name类型。这个情况有两个例外。

一是在继承列表中不能使用typename

template<typename T>

class Derived : public Base<T>::Nested{};

//Base<T>::Nested是个类型名称,但是不需要使用typename

二是在成员初始化列表中不使用typename

Derived()::Base<T>Nested(){}

//调用Base<T>::Nested的构造函数,但是不使用typename

其他的,声明变量/返回值/参数类型,都需要typename,返回值需要使用后置返回值才能确定T类型。

对于typename标定标定的类型,通常都是非常长的名字,所以用using/typedef声明一个别名会更好。

条款四十三

处理模板化基类内的名称

继承一个模板类型的基类时需要使用<T>或者某个具体的类型。

在派生类中是无法直接调用继承的基类的成员的,不管是哪一种作用域(指定继承特定类型的情况除外)。因为派生类可能继承任何类型的模板基类,有些类型的基类可能会无法直接使用基类的成员(但是你使用某一个派生对象的时候是可以的,那时候已经确定类型了,即确定了T是什么)。因为派生类可能继承任何类型的模板基类,而有些基类会拥有其特例化版本,这些版本中或许会有新的/缺少某些成员,所以在派生类中无法保证哪些成员存在,哪些成员不存在。

有三种解决方法:

一、使用this->成员,告诉编译器调用继承自基类的成员(或者是自己在派生类中重写的)。

二、使用using声明基类成员 : base Base<T>::name

三、使用作用域限定符,问题是会强制使用基类版本,妨碍了virtual继承(即失去动态绑定)。

以上三种做法都无法调用基类的私有成员(虽说貌似显而易见),using使用的前提是成员的所有版本(包括重载函数)都得是public/protected的。不过派生类是可以重写基类的private virtual。

继承时,如果想让派生类在某个成员函数上的操作与基类的不同,又希望派生类可以调用基类版本的函数,或者是想要节省一个virtual调用的成本,那么可以把基类的这个函数定义成non-virtual函数,然后在派生类中写出一个名字相近的函数去调用从基类继承来的那个版本的函数。

条款四十四

把与template无关的代码移出template

namespace clause44 {
	namespace normal {
		template<typename T,std::size_t n>
		class SquareMatrix {
		public:
			void invert() { return matrix; }//不同数值类型或不同的n都引起新的invert()代码
		private:
			T matrix[n*n];
		};//整个类也比较大,采用指针会好一点
	}

	namespace withParameter {
		template<typename T>
		class SquareMatrixBase {
		public:
			T* invert(std::size_t n) { return matrix; }//执行实际的功能,需要绑定尺寸n
		private:
			T *matrix;//使用指针指向矩阵
		};//每个类型只有一份invert的代码

		template<typename T,std::size_t n>
		class SquareMatrix :private SquareMatrixBase<T>{
		public:
			using SquareMatrixBase<T>::invert;
			T* invert() { return this->invert(n); }//调用基类的处理
		private:
			T matrix[n*n];//存放实际的数据
		};//对象的体积会很大
	}

	namespace withNew {
		template<typename T>
		class SquareMatrixBase {
		public:
			SquareMatrixBase() :matrix(nullptr) {}
			SquareMatrixBase(T* m,size_t n) :size(n),matrix(m) {}
			T* invert() { return matrix; }//执行实际的功能,需要绑定尺寸n
		private:
			size_t size;//保存大小
			T* matrix;//使用指针指向矩阵
		};//每个类型才会有一个共有的invert版本(代码只有一份)

		template<typename T, std::size_t n>
		class SquareMatrix :private SquareMatrixBase<T> {
		public:
			using SquareMatrixBase<T>::invert;
			SquareMatrix() :matrix(new T[n*n]),//把矩阵数据放到动态内存
				SquareMatrixBase<T>(matrix.get(),n) {}//传递指针和大小
			
			T* invert() { return this->invert(n); }//调用基类的处理
		private:
			std::shared_ptr<T> matrix;//存放指针
		};
	}
}

结论:

对于非类型参数带来的膨胀(上面的size_t n)可以通过class成员变量/函数参数来改变,但这样你就无法让编译器检测“size_t n = 5的对象应不应该接受一个size_t n=10的同类对象”,要不你就需要一个template<typename T>的基类,或者是这样的一个类的变量/指针/引用,然后在这个类里定义其他的函数。

对于类型参数带来的膨胀,可以选择让拥有同样二进制表述的具现类型调用同一份底层实现码。即容器执有T*类型的时候,应该全部去调用一份处理(void *)类型的函数。

 

 

 

 

 

条款四十五

运用成员函数模板接受全部兼容类型

即在一个template<typename T>的类里去声明另一个template成员函数并使用不同类型名称template<typename U>,然后把U作为参数的类型或参数作为某些模板的类型参数再把模板类作为参数。这样就可以兼容所有类型的参数,但实现“兼容”这一点需要在之后的函数中做相应的限定,这样用户的使用错误才会在连接期报错。另外即使声明了泛化的拷贝函数/拷贝运算符,你还是会需要自己定义普通的版本(也就是仍然会需要合成默认构造函数)

namespace clause45 {
	template<class T>
	class SmartPtr {
	public:
		SmartPtr() :p(nullptr) {}
		template<class U>
		explicit SmartPtr(U* up) :p(up) {}//接受全部类型的指针初始化
		template<class U>
		SmartPtr(const SmartPtr<U> &sp) :p(sp.get()) {}//接受可以转换的SmartPtr
		SmartPtr(const SmartPtr& sp) :p(sp.get()) {}//仍然要定义一个自己版本的拷贝构造
		template<class U>
		explicit SmartPtr(std::unique_ptr<U> &up) : p(sp.get()) {}
		template<class U>
		explicit SmartPtr(std::weak_ptr<U> &wp) :p(wp.get()) {}

		template<class U>
		SmartPtr &operator=(SmartPtr<U> &sp);
		template<class U>
		SmartPtr &operator=(std::unique_ptr<U> &up);
		SmartPtr &operator=(const SmartPtr<T> &sp);

		T* get() { return p; }

	private:
		T * p;
	};
	template<class T>
	template<class U>
	SmartPtr<T> &SmartPtr<T>::operator=(SmartPtr<U> &sp)
	{
		p = sp.get();
		return *this;
	}

	template<class T>
	template<class U>
	SmartPtr<T> &SmartPtr<T>::operator=(std::unique_ptr<U> &up)
	{
		p = up.release();
		return *this;
	}

	template<class T>
	SmartPtr<T> &SmartPtr<T>::operator=(const SmartPtr<T> &sp)
	{
		p = sp.get();
		return *this;
	}
}

条款四十六

把参数需要隐式转换的non-member函数变为友元(通常是模板函数),并在声明友元的地方提供实现(并不是说一定要把具体的功能代码也放在那里,如果功能复杂,则可以在类外定义另一个template函数,然后用这个友元函数来调用它)。比如operator*就是一个可以(应该)这么做的函数(即使不是模板)。

froend class &operator*(const class& lhs,const class& rhs)

{ 类外函数(lhs,rhs); }

template<typename T>

class<T> &类外函数(const class<T>& lhs, const class<T> &rhs)

{ 具体实现operator*的功能; }

这样operator*便可以接受一个class与一个可转换成class的操作数运行,通常的思维是定义一个template的operator*,但是这样便无法进行类型转换,因为模板要求接受一样的类型作为类型推断的结果,比如lhs = class<int>;rhs = int,那么template怎么推断T的类型呢?编译器并不会先根据class<int>推出T = int,然后再把rhs转换成class<int>。在类型推断时是无法进行类型转换的(const转换与函数到函数指针的转换例外)。

所以需要把operator*声明为友元,从class处得到T的类型,也就是class<T>被编译的时候会同时实现class<T>型的operator*,那么能做到这一点的只有成员函数/友元函数。理由很简单,对应版本的template函数是在T类型的类里面声明的,也就是每个T类型都有一个T类型对应的operator*声明在类内,所以要为每个这样的函数提供定义,而不是在外部提供一个template泛化的operator*,如果不在类内定义则会在连接阶段出错。简单得说,外部template的T和class的T可不是同一个,声明为友元的那个函数根本就不是泛型函数,反而更类似其他泛型内的成员函数。

template<typename T>
class test {
public:
	test() = default;
	test(test<double> &a):test() {}
	void fun(test a, test b);  //a,b均可以进行类型转换,若是操作符则需要使用友元的形式
};
template<typename T>
void test<T>::fun(test a,test b)
{
	cout << "test<T>::fun" << endl;
}

int main() 
{
	test<int> a, b;
	test<double> c, d;
	a.fun(a, c);

	system("pause");
}
template<typename T>
class test {
public:
	test() = default;
	test(test<double> &a):test() {}
	friend test& operator* (test a, test b)
	{
		cout << "test<T>::*" << endl;
		return a;
	}
};
template<typename T>
test<T> &operator+(test<T> a, test<T> b)
{
	cout << "test<T>::+" << endl;
	return b;
}//无法实现类型转换


int main() 
{
	test<int> a, b;
	test<double> c, d;
	a*c;//ok
	a + c;//无法求和

	system("pause");
}

条款四十七

使用traits class返回类型信息

traits class用以从迭代器中提取迭代器的种类。对于一般的类型,iterator_traits做的是从iterator中取得某个类型成员来作为迭代器种类的判断,而iterator对于内置指针又应该发挥与迭代器一样的作用,因为迭代器与指针有类似的性质,但是内置指针内无法添加任何的信息,故首先排除简单得直接使用一个类,然后在类中存放自己的类型这种“类型内的嵌套信息”。因为你使用一个此类的指针类型去定义iterator_traits的时候,traits class的模板T等于一个指针类型,那么traits class是无法使用一个指针类型来提取指针自身中的某个成员作为指针类型的种类的。

所以应当定义一个接受T*的特例化版本,然后返回一个固定类型,或者返回typename T::类型成员。

对于iterator_traits的应用,函数可以定义无名参数去去作为重载版本的一个区分条件。为函数定义一个无名参数,这个参数的类型为iterator_traits返回的类型成员的类型,即函数可以接受iterator_traits中的类型成员来执行不同的重载版本。

namespace clause47 {
	struct iterType1 {
		iterType1() {
			std::cout << "iterType1构造" << std::endl;
		}
	};//第一种类型
	struct iterType2 {
		iterType2() {
			std::cout << "iterType2构造" << std::endl;
		}
	};//第二种类型

	class iterClass1
	{
	public:
		class iterator {
		public:
			using iterator_category = iterType1;
		};
	};//拥有迭代器的类型一

	class iterClass2
	{
	public:
		class iterator {
		public:
			using iterator_category = iterType2;
		};
	};//二


	template<typename T>
	struct myTraits {
		using iterator_category = typename T::iterator::iterator_category;
	};//traits通用版本

	template<typename T>
	struct myTraits<T*>
	{
		using iterator_category = iterType2;
	};//针对指针的traits,返回的是第二种类型

	template<typename T>
	void doAdvance(T, iterType1)
	{
		std::cout << "T类型:" << typeid(T).name() << std::endl;
		std::cout << "doAdvance(iterType1)" << std::endl;
	}//针对第一种迭代器的doAdvance

	template<typename T>
	void doAdvance(T, iterType2)
	{
		std::cout << "T类型:" << typeid(T).name() << std::endl;
		std::cout << "doAdvance(iterType2)" << std::endl;
		std::cout << "-------------" << std::endl;
	}//针对第二种迭代器以及指针的doAdvance

	template<class T>
	void myAdvance(T)
	{
		std::cout << "myAdvance" << std::endl;
		doAdvance(T(), typename myTraits<T>::iterator_category());
        //根据traits返回的类型作为重载版本选择的依据
	}//实际用户使用的advance函数

	/*
	iterClass1 *itcp1 = new iterClass1();
	iterClass2 itcp2;
	
	myAdvance(itcp1);
	myAdvance(itcp2);
	*/
}

条款四十七

模板源编程

template<unsigned n>
struct Factoria{
	enum{ value = n * Factoria<n-1>::value; }
};
template<unsigned n>
struct Factoria<0>{
	enum{ value = 1; }//特例化0的情况 = 1
};

一个由模板完成的阶乘,并且在编译期就计算完成。模板元编程能够使用较少的内存(不需要运算对象)进行更快的运算。也就是可以把工作从运行期转到编译期。TMP还可以防止用错误的类型(因为你一开始就指定了unsigned n,这个类型无法运算的话就通不过编译),或根据不同的类型定制不同的客户定制代码。

要注意的一点是用模板就意味着承认所有的代码都可能发生,比如if语句,其所有结果的代码都要是可以运行的,而不是在if的条件成立时才可以运行,而是即使不执行这个选项,这个选项的代码也全部能够正常运行,否则无法通过编译。此时就意味着你需要使用特例化版本来构成重载函数来实现条件判断语句。如之前的对不同的指针类型进行运算,其中random_access_iterator可以进行+=运算,bidirctional指针则不可以,故if的两个分支分别为“在random执行+=,在bidirectional时执行++”,这样的一个条件判断是无法通过编译的,因为bidirectional无法执行+=,而模板要求每种调用的情况都可以执行每个分支(所以其实你不用bidirectional调用的话就发现不了这个错误了,结果这段代码成为一个随时可能出错的炸弹)。

条款四十九

了解new handler的机制

namespace clause49 {
	
	class ReturnHandler_RAII {
	public:
		ReturnHandler_RAII(std::new_handler old) :oldHandler(old) { std::cout << "use handler RAII" << std::endl; }
		ReturnHandler_RAII(const ReturnHandler_RAII &) = delete;//阻止拷贝构造与其他类到此类的转换
		ReturnHandler_RAII &operator=(const ReturnHandler_RAII &) = delete;//阻止赋值运算
		~ReturnHandler_RAII() { 
			std::set_new_handler(oldHandler);//重新绑定global的handler
			std::cout << "set global handler" << std::endl;
		}
	private:
		std::new_handler oldHandler;
	};

	class selfHandler {
	public:
		std::new_handler set_new_handler(std::new_handler handler) noexcept;//用以改变类内的handler
		void *operator new(std::size_t n) throw(std::bad_alloc);
		//使用内部set_new_handler,并用RAII控制global_handler的装填,new为隐式static

	private:
		static std::new_handler myHandler;//由于new为static故handler需要是static才可以使用
	};

	void defaultHandler()
	{
		std::cout << "defaultHandler" << std::endl;
	}

	std::new_handler selfHandler::myHandler = defaultHandler;//设定默认处理

	std::new_handler selfHandler::set_new_handler(std::new_handler handler) noexcept
	{
		std::cout << "set a new handler" << std::endl;
		std::new_handler old = myHandler;
		myHandler = handler;
		return old;//交换并返回
	}

	void* selfHandler::operator new(std::size_t n) throw(std::bad_alloc)
	{
		std::cout << "called operator new" << std::endl;
		ReturnHandler_RAII raii(std::set_new_handler(myHandler));
		//使用RAII自动在内存请求完毕后绑回原来的handler
		return ::operator new(n);//调用全局的operator new
	}


	/*测试
	using namespace clause49;
	selfHandler *sh1 = new selfHandler();
	sh1->set_new_handler(testHandler);
	selfHandler *sh2 = new selfHandler();
	*/
}




模板版本
namespace TMPhandler {//与之前的版本一致,但是可以让任何类继承这个support类(public继承)然后使用
		template<typename T>//T实际上没有用到,其作用是区分不同类型然后生成不同Static handler
		class NewHandlerSupport {//使用模板另一个好处是你还可以定制(特例化)不同类的方式
		public:
			std::new_handler set_new_handler(std::new_handler handler) noexcept;
			void *operator new(std::size_t n) throw(std::bad_alloc);

		private:
			static std::new_handler classHandler;
		};
		template<typename T>
		std::new_handler NewHandlerSupport<T>::classHandler = nullptr;

		template<typename T>
		std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler handler) noexcept
		{
			std::new_handler old = classHandler;
			classHandler = handler;
			return old;
		}

		template<typename T>
		void *NewHandlerSupport<T>::operator new(std::size_t n) throw(std::bad_alloc)
		{
			ReturnHandler_RAII raii(std::set_new_handler(classHandler));
			return ::operator new(n);
		}
	}

结论:

new_handler在使用new,并且无法分配足够内存的时候调用。

nothrow版本的new( new(std::nothrow)type )只能保证表达式本身不会抛出异常,事实上type的构造函数还是可能抛出异常的,所以其实没有什么一定使用它的必要,抛出异常,然后处理就好了。

条款五十

了解new与delete的合理替换时机

替换自带的new与delete有很多理由

比如防止错误的heap使用和收集heap使用信息

提高效能等

条款五十一

修改new与delete时务必恪守成规

new必须返回正确的值,内存不足时应该调用new handler,new handler为null时抛出异常。

new中必须有一个循环,不断请求分配足够的空间,若失败就调用new handler,它应该可以释放足够的空间或者进行别的处理。

new还应该在用户请求size=0时进行size=1的处理

当重写了一个类的operator new时,要注意其派生类会继承这个函数,所以new的开始需要判断,size == sizeof(base),这同时也判断了了size = 0的情况,不相等的话调用::operator new全局的new(size)

对于operator new[],“array new”进行分配时,分配的实际空间很有可能大于size,因为或许要把分配的数组大小存放进去,也很难决定每个元素的大小,因为继承体系中的operator new无法判断派生类的大小。所以重写它只能分配一片原始空间返回。

delete

重写delete最需要记住的一点就是“无论何时删除空指针都安全”,所以当ptr == nullptr,直接return;若是类内部的delete,则也需要进行大小是否等于base的比较(如果有派生类,或者说如果new里面检测了大小)

if(size == sizeof(base))….

       分配内存

else

       ::operator delete(ptr,size);

       return:

 

 

 

 

 

条款五十二

定义了placement new,就也要定义一个placement delete

定义额外参数的new就要定义同样的接受这份额外参数的delete,因为new失败时,会调用拥有相同额外参数版本的delete去恢复内存的分配,所以new与delete需要成对定义。

另一点是,new和delete的class版本会覆盖global/基类的版本,所以要定义全部的new/delete版本,不要定制的版本去调用global或者基类的版本。另一个选择是定义一个专门的基类,其中含有所有的new和delete版本,均使用global的运算符,然后在需要定制的类里继承这个类并使用using声明。

猜你喜欢

转载自blog.csdn.net/qq_37051430/article/details/83475462