萃取的实现(一)

一个例子:对一个序列求和

固定的萃取

        用一个函数模板对数组中的所有元素就行求和,我们可以很轻松的写出下面的源码:

#include <iostream>

template <typename T>
T accum(const T *begin, const T *end) {
    T total{};
    while (begin != end) {
        total += *begin;
        ++begin;
    }
    
    return total;
}

int main(int argc, const char **argv) {
    int num[] = { 1, 2, 3, 4, 5 };
    std::cout << "the average value of the integer values is " << accum(num, num + 5) / 5 << std::endl;
    char name[] = "templates";
    int length = sizeof(name)-1;
    std::cout << "the average value of the characters in \"" << name <<
    "\" is " << accum(name, name + 9) / 9 << std::endl;
    
    return 0;
}

        然而,程序的运行结果却令人大失所望:

the average value of the integer values is 3
the average value of the characters in "templates" is -5

        name所有元素之和为负值,很明显是和超出了char所能表示的范围。所以对数组求和,和的数据类型的表示范围要大于数组的数据类型的表示范围。因此源码中total的类型不能是T。一种解决方案如下:

accum<int>(name,name+5);

        另一种解决方案是使用萃取模板,通过模板的偏化建立T和对应返回值类型的联系,如下:

//...
template<typename T> struct AccumulationTraits;

template<>
struct AccumulationTraits<char> { using AccT = int; };

template<>
struct AccumulationTraits<short> { using AccT = int; };

template<>
struct AccumulationTraits<int> { using AccT = long; };

template<>
struct AccumulationTraits<unsigned int> { using AccT = unsigned long; };

template<>
struct AccumulationTraits<float> { using AccT = double; };

template <typename T>
auto accum(const T *begin, const T *end) {
    using AccT = typename AccumulationTraits<T>::AccT;
    AccT total{};
    while (begin != end) {
        total += *begin;
        ++begin;
    }
    
    return total;
}

//...

        这样返回值类型的问题边解决了,新的执行结果如下:

the average value of the integer values is 3
the average value of the characters in "templates" is 108

值萃取 

        在accum模板的例子中,我们通过默认构造函数对返回值进行了初始化,但该种方案无法处理本身就没有默认构造函数的类型,如下面的BigInt类:

class BigInt {
public:
    BigInt(long long data) : m_data(data) {}
    
    BigInt operator+(const BigInt &rhs) { return BigInt{m_data + rhs.m_data}; }
    BigInt &operator+=(const BigInt &rhs) {
        m_data += rhs.m_data;
        return *this;
    }
    
    long long value(void) const { return m_data; }
private:
    long long m_data;
};

        关于这个问题,我们可以通过值萃取来解决,完整源码如下:

#include <iostream>

class BigInt {
public:
    BigInt(long long data) : m_data(data) {}
    
    BigInt operator+(const BigInt &rhs) { return BigInt{m_data + rhs.m_data}; }
    BigInt &operator+=(const BigInt &rhs) {
        m_data += rhs.m_data;
        return *this;
    }
    
    long long value(void) const { return m_data; }
private:
    long long m_data;
};

template<typename T> struct AccumulationTraits;

template<>
struct AccumulationTraits<char> {
    using AccT = int;
    static const AccT zero = 0; };

template<>
struct AccumulationTraits<short> { 
    using AccT = int;
    static const AccT zero = 0; };

template<>
struct AccumulationTraits<int> { 
    using AccT = long;
    static const AccT zero = 0; };

template<>
struct AccumulationTraits<unsigned int> { 
    using AccT = unsigned long;
    static const AccT zero = 0; };

template<>
struct AccumulationTraits<float> { 
    using AccT = double;
    static constexpr AccT zero = 0.0f; };

template<>
struct AccumulationTraits<BigInt> {
    using AccT = BigInt;
    static const BigInt zero; };
const BigInt AccumulationTraits<BigInt>::zero = BigInt{0};

template <typename T>
auto accum(const T *begin, const T *end) {
    using AccT = typename AccumulationTraits<T>::AccT;
    AccT total = AccumulationTraits<T>::zero;
    while (begin != end) {
        total += *begin;
        ++begin;
    }
    
    return total;
}

        如果仔细观察,会发现使用float和BigInt进行偏化与使用整型(char,short,int)进行偏化不的方法不一样:

  • c++只允许在类中对整型或枚举类型的static const数据成员进行初始化,所以float进行偏化时使用了float
  •  无论是const还是constexpr,都只能对字面值类型(int,float,double)进行初始化,所以使用BigInt进行偏化只能使用上面这种方式。

        这样虽然可以工作,但是却有些麻烦(必须在两个地方同时修改代码),这样可能还会有些 低效,因为编译期通常并不知晓在其它文件中的变量定义。为了方便,可以使用函数取代成员变量,如下:

