上篇博客的解答与模板的应用
之前的一篇博客中已经提到过了, 使用模板的目的是提高效率, 可是会因为用户输入的不可预知性导致计划中的函数没有匹配到, 而是被模板函数接收, 所以我们的策略就是, 使用 SFINAE 这个 trick:
template<typename T> void LogAndAdd(T &&name) { LogAndAddImpl (std::forward<T>(name) , std::is_integral<T>()); }
可是问题来了, 传入的引用是一个左值, 所以推断出的 T 就是那个左值的引用类型(如果传入一个 int, 那么推断出的 T 就是 int&) 所以揭开传入参数的本来面目, 就要把引用去掉, 对于这个需求, 简单暴力的模板函数:
remove_reference<T>
简单暴力到看名字就知道这是什么了, 是这么用的:
template<typename T> void LogAndAdd(T &&name) { LogAndAddImpl( std::forward<T>(name) , std::is_integral<typename std::remove_reference<T>::type>() ); }
然而, 这又引出一个问题: is_integral<T>() 的值是在运行时才能判断的, 而使用模板的目的就是想要把在运行时处理的问题提前到编译时期, 所以解决的方式是使用 tags, 我个人觉得有点 switch case 的神韵:
//两种情况 template<typename T> void LogAndAddImpl(T &&name, std::flase_type) { auto now = chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } std::string NameFromIdx(int idx, std::true_type) { void LogAndAdd(NameFromIdx(idx)); }
好, LogAndAdd 问题已经基本被比较满意的解决了, 下面则是对于类 Person 构造函数的问题.
对于这个问题我们要使用一个 trick, 叫做 enable_if<conditon>:type, 大概意思就是, 只有在满足 conditon 的条件下 , 其中的 type 才会被产生:
class Person{ public: template<typename T , typename = typename std::enable_if<condition>::type> explicit Person(T &&n); ... };
enable_if 其实又是另一个 trick 的一个运用, 这个 trick 叫做 SFINAE, 在此也不细说.
然后问题来了, 这既然是一个 拷贝/移动 构造函数的模板, 那么怎么判断拷贝的是不是一个 Person 呢?
这里又有一个模板类, std::is_same<T, U>, 其中, std::is_same<T, U>::type 的类型是 bool, 用于表示 T 和 U 是否是同一类型. 这样, 问题就解决了......吗? 并不是, 还需要考虑到两个问题:
1. Person, Person& 和 Person&& 可不是一个类型
2. 是否有 const 和 volatile 的前缀又是一个问题
如此多的干扰, 总不能把这些情况的排列组合写一遍吧?! 幸好这里又有一个使参数返璞归真的模板类 std::decay<T>, 它的 type 就是 T 的本来面目. 剔除了干扰, 就方便我们的判断了:
class Person{ public: template< typename T , typename = typename std::enable_if< !!std::is_same<Person , typename std::decay<T>::type >::value >::type > explicit Person(T &&n); ... };
终于完成了! 我简直想要撒h......需求又变了, Person 出于某种原因, 需要一个子类: SpecialPerson.
class SpecialPerson : public Person{ public: SpecialPerson(const SpecialPerson &rhs) :Person(rhs) {...} SpecialPerson(SpecialPerson &&rhs) :Person(std::move(rhs)) {...} ... };
看起来倒是正确的, 可是, Person 和 SpecialPerson 可不是一样的, 即使是在 decay 之后(果然够 special), 直接放进去必然会出错. 那么这该如何是好呢?
所以说 C++11 大法好, 居然还有亲子鉴定模板类 std::is_base_of<T, U>, 用于判断 U 是否继承 T. std::is_base_of<T, U>::value 也是一个 bool 类型的值, 所以我们只要把之前的代码稍加修改:
class Person{ public: tempalte< typename T , typename = typename std::enable_if< !std::is_base_of<Person , typename std::decay<T>::type >::value >::type > explicit Person(T &&n); ... };
哈哈哈! 终于完成了, 只是有一点点不爽: 中间又是 type 又是 value 的, 一不小心就乱了(我初学, 容易弄乱) 所以对于 C++11, 还是 C++14 大法更好:
//C++14 class Person{ public: tempalte< typename T , typename = std::enable_if_t< !std::is_base_of<Person , std::decay_t<T> >::value > > explicit Person(T &&n); ... };
我突然发现边看书边做笔记的坏处, 就是容易被书牵着鼻子走, 我都不知道已经是第几遍说:"这样就完成了!" 结果呢? 尼玛居然还是没完!
为啥捏? 因为假如用户给咱们的模板函数传入一个既不是 Person, 也不是 SpecialPerson 还不是 整数的参数, 这个函数会如何处理呢? 哈, 当然是当作整数来处理了......这能行? 所以要再加一道锁链:
class Person{ public: template< typename T , typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference_t<T>>::value > > explicit Person(T &&n) :_name(std::forward<T>(n)) {...} explicit Person(int idx) :_name(NameFromIdx(idx)) {...} ... };
到此大功告成, 无论传进模板化构造函数的参数是左值还是右值, 都会得到最佳的处理, 无论是 有着 const 还是 volatile 都会被正确的处理, 传入的整数也不会被错误的被卷入模板. 我们仅用了区区这几行代码就一举处理了如此多的情况, 证明之前的讨论是值得的.
最后的最后, 其实还是有一个问题, 这个设计是正确的, 问题是 forward<T> 本身有缺陷
1. forward 这个过程并不完美, 在以后细说.
2. 错误信息不明确, 这个说明一下:
举个例子, 有个用户传进来一个 char16_t 的数组:
Person(u"Konrad Zuse"); //character of type fucking const char16_t
模板遇到这个奇葩就会给出一个错误信息......嗯, 最多大概能有 160 行, 中间路过的层数越多, 给的错误信息就越多.
咋办呢? 既然要检查, 就别让错误走远, 一开始就来个断言, 并自己给一个说人话的错误信息, 此外也把运行时的错误检查提前到编译期, 提升性能.
class Person{ public: template< typename T , typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference<T>>::value > > explicit Person(T &&n) :_name(forward<T>(n)) { static_assert( std::is_constrctible<std::string, T>::value , "Parameter n cannot be used to construct a std::string" ); ... } ... };