C++ 萃取技术——固定萃取技术

发布于:2024-09-18 ⋅ 阅读:(49) ⋅ 点赞:(0)

C++ 萃取技术——固定萃取技术

在 C++ 的模板和泛型编程领域,“萃取”(trait)技术发挥着至关重要的作用。这种技术让程序能够在编译时对类型执行各种操作和计算,有效提取和利用类型信息,极大地增强了代码的灵活性和可重用性。

“萃取”,源自英文单词 “trait”,有时也被翻译为“特征”。在 C++ 中,通过定义所谓的“trait类模板”来实现对模板参数的高级管理和操作。这些类模板充当中间件的角色,不仅连接不同的功能模块,还使得模块之间的交互更加灵活和高效。

1. 萃取技术简介

萃取技术用于对模板中的各种模板参数进行管理。这种技术往往是通过书写一个类模板来体现,而这种类模板也称为“trait类模板”。从表现上来说,trait类模板像一个中间件一样,夹在不同的功能代码之间,使代码之间结合或调用(交互)变得更加灵活。

类型萃取(Type Traits):

"类型萃取"是萃取技术中的一个关键部分,它允许在编译时查询或修改类型的特定属性。类型萃取的概念最初在 Boost 库中形成,并随后被纳入 C++11 标准库。它包括对类型信息的提取、类型转换、以及基于类型属性的决策等操作,为高级模板编程提供了强大的工具。

C++ 标准库 <type_traits> 提供了大量的类型萃取模板,以下是一些常见的例子:

  • std::is_integral<T>:检测 T 是否为整数类型。
  • std::is_floating_point<T>:检测 T 是否为浮点数类型。
  • std::is_array<T>:检测 T 是否为数组类型。
  • std::is_pointer<T>:检测 T 是否为指针类型。
  • std::is_class<T>:检测 T 是否为类类型。
  • std::decay<T>:用于模拟函数参数传递时的类型退化行为。

学习萃取技术的目的主要有两个:

  1. 了解实现:了解标准库中许多萃取技术的实现方法。
  2. 灵活应用:灵活运用并组合这些实现方法,写出功能更强大、更优雅和实用的代码。

2. 固定萃取技术

固定萃取技术主要关注于如何通过输入一个固定的类型信息,从而萃取或推导出另一种类型。这种技术在实际编程中非常有用,尤其是在处理需要类型转换的场景。

2.1 固定萃取常规范例

考虑下面的函数模板 funcsum(),该模板用于计算数组中所有元素的总和。通过引入固定萃取,可以根据输入类型自动确定累加和的类型,从而避免潜在的类型溢出问题。

#include <iostream>

// 固定萃取类模板的泛化版本
template<typename T>
struct SumFixedTraitraits; // 不需要实现代码,因为不需要用该版本进行实例化

// 各个固定萃取类模板的特化版本
// (1) 传入的是 char 类型时,返回的时 int 类型
template<>
struct SumFixedTraitraits<char> // char 表示进来的是 char 类型
{
    using sumT = int; // sumT 为 int 类型,代表返回的是一个 int 类型
};

// (2) 传入的是 int 类型时,返回__int64(long long/int64_t) 类型
template<>
struct SumFixedTraitraits<int> // int 表示进来的是 int 类型
{
    using sumT = __int64; // sumT 为 __int64 类型,代表返回的是一个 __int64 类型
};

template<typename T>
auto funcsum(const T* begin, const T* end)
{
    // 传入一个类型(T),返回一个类型(sumT),这是固定萃取的运用
    using sumT = typename SumFixedTraitraits<T>::sumT;

    sumT sum{};
    for(;;)
    {
        sum += (*begin);
        if(begin == end)
        {
            break;
        }
        ++begin;
    }
    return sum;
}

int main()
{
    int myintarray1[] = {10,15,20};
    int myintarray2[] ={1000000000,1500000000,2000000000}; //10亿,15亿,20亿
    char mychararray[] = "abc"; //97,98,99
    std::cout << funcsum(&myintarray1[0], &myintarray1[2]) << std::endl; // 45 
    std::cout << funcsum(&myintarray2[0], &myintarray2[2]) << std::endl; // 4500000000
    std::cout <<(int)(funcsum(&mychararray[0], &mychararray[2])) << std::endl; // 294
    return 0;
}

