模板

在这篇文章提到了模板类型推导,关于模板的知识还不够全面,通过这篇文章再结合实际生产中遇到相关问题进一步完善模板相关知识

模板


模板可以包含类型参数和非类型参数,像下面这个模板同时包含了这两个参数:

template<typename T,int N>
class Test;

T为类型参数,N为非类型参数,N必须是个编译期已知的值,关于编译期已知值可以参考这篇文章,上面的模板声明和C++11中的STL模板Array一致,非类型参数的类型有一定限制,可以是整型、枚举、 引用、指针、自定义类型,下面列出了合法使用和不合法使用:

// NO:
template<typename T,double N>
class Test;

// YES:
template<typename T,double* N>
class Test;

模板内代码不能修改参数的值,也不能使用参数的地址,不合法的使用:

// NO:
N++;
// NO:
&N;

类型参数可以指定默认类型,如:

template<typename T,typename U = int>
class TestDefault;

同时可以递归使用模板,例如上面提到Test模板:

Test<Test<int,2>,10> obj;

值得一提的是下面两种模板的使用方式会是两种不同的类型:

Test<int,10> t1;
Test<int,11> t2;

稍后会利用这一特性实现标签分派

模板具体化


模板具体化分为:隐式实例化、显示实例化、显示具体化、部分具体化

  • 隐式实例化

    上面提到的都是隐式实例化,例如:

      Test<int,11> t2;
    

    编译器在需要对象之前不会生成类的隐式实例化,例如:

      // 并不需要隐式实例化
      Test<int,11> *t2;
      ...
      // 隐式实例化
      t2 = new Test<int,11>;
    
  • 显示实例化

    显示实例化的语法为:

      template class Test<int,11>;
    

    声明必须位于模板定义所在的名称空间内,虽然没有创建对象,编译器也将生成类声明,和隐式实例化一样,也将根据通用模板来生成具体化

  • 显示具体化

    显示具体化的语法为:

      template<> 
      class Test<int>{...};
    

    显示具体化经常被用到,例如:类模板不接受某个特定类型,或者对某个特定类型的特殊处理

  • 部分具体化

    部分具体化可以给类型参数之一指定具体的类型,部分具体化的语法为:

      template<class T1,class T2>
      class TestMulti{};
    	
      // 部分具体化
      template<class T1>
      class TestMulti<T1,int>{};
    

    如果有多个模板可供选择,编译器将使用具体化程度最高的模板,也可以通过为指针提供特殊版本来部分具体化,例如:

      template<class T>
      class TestMulti{};
    	
      template<class T*>
      class TestMulti{};
    

    如果提供的类型不是指针,则编译器将使用通用版本,如果提供的是指针,则编译器将使用指针具体化版本

类模板和成员模板


模板可以用作结构、类、模板类的成员,可以将一个模板类作为另一个模板类的成员

类模板可以像普通类一样在.h文件中声明,.cpp文件定义,这里涉及到包含编译分别编译,如果没必要还是尽量采用建议——将声明和定义都编写在.h文件中,如果一定要声明和实现要分开,可以采用包含编译

例如下面这个例子:

// simple_template_test.h
class SimpleTemplateTest
{
public:
	template<typename T>
	void Fun(T param);
};

可以将函数Fun实现写于simple_template_test.h文件内,例如:

// simple_template_test.h

#ifndef SIMPLE_TEMPLATE_TEST_H_
#define SIMPLE_TEMPLATE_TEST_H_
class SimpleTemplateTest
{
public:
	template<typename T>
	void Fun(T param)
	{
		std::cout << "Fun" << std::endl;
	}
};
#endif

如果将函数Fun实现写于simple_template_test.cpp文件内,像这样:

// simple_template_test.cpp

#include "simple_template_test.h"

template<typename T>
void SimpleTemplateTest::Fun(T param)
{
	std::cout << "Fun" << std::endl;
}

导致编译失败——无法解析的外部符号,按照上面提到方案采用包含编译,可以做如下修改:

// simple_template_test.h

#ifndef SIMPLE_TEMPLATE_TEST_H_
#define SIMPLE_TEMPLATE_TEST_H_

class SimpleTemplateTest
{
public:
	template<typename T>
	void Fun(T param);

public:
	U param_;
};

#include "simple_template_test.ipp"

#endif


// simple_template_test.ipp

#ifndef SIMPLE_TEMPLATE_TEST_IPP_
#define SIMPLE_TEMPLATE_TEST_IPP_

#include "simple_template_test.h"

template<typename T>
void SimpleTemplateTest::Fun(T param)
{
	std::cout << "Fun" << std::endl;
}

#endif

这里就利用了包含编译,包含编译需要注意当有显式具体化的时候,例如当有针对string类型的特殊处理时:

template<>
void SimpleTemplateTest::Fun<std::string>(std::string& param)
{
	std::cout << "Fun" << std::endl;
}

