上篇博客的解答与模板的应用

之前的一篇博客中已经提到过了, 使用模板的目的是提高效率, 可是会因为用户输入的不可预知性导致计划中的函数没有匹配到, 而是被模板函数接收, 所以我们的策略就是, 使用 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"
        );
        ...
    }
    ...
};

 

posted @ 2015-01-07 16:17  wu_overflow  阅读(208)  评论(0编辑  收藏  举报