通过此示例可见,固定萃取类模板(SumFixedTraits)通过输入某个类型,得到另外一个适当的类型,这在模板与泛型编程中是一种非常常见且有用的应用方式。

2.2 迭代器范例

在 C++ 中,迭代器萃取技术是一种广泛用于识别和处理不同类型迭代器的方法。该技术依赖于 iterator_traits 类模板,这是一个特定的固定萃取类模板,用于从任何迭代器类型中提取有用的信息,如迭代器类别、指向的值类型等。

迭代器分为以下5类。

  1. 输出型迭代器(Output Iterator):struct output_iterator_tag
  2. 输入型迭代器(Input Iterator):struct input_iterator_tag
  3. 前向迭代器(Forward Iterator):struct forward_iterator_tag
  4. 双向迭代器(Bidirectional Iterator):struct bidirectional_iterator_tag
  5. 随机访问迭代器(Random-Access Iterator):struct random_access_iterator_tag

利用 iterator_traits,可以编写与迭代器类型无关的代码,从而增强代码的泛型能力和灵活性。下面是一个示例,展示如何使用 iterator_traits 来识别迭代器的类别并据此打印出相应的信息:

#include <iostream>
#include <iterator>
#include <vector>
#include <list>
#include <forward_list>
#include <sstream>

template <typename T>
void display_category(T iter)
{
    // 萃取机
    typedef typename std::iterator_traits<T>::iterator_category category;

    // 检查迭代器类别并打印相关信息
    if (std::is_same<category, std::random_access_iterator_tag>::value)
    {
        std::cout << "Random Access Iterator" << std::endl;
    }
    else if (std::is_same<category, std::bidirectional_iterator_tag>::value)
    {
        std::cout << "Bidirectional Iterator" << std::endl;
    }
    else if (std::is_same<category, std::forward_iterator_tag>::value)
    {
        std::cout << "Forward Iterator" << std::endl;
    }
    else if (std::is_same<category, std::input_iterator_tag>::value)
    {
        std::cout << "Input Iterator" << std::endl;
    }
    else if (std::is_same<category, std::output_iterator_tag>::value)
    {
        std::cout << "Output Iterator" << std::endl;
    }
}

int main()
{
    // Random Access Iterator
    std::vector<int> vec{1, 2, 3};
    display_category(vec.begin());

    // Bidirectional Iterator
    std::list<int> lst{1, 2, 3};
    display_category(lst.begin());

    // Forward Iterator
    std::forward_list<int> flst{1, 2, 3};
    display_category(flst.begin());

    // Input Iterator
    std::istringstream iss("1 2 3");
    std::istream_iterator<int> in_it(iss);
    display_category(in_it);

    // Output Iterator
    std::ostream_iterator<int> out_it(std::cout, " ");
    display_category(out_it);

    return 0;
}

这个示例清晰地展示了如何根据迭代器的特性来判断其类型,并根据类型进行相应的操作。通过这种方式,可以编写更灵活和高效的通用代码。

接下来,看一个更实际的应用,即如何使用 iterator_traits 来决定如何有效地增加迭代器的位置:

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

// 针对随机访问迭代器的优化实现
template <typename RandomAccessIterator, typename Distance>
void custom_advance_impl(RandomAccessIterator& it, Distance n, std::random_access_iterator_tag)
{
    std::cout << "Using random access iterator optimization.\n";
    it += n;
}

// 适用于其他类型迭代器的通用实现
template <typename InputIterator, typename Distance>
void custom_advance_impl(InputIterator& it, Distance n, std::input_iterator_tag)
{
    std::cout << "Using single step iteration.\n";
    while (n--) ++it;
}

// 通用的advance函数,根据传入的迭代器类型选择最优的前进方式
template <typename Iterator, typename Distance>
void custom_advance(Iterator& it, Distance n)
{
    // 使用iterator_traits获取迭代器的类别
    using category = typename std::iterator_traits<Iterator>::iterator_category;
    custom_advance_impl(it, n, category());
}

int main()
{
    std::vector<int> vec = {0, 1, 2, 3, 4, 5};
    auto vec_it = vec.begin();
    custom_advance(vec_it, 3);
    std::cout << "Element in vector at position 3: " << *vec_it << std::endl;

    std::list<int> lst = {0, 1, 2, 3, 4, 5};
    auto lst_it = lst.begin();
    custom_advance(lst_it, 3);
    std::cout << "Element in list at position 3: " << *lst_it << std::endl;
}