template<typename T> struct AccumulationTraits;

template<>
struct AccumulationTraits<char> {
    using AccT = int;
    static constexpr AccT zero(void) { return 0; } };

template<>
struct AccumulationTraits<short> { 
    using AccT = int;
    static constexpr AccT zero(void) { return 0; } };

template<>
struct AccumulationTraits<int> { 
    using AccT = long;
    static constexpr AccT zero(void) { return 0; } };

template<>
struct AccumulationTraits<unsigned int> { 
    using AccT = unsigned long;
    static constexpr AccT zero(void) { return 0; } };

template<>
struct AccumulationTraits<float> { 
    using AccT = double;
    static constexpr AccT zero(void) { return 0.0f; } };

template<>
struct AccumulationTraits<BigInt> {
    using AccT = BigInt;
    static  BigInt zero(void) { return BigInt{0}; } };

template <typename T>
auto accum(const T *begin, const T *end) {
    using AccT = typename AccumulationTraits<T>::AccT;
    AccT total = AccumulationTraits<T>::zero();
    while (begin != end) {
        total += *begin;
        ++begin;
    }
    
    return total;
}

参数化的萃取

        前面所提到的萃取方式称为固定萃取。为了更加灵活,我们可以引入一个新的模板参数,专门用于萃取,如下:

template <typename T, typename AT =  AccumulationTraits<T>>
auto accum(const T *begin, const T *end) {
    using AccT = typename AT::AccT;
    AccT total = AccumulationTraits<T>::zero();
    while (begin != end) {
        total += *begin;
        ++begin;
    }
    
    return total;
}

萃取及策略类

        前面的例子没有区分累积和求和,如果想换种累计方式,如求积,该如何处理?最直接了当的方案是把accum拷贝一遍,重新命个名,然后把total += *begin;修改为total *= *begin; ,很明显,这不是我们想要的答案。除了这种直接了当的方案,还可以通过萃取策略来解决这个问题,源码如下:

//...
class SumPolicy {
public:
    template<typename T1, typename T2>
    static void accumulate (T1& total, T2 const& value) { total += value; }
};

class MultPolicy {
    public:
    template<typename T1, typename T2>
    static void accumulate (T1& total, T2 const& value) { total *= value; }
};

template<typename T,
typename Policy = SumPolicy,
typename Traits = AccumulationTraits<T>> 
auto accum (T const* begin, T const* end) {
    using AccT = typename Traits::AccT;
    AccT total = Traits::zero();
    while (begin != end) {
        Policy::accumulate(total, *begin);
        ++begin;
    }
    return total;
}

int main(int argc, const char **argv) {
    int num[] = { 1, 2, 3, 4, 5 };
    std::cout << "the product of the integer values is  " << accum<int,MultPolicy>(num, num + 5) << std::endl;
    return 0;
}

        但程序的运行结果却是0。之所以会这样,是因为我们选取的初始值为0。由此可见,萃取和策略可能会相互影响。通过这个例子,我们有可能会意识到:累计循环的初始值应该累计策略的一部分。 但并不是所有的事情都需要用到萃取和策略才能解决。例如,C++标准库中的 std::accumulate()就将其初始值当作了第三 个参数。

成员模板还是模板模板参数

        为实现累积策略,我们将SumPolicy和MultiPolicy实现称为有成员模板的常规类,除此之外,还有另一种方案,使用类模板实现策略接口,如下:

//...
template<typename T1, typename T2>
class SumPolicy {
public:
    static void accumulate (T1& total, T2 const& value) { total += value; }
};

template<typename T1, typename T2>
class MultPolicy {
    public:
    static void accumulate (T1& total, T2 const& value) { total *= value; }
};