需要注意以下两点:

  1. 如果还将#include "simple_template_test.ipp"这行代码放在simple_template_test.h文件中,又有多个编译单元include simple_template_test.h文件,会导致链接错误——多重定义的符号, 只能放在一个编译单元里

  2. 显式具体化一定要在调用这个具体化函数之前出现,否则会出现编译期间的错误

     // 报错信息
     无法显式专用化,已实例化void SimpleTemplateTest::Fun<std::string>(const T&)
    

    像下面这样:

     // simple_template_test.cpp
    	
     void SimpleTemplateTest::UseMemberTemplateFunction()
     {
       std::string s = "aa";
       Fun(s);
     }
    	
     #include "simple_template_test.ipp"
    

    上面代码在成员函数UseMemberTemplateFunction里调用成员模板函数Fun,这里已经推导出了void SimpleTemplateTest::Fun<std::string>(const T&),下面继续include simple_template_test.ipp, 而针对string类型的显式具体化在simple_template_test.ipp里实现,所以当再次具体化时,发现已经存在了

实际应用


实际生产中有一个声明和定义需分开的场景——一个普通类和模板类互相依赖,起初的问题:

  • 外层先include普通类MessageCenter

  • 普通类MessageCenter的.h文件include模板类MessageHandlerMgr

  • 模板类MessageHandlerMgr.h文件include普通类MessageCenter,模板类的实现用到了普通类

有如下解决方案:

  • 如果将这两个类定义于同一个文件内,将模板类和普通类同层次编写

    如果使用的VS编译器,编译成功,如果使用的是GCC编译器,编译失败(触发先有蛋还是先有鸡的哲学问题),像这样:

      // .h
    	
      template<typename MsgBaseType, typename Fun>
      class MessageHandler
      {
      public:
          ...
    	
          void HandleMsg(char const*const msg, const int l)
          {
              MessageCenter::GetInstancePtr()->DeserializationMsg(p_, msg, l);
    	
              f_(p_);
    	
              MessageCenter::GetInstancePtr()->HandleDone(p_, l);
          }
    	
      private:
          Fun f_;
          MsgBaseType* p_;
      };
    	
      template<typename MsgBaseType, typename Fun>
      class MessageHandlerMgr : public Singleton<MessageHandlerMgr<MsgBaseType, Fun>>
      {
      public:
          ...
    		
          void HandleMsg(const int msg_id, char const*const msg, const int l)
          {
              if (msg_.find(msg_id) == msg_.end())
              {
                  return;
              }
              msg_[msg_id]->HandleMsg(msg, l);
          }
    	
      private:
          std::map<int, MessageHandler<MsgBaseType, Fun>*> msg_;
      };
    		
      class MessageCenter : public Singleton<MessageCenter>
      {
      public:
    	
          ...
    		
          MessageCenter(EnumAppProto proto):Singleton<MessageCenter>(),
              proto_(proto), handler_mgr_(nullptr)
          {
              if (proto_ == EnumAppProto::ENUM_BUFF)
              {
                  handler_mgr_ = (void*)(new MessageHandlerMgr<BuffMsgBase, HandlerBuffFunType>);
              }
              else if (proto_ == EnumAppProto::ENUM_MSGPACK)
              {
                  handler_mgr_ = (void*)(new MessageHandlerMgr<MsgPackMsgBase, HandlerMsgPackFunType>);
              }
          }
      }
    
  • 同一文件内的第二种方案,将模板类MessageHandlerMgr嵌套进普通类MessageCenter

    这种方案在windows和linux均可编译成功,像这样:

      class MessageCenter : public Singleton<MessageCenter>
      {
      public:
    	
          ...
    		
          MessageCenter(EnumAppProto proto):Singleton<MessageCenter>(),
              proto_(proto), handler_mgr_(nullptr)
          {
              if (proto_ == EnumAppProto::ENUM_BUFF)
              {
                  handler_mgr_ = (void*)(new MessageHandlerMgr<BuffMsgBase, HandlerBuffFunType>);
              }
              else if (proto_ == EnumAppProto::ENUM_MSGPACK)
              {
                  handler_mgr_ = (void*)(new MessageHandlerMgr<MsgPackMsgBase, HandlerMsgPackFunType>);
              }
          }
    		
      private:
          template<typename MsgBaseType, typename Fun>
          class MessageHandler
          {
          public:
              ...
    		
              void HandleMsg(char const*const msg, const int l)
              {
                  MessageCenter::GetInstancePtr()->DeserializationMsg(p_, msg, l);
    		
                  f_(p_);
    		
                  MessageCenter::GetInstancePtr()->HandleDone(p_, l);
              }
    		
          private:
              Fun f_;
              MsgBaseType* p_;
          };
    		
          template<typename MsgBaseType, typename Fun>
          class MessageHandlerMgr : public Singleton<MessageHandlerMgr<MsgBaseType, Fun>>
          {
          public:
              ...
    			
              void HandleMsg(const int msg_id, char const*const msg, const int l)
              {
                  if (msg_.find(msg_id) == msg_.end())
                  {
                      return;
                  }
                  msg_[msg_id]->HandleMsg(msg, l);
              }
    		
          private:
              std::map<int, MessageHandler<MsgBaseType, Fun>*> msg_;
          };
      };
    
  • 将模板类声明、实现分开

    使用包含编译将模板类MessageHandlerMgr的声明和实现分开,并且将模板类MessageHandlerMgr从MessageCenter.h中挪出来,分别创建MessageHandlerMgr.cpp、MessageHandlerMgr.h文件 当普通类MessageCenter编译完再include 模板类cpp文件,

    模板类MessageHandlerMgr.h文件编写:

      //.h
      template<typename MsgBaseType, typename Fun>
      class MessageHandler
      {
      public:
    	
          ...
    	
          void HandleMsg(Session* session, uint8 const*const msg, const MessageLength l);
    	
      private:
          Fun f_;
          MsgBaseType* p_;
      };
    	
      template<typename MsgBaseType, typename Fun>
      class MessageHandlerMgr : public Singleton<MessageHandlerMgr<MsgBaseType, Fun>>
      {
      public:
          ...
    		
          void HandleMsg(const int msg_id, char const*const msg, const int l)
          {
              if (msg_.find(msg_id) == msg_.end())
              {
                  return;
              }
              msg_[msg_id]->HandleMsg(msg, l);
          }
    	
      private:
          std::map<int, MessageHandler<MsgBaseType, Fun>*> msg_;
      };
    

    模板类MessageHandlerMgr.cpp内编写:

      // .cpp
      #ifndef MESSAGE_MANAGER_CPP_
      #define  MESSAGE_MANAGER_CPP_
    	
      #include "message_manager.h"
      #include "message_center.h"
    	
      ...
    	
      template<typename MsgBaseType, typename Fun>
      void MessageHandler<MsgBaseType, Fun>::HandleMsg(Session* session, uint8 const*const msg, const MessageLength l)
      {
          MessageCenter::GetInstancePtr()->DeserializationMsg(p_, msg, l);
    
          f_(session, p_);
    
          MessageCenter::GetInstancePtr()->HandleDone(p_, l);
      }
      #endif
    

    在MessageCenter.h文件最后添加#include "message_manager.cpp",这就解决了先有蛋还是先有鸡的问题