在这个例子中,定义了一个 custom_advance 函数模板,该函数接受任何类型的迭代器和一个距离参数。该函数使用 iterator_traits 来获取迭代器的类别,并根据类别调用不同的辅助函数 custom_advance_impl

  • 对于随机访问迭代器,可以直接跳过 n 个元素,这是最有效的。
  • 对于输入迭代器或其他不支持随机访问的迭代器,我们只能一步步前进。

这个例子演示了如何通过萃取迭代器的特性来选择最合适的算法实现,从而使代码既通用又高效。这正是固定萃取技术在 C++ 标准库中的典型应用——通过传入迭代器类型,萃取出其种类,并据此采取最优的操作策略。

2.3 通过容器(数组)类型萃取元素类型范例

在C++模板和泛型编程中,萃取技术扮演了一个关键角色,尤其是在处理容器和数组时,通过萃取元素类型可以极大地增强代码的通用性和灵活性。

1. 常规实现 GetEleType 类模板

首先定义名为 GetEleType 的类模板,该模板旨在从多种容器和数组类型中提取元素类型。以下是此类模板的详细实现:

#include <iostream>
#include <vector>
#include <list>
#include <typeinfo>  // 需要包含头文件以使用 typeid

// 泛化版本的模板,没有实现体,仅用于被特化
template<typename T>
struct GetEleType;

// 特化版本对 std::vector 类型进行特化
template<typename T>
struct GetEleType<std::vector<T>>
{
    using type = T;
};

// 特化版本对 std::list 类型进行特化
template<typename T>
struct GetEleType<std::list<T>>
{
    using type = T;
};

// 特化版本对固定大小的数组进行特化
template<typename T, std::size_t Size>
struct GetEleType<T[Size]>
{
    using type = T;
    static const std::size_t size = Size;  // 增加一个静态常量成员存储数组的大小
};

int main()
{
    // 使用 typeid().name() 获取类型名称,输出 vector<double> 的元素类型
    std::cout << "vector<double> 的元素类型为:" << typeid(GetEleType<std::vector<double>>::type).name() << std::endl;
    
    // 输出 list<int> 的元素类型
    std::cout << "list<int> 的元素类型为:" << typeid(GetEleType<std::list<int>>::type).name() << std::endl;
    
    // 输出 float[45] 的元素类型
    std::cout << "float[45] 的元素类型为:" << typeid(GetEleType<float[45]>::type).name() << std::endl;
    
    // 输出 float[45] 的数组元素数量
    std::cout << "float[45] 的数组元素数量为:" << GetEleType<float[45]>::size << std::endl;
    
    return 0;
}

2. 引入 PrintEleType 函数模板

为了提升代码复用性,引入名为 PrintEleType 的函数模板,负责输出指定容器或数组的元素类型:

template<typename T>
void PrintEleType(const T &container)
{
    // 输出元素类型
    std::cout << "容器(数组)的元素类型为:" << typeid(typename GetEleType<T>::type).name() << std::endl;
}
int main()
{
    std::vector<double> mydblvec;
    PrintEleType(mydblvec);
    std::list<int> myintlist;
    PrintEleType(myintlist);
    float myfloatarr[45];
    PrintEleType(myfloatarr);

    return 0;
}

3. 简化 GetEleType 类模板的实现

进一步简化 GetEleType 类模板,使其泛化版本直接支持所有标准容器类型,仅保留数组类型的特化:

// 泛化版本的模板,用泛化版本实现对容器类型的支持
template<typename T>
struct GetEleType
{
    using type = typename T::value_type; // 针对容器
};
// 特化版本对固定大小的数组进行特化
template<typename T, std::size_t Size>
struct GetEleType<T[Size]>
{
    using type = T;
    static const std::size_t size = Size;  // 增加一个静态常量成员存储数组的大小
};

并对 PrintEleType 进行调整,使用别名模板简化类型名称的使用:

// 别名模板
template<typename T>
using EleType = typename GetEleType<T>::type;

template<typename T>
void PrintEleType(const T &container)
{
    // 输出元素类型
    // std::cout << "容器(数组)的元素类型为:" << typeid(typename GetEleType<T>::type).name() << std::endl;
    std::cout << "容器(数组)的元素类型为:" << typeid(EleType<T>).name() << std::endl;
}

通过上述示例与改进,展示了萃取技术如何在实际编程中简化操作,同时提供了一种类型安全的方式来处理不同的数据结构。