template<typename T,
template<typename,typename> class Policy = SumPolicy,
typename Traits = AccumulationTraits<T>>
auto accum (T const* begin, T const* end) {
    using AccT = typename Traits::AccT;
    AccT total = Traits::zero();
    while (begin != end) {
        Policy<AccT,T>::accumulate(total, *begin);
        ++begin;
    }
    return total;
}
//...

        如此实现,优点是策略类的成员变量可以很方便的使用类型信息(如果有);缺点是类必须是模板类,且必须和接口(accum)定义参数一致,这无疑使萃取本身变得更加复杂,也不自然。 

类型函数

        值函数:接收一些值作为参数,返回一个值作为结果。

        类型函数(模板):接收一些类型作为参数,返回一个类型或常量作为结果。以下便是一个类型函数的例子:

template <typename T>
struct type_size {
    static const std::size_t value = sizeof(T);
};

int main(int argc, char **argv) {
    std::cout << type_size<int>::value << std::endl;
    std::cout << type_size<double>::value << std::endl;
    return 0;
}

元素类型

        通过模板偏特化实现一个工具获取容器中元素的类型,源码如下:

#include <iostream>
#include <vector>
#include <list>

template <typename T>
struct element_t;

template <typename T>
struct element_t<std::vector<T>> {
    using type = T;
};

template <typename T>
struct element_t<std::list<T>> {
    using type = T;
};

template <typename T, size_t N>
struct element_t<T[N]> {
    using type = T;
};

template <typename T>
struct element_t<T[]> {
    using type = T;
};

template <typename T>
void print_element_type(const T &container) {
    std::cout << typeid(typename element_t<T>::type).name() << std::endl;
}

int main(int argc, char **argv) {
    std::vector<std::vector<int>> v1;
    print_element_type(v1);
    
    int a1[10];
    print_element_type(a1);
    
    return 0;
}

        类型函数的作用:允许我们更具容器类型参数化一个模板,但又无需提供代表了元素类型和其他特性的参数,例如:

//需要显示指定元素类型
template<typename T, typename C> 
T sumOfElements (C const& c);

//无需显示指定元素类型
template<typename C>
typename ElementT<C>::Type sumOfElements (C const& c);

转换萃取 

        除了可以被用来访问主参数类型的某些特性,萃取还可以被用来做类型转换,比如为某个类

型添加或移除引用、const 以及 volatile 限制符,下面是移除引用的例子:

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
struct remove_refference_t {
    using type = T;
};

template <typename T>
struct remove_refference_t<T &> {
    using type = T;
};

template <typename T>
struct remove_refference_t<T &&> {
    using type = T;
};


int main(int argc, char **argv) {
    std::string str0 = "10";
    std::string &str1 = str0;
    std::string &&str2 = "0";
    std::cout << std::is_same<decltype(str0), decltype(str1)>::value << std::endl;
    std::cout << std::is_same<decltype(str0), remove_refference_t<decltype(str1)>::type>::value << std::endl;
    std::cout << std::is_same<decltype(str0), decltype(str2)>::value << std::endl;
    std::cout << std::is_same<decltype(str0), remove_refference_t<decltype(str2)>::type>::value << std::endl;
    
    return 0;
}

预测性萃取

        说到预测性萃取,能想到的第一个便是is_same,用于判断两个类型是否相同,其实现如下:

template <typename T1, typename T2>
struct is_same {
    static constexpr bool value = false;
};

template <typename T>
struct is_same<T, T> {
    static constexpr bool value = true;
};

        其实现也比较好理解,唯一难以理解的就是偏化实现,自己写代码时很少用到这种方式。stl关于is_same的实现如下:

template <typename T, T v>
struct integral_constant {
    static const T value= v;
    typedef T value_type;
    typedef integral_constant type;
};

typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;

template <typename T1, typename T2>
struct is_same : false_type{} {};

template <typename T>
struct is_same<T, T> : true_type {} {};

        与先前的实现相比,新的实现引入了integral_constant,is_same直接从integral_constant派生。如果仔细观察stl,会发现大部分is_xxx函数都是从integral_constant派生。这样做最大的优点就是访问方式统一,对于所有的is_xxx函数都可以通过is_xxx::value访问。

返回结果类型萃取

        写一个函数,实现两个向量相加,我们可以很快写出下面的代码:

template <typename T>
std::vector<T> operator+(const std::vector<T> &lhs, const std::vector<T> &rhs) {
    size_t min = std::min(lhs.size(), rhs.size());
    size_t max = std::max(lhs.size(), rhs.size());
    size_t i = 0;
    std::vector<T> result;
    for (; i < min; ++i)
        result.push_back(lhs[i]+rhs[i]);
    
    if (lhs.size() == max) {
        while (i < max)
            result.push_back(lhs[i++]);
    }
    
    if (rhs.size() == max) {
        while (i < max)
            result.push_back(rhs[i++]);
    }
    
    return result;
}

        由于C++本身支持一个double类型数值和一个int类型数值求和,因此,我们希望向量也支持这种混合类型的操作。对此,我们很容易想到使用两个模板参数,如下:

template <typename T1, typename T2>
std::vector<???> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs)

        但如何选择返回值的类型,成为了一个难题。面对这个难题,有两个解决方案:

  • 使用类型萃取

template <typename T1, typename T2>
struct plus_result { using type = decltype(T1() + T2()); };

template <typename T1, typename T2>
using plus_result_t = typename plus_result<T1, T2>::type;

template <typename T1, typename T2>
std::vector<plus_result_t<T1, T2>> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs) {
    size_t min = std::min(lhs.size(), rhs.size());
    size_t max = std::max(lhs.size(), rhs.size());
    size_t i = 0;
    std::vector<plus_result_t<T1, T2>> result;
    for (; i < min; ++i)
        result.push_back(lhs[i]+rhs[i]);
    
    if (lhs.size() == max) {
        while (i < max)
            result.push_back(lhs[i++]);
    }
    
    if (rhs.size() == max) {
        while (i < max)
            result.push_back(rhs[i++]);
    }
    
    return result;
}
  • 使用auto 
template <typename T1, typename T2>
auto operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs) {
    size_t min = std::min(lhs.size(), rhs.size());
    size_t max = std::max(lhs.size(), rhs.size());
    size_t i = 0;
    using type = decltype(T1() + T2());
    std::vector<type> result;
    for (; i < min; ++i)
        result.push_back(lhs[i]+rhs[i]);
    
    if (lhs.size() == max) {
        while (i < max)
            result.push_back(lhs[i++]);
    }
    
    if (rhs.size() == max) {
        while (i < max)
            result.push_back(rhs[i++]);
    }
    
    return result;
}

        上述两种方案无论哪种,都是使用decltype来推断表达式T1()+T()的类型,因此存在两个共同的问题:

  • decltype保留了过多的类型信息,如plus_result::type有可能返回一个引用类型,也可能具有const限制,这可能不是我们想要的,可以通过使用stl标准库工具去掉限制,如下是使用std::remove_reference_t去掉引用的例子
template <typename T1, typename T2>
std::vector<std::remove_reference_t<plus_result_t<T1, T2>>> operator+(const std::vector<T1> &lhs, const std::vector<T2> &rhs)
  • 由于表达式 T1() + T2()试图对类型 T1 和 T2 的数值进行值初始化,这两个类型必须要有可访问的、未被删除的默认构造函数,如下面的代码是无法通过编译的:
class Int {
public:
    Int(int i) : m_Data(i) {}
    Int operator+(Int &rhs) { return Int(m_Data + rhs.m_Data); }
    
public:
    int m_Data;
};

class Double {
public:
    Double(double d) : m_Data(d) {}
    Double operator+(Double &rhs) { return Double(m_Data + rhs.m_Data);}
    Double(Int i) : m_Data(i.m_Data) {}
    
public:
    double m_Data;
};

Double operator+(Int lhs, Double rhs) { return Double(lhs.m_Data + rhs.m_Data); }
Double operator+(Double lhs, Int rhs) { return Double(lhs.m_Data + rhs.m_Data); }

//......
std::vector<Int> v7 {{1}, {2}, {3}};
std::vector<Double> v8 {{1.1}, {2.1}, {3.1}};
auto v9 = v7 + v8;
for (auto i : v9)
    std::cout << i.m_Data << " ";
std::cout << std::endl;

        下面是编译输出的错误之一:编译 decltype(T1() + T2())时,Int找不到合适的构造函数。

result_type_trait3.cpp:51:44: error: no matching constructor for initialization of 'Int'
struct plus_result { using type = decltype(T1() + T2()); };

        解决方案就是使用std::declval重写plus_result,如下:

template <typename T1, typename T2>
struct plus_result { using type = decltype(std::declval<T1>() + std::declval<T2>()); };

        有了std::declval,我们就可以在使用decltype推断类型时,不依赖默认构造函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值