分别编译需要使用export,很多编译器并不支持分别编译,至少VS是的,上述代码片段摘自一个自己的网络库代码(还没有完成),代码github地址

标签分派技术应用


在这篇文章提到了标签分派技术,通过std::false_type和std::true_type标签实现,这里有个局限就是只适合类型不能超过两种,如果有更多类型这种方式便不能解决,而自己的网络库希望支持多种不同和的 协议,如msgpack、protobuf、自定义等,上述代码与自己的网络库代码有些不同,为了说明问题做了很多简化

在message_center.h文件中使用了上面提到模板 的一个重要特性来实现标签分派,使用标签分派技术是为了解决根据上层选择的消息协议(编译期已知)来确定创建什么类型的模板,部分代码如下:

template <std::size_t v>
struct SizeT2Type {
	enum { value = v };
};

template<typename MsgSubType, typename F, typename ProtoType, std::size_t N>
bool RegisterHandler(const MessageID msg_id, F&& f, ProtoType(&)[N])
{
	return RegisterHandler<MsgSubType, F>(msg_id, std::forward<F>(f), SizeT2Type<N>());
}

template<typename MsgSubType, typename F>
bool RegisterHandler(const MessageID msg_id, F&& f, SizeT2Type<BUFF_PROTO_SIZE>)
{
	MessageHandlerMgr<BuffMsgBase, HandlerBuffFunType>* p = (MessageHandlerMgr<BuffMsgBase, HandlerBuffFunType>*)handler_mgr_;
	return p->RegisterHandler<MsgSubType, F>(msg_id, std::forward<F>(f));
}

template<typename MsgSubType, typename F>
bool RegisterHandler(const MessageID msg_id, F&& f, SizeT2Type<MSG_PACK_PROTO_SIZE>)
{
	MessageHandlerMgr<MsgPackMsgBase, HandlerMsgPackFunType>* p = (MessageHandlerMgr<MsgPackMsgBase, HandlerMsgPackFunType>*)handler_mgr_;
	return p->RegisterHandler<MsgSubType, F>(msg_id, std::forward<F>(f));
}

// 上层调用这个函数
template<typename MsgSubType, typename F,typename ProtoType>
bool RegisterHandler(const MessageID msg_id, F&& f, ProtoType& proto)
{
	return msg_proto_->RegisterHandler<MsgSubType,F>(msg_id,std::forward<F>(f), proto);
}

试想如果在编译期没能确定这个模板具体类型,会有什么问题?编译期会有类型错误