2.4 引用类型的移除和增加

1. 引用类型的移除

C++ 11标准库中提供了一个std::remove_reference类模板,如果传递进来的模板参数是一个引用类型,则会把这个引用类型中的引用部分删除。

可以通过一个简单的示例来观察其用法:

#include <iostream>

template<class T1, class T2>
void print_is_same()
{
    std::cout << "T1 类型为:" << typeid(T1).name() <<std::endl;
    std::cout << "T2 类型为:" << typeid(T2).name() <<std::endl;
    std::cout << "T1 和 T2 类型是否相等:" << std::is_same<T1, T2>() <<std::endl;
}

int main()
{
    std::remove_reference<int>::type a;
    std::remove_reference<int&>::type b;
    std::remove_reference<int&&>::type c;
    print_is_same<decltype(a),decltype(b)>();
    print_is_same<decltype(a), decltype(c)>();
    return 0;
}

运行程序,结果如下。

T1 类型为:i
T2 类型为:i
T1 和 T2 类型是否相等:1
T1 类型为:i
T2 类型为:i
T1 和 T2 类型是否相等:1

std::remove_reference也是一个 trait 类模板,自己实现一个类似的功能。

#include <iostream>
#include <boost/type_index.hpp> // 这是第三方库

//泛化版本
template<typename T>
struct RemoveReference
{
    using type = T;
};

//特化版本
template<typename T>
struct RemoveReference<T&>
{
    using type = T;
};

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

template< typename T >
using RemoveReference_t = typename RemoveReference<T>::type;

int main()
{
    int&& a2 = 12;
    RemoveReference_t<decltype(a2)> b2 = 125;
    int i = 64;
    int& c2= i;
    RemoveReference_t<decltype(c2)> d2 = 500;
    using boost::typeindex::type_id_with_cvr;
    std::cout << "b2=" <<type_id_with_cvr<decltype(b2)>().pretty_name() << std::endl;
    std::cout << "d2=" <<type_id_with_cvr<decltype(d2)>().pretty_name() << std::endl;
    return 0;
}

2. 引用类型的增加

所谓引用类型的增加,其实就是根据给定的类型创建一个左值或右值引用。

C++ 11 标准库中提供了一个std:: add_lvalue_reference类模板,用于传入一个类型,返回该类型对应的左值引用类型。例如,传入一个int类型,返回int&类型。C++ 11 也提供了一个std:: add_rvalue_reference类模板,用于传入一个类型,返回该类型对应的右值引用类型。

例如,传入一个int类型,返回int&&类型。相对应地,还有std::is_lvalue_referencestd::is_rvalue_reference类模板,用于判断某个类型是否是左值引用类型或右值引用类型。看一下这些类模板的基本用法。

#include <iostream>

int main()
{
    int a = 15;
    std::add_lvalue_reference<decltype(a)>::type b = a; //b的类型为int &
    std::add_rvalue_reference<decltype(a)>::type c = 16; //c的类型为int &&

    using btype = std::add_lvalue_reference_t<int>;    //_t是别名模板
    std::cout <<std::is_same<int&,btype>() << std::endl; // 1

    using ctype =std::add_rvalue_reference_t<int>;
    std::cout <<std::is_lvalue_reference<btype>::value << std::endl; // 1
    std::cout <<std::is_rvalue_reference<ctype>::value << std::endl; // 1

    std::add_rvalue_reference_t<int&> cc1 = a; // cc1的类型为int &,这里涉及引用折叠:& 和&&折叠得到&
    std::add_rvalue_reference_t<int&&>cc2 = 16; // cc2的类型为int &&,这里涉及引用折叠: &&和&&折叠得到&&

    return 0;
}

2.5 const 修饰符的移除

在 C++ 中,处理类型属性时经常需要对 const 修饰符进行操作。为了提供一种系统化的方法来移除类型中的 const 修饰符,C++ 标准库提供了 std::remove_const 类模板。类似地,可以实现一个自定义的 RemoveConst 类模板来展示这一功能。

以下是 RemoveConst 类模板的实现,包括泛化版本和针对 const 类型的特化版本:

#include <iostream>

// 泛化版本
template<typename T>
struct RemoveConst
{
    using type = T;
};

// 特化版本,用于移除 const 修饰符
template<typename T>
struct RemoveConst<const T>
{
    using type = T;
};

// 别名模板,简化类型使用
template<typename T>
using RemoveConst_t = typename RemoveConst<T>::type;

int main()
{
    RemoveConst_t<const int> nca = 15; // nca是int类型
    nca = 18; // 可以给nca重新赋值
    return 0;
}

在上述代码中,RemoveConst 类模板通过特化处理,精确移除了传入类型的 const 修饰符。这样,即使原始类型是 const int,在去除 const 后也能够对其重新赋值。

std::remove_const 的工作原理与 RemoveConst 类似,它也是通过模板特化来移除类型的 const 限定。以下是 std::remove_const 的基本用法:

#include <iostream>

int main()
{
    std::remove_const<const int>::type a = 10; // a 是 int 类型
    a = 20; // 可以重新赋值
    std::cout << "a 的新值为: " << a << std::endl;

    return 0;
}

通过使用 std::remove_const,可以有效地处理被 const 修饰的类型,使得这些类型在需要时可以被修改。这一点在模板编程和元编程中尤为重要,因为它允许开发者编写出更灵活、更通用的代码。此外,理解和运用这些类型萃取技术可以帮助开发者更好地掌握 C++ 的类型系统,从而编写出更为健壯和高效的程序。

2.6 退化技术

在 C++ 编程中,类型退化是一个关键的概念,尤其显著于函数模板的类型推导过程中。类型退化(type decay)指的是在类型作为函数模板参数传递时,某些类型修饰符被自动丢弃的现象。这包括但不限于 constvolatile 修饰符的移除,以及数组和函数类型向指针类型的转换。

此过程的根本目的在于简化函数调用和参数处理机制。例如,数组在传递给函数时自动退化为指针,函数类型则退化为对应的函数指针。这些行为保证了类型系统的整洁和一致性。为了有效模拟通过函数模板参数传递时发生的退化行为,C++11 引入了 std::decay 类模板。该模板是类型萃取的一部分,用于展示和处理类型退化。

#include <iostream>
#include <boost/type_index.hpp> // 第三方库

template<typename T>
void printDecayType()
{
    using DecayedType = typename std::decay<T>::type;
    // 使用 boost::typeindex::type_id_with_cvr<>().pretty_name() 获取更易读的类型名称
    std::cout << "Original type: " << boost::typeindex::type_id_with_cvr<T>().pretty_name()
              << ", Decayed type: " << boost::typeindex::type_id_with_cvr<DecayedType>().pretty_name() << std::endl;
}

int main() {
    printDecayType<const int>();         // 将 const int 退化为 int
    printDecayType<int[5]>();            // 将 int[5] 退化为 int*
    printDecayType<int(int)>();          // 将 int(int) 退化为 int(*)(int)
    printDecayType<void()>();            // 将 void() 退化为 void(*)()
    return 0;
}

在此程序中:

  • const int 被退化为 int,其中 const 修饰符被移除。
  • 数组类型 int[5] 被退化为指针 int*
  • 函数类型 int(int) 被退化为函数指针 int(*)(int)
  • 无参数的函数类型 void() 同样退化为函数指针 void(*)()

理解并掌握类型退化的概念是精通 C++ 类型系统的重要一环,同时也是高效运用模板与泛型编程的基础。通过利用 std::decay 及相关技术,开发者能够编写出更加健壮、灵活且高效的代码。

总结

固定萃取技术(Type Traits)是 C++ 中的一种高级编程技术,它允许程序员在编译时查询和操作类型信息。这种技术广泛应用于模板编程,特别是在实现泛型算法和数据结构、优化代码以及提供编译时类型检查和选择性编译时非常有用。

主要特点与应用

  1. 编译时类型信息:固定萃取技术利用模板和元编程的能力,提供了一种在编译时而非运行时检查和使用类型信息的方法。这可以极大地增加代码的灵活性和安全性。
  2. 类型判断:通过标准库中提供的萃取,如 std::is_integral, std::is_floating_point 等,可以根据不同的类型属性编写特定的代码,实现类型安全的操作。
  3. 代码优化:基于类型特征的不同,编译器可以优化生成的机器代码。例如,对于某些特定类型的操作,如果知道它们是 POD(Plain Old Data)类型,可以采用更快的内存操作策略。
  4. 条件编译:使用 std::enable_if 或者 SFINAE(Substitution Failure Is Not An Error)技术,可以根据类型特征有条件地启用或禁用某些函数重载,这对于创建高度通用且安全的模板库非常重要。