目录

1.  本章讨论了标签的用法。在我们经常使用的标准模板库(STL)中也存在标签的概念。STL将迭代器进行了划分,为不同的迭代器赋予了不同的标签(如双向迭代器、随机访问迭代器等)。在网络上搜索一下相关的概念,学习并了解STL中标签的用法,并于本章中标签的用法进行比较。

双向迭代器

随机访问迭代器

STL中的标签用法

2.  在本章中,我们讨论了使用函数参数或模板参数传递类别标签。STL将标签作为函数参数进行传递,这样做的一个好处是可以自动处理标签的继承关系。STL中的迭代器标签具有派生层次,比如前向迭代器是一种特殊的输入迭代器,这体现在标签体系中,是表示前向迭代器的标签forward_iterator_tag派生自input_iterator_tag。而对于本章所讨论的_distance实现来说,如果传入其中的第3个参数是前向迭代器,那么编译器会自动选择输入迭代器的版本进行计算。如果像本章所讨论的那样,使用模板参数来传递迭代器标签,则不能简单地通过std::is_same进行比较来实现类似的效果。请尝试引入新的元函数,在使用模板参数传递迭代器类别的算法中实现类似的标签匹配效果。更具体来说,实现的元函数应当具有如下调用方式:

3.  使用模板参数而非函数参数来传递标签信息还有另一个好处,我们不再需要提供标签类型的定义了。为了基于函数参数来传递标签信息,STL不得不引入类似下面的类型定义:

4.  STL提供了一个元函数is_base_of,用来判断某个类是否是另一个类的基类,使用这个元函数,修改第2章的_distance声明,使其更加简洁。基于修改后的元函数声明,再次考虑第3题:此时,我们是否需要迭代器标签的类型定义呢?为什么?

5.  在讨论DataCategory_的实现时,我们为其引入了一个名为helper的辅助元函数。它是声明在DataCategory_内部的。尝试将其提取到DataCategory_的外部。思考一下这种改进是否会像第一1章所讨论的那样,减少编译过程中所构造的实例数。尝试验证你的想法。

6.  本章介绍了MetaNN所使用的矩阵类Matrix,考虑如下声明:

7.  在子矩阵的讨论中,我们通过引入m_rowLen来确定两行的间距,除了这种方式,还可以标记连续两行中上一行结尾与下一行的开始之间的元素个数。考虑这种方式与本书所采用的方式之间的优劣。

8.  阅读并分析Batch、Array与Duplicate中针对标量的实现代码

9.  阅读并分析OneHotVector与ZeroMatrix的实现代码。

10.  ArrayImp的一个构造函数引入了元函数IsIterator来确保输入的参数是迭代器。能否去掉这个元函数,采用下面的函数声明:

11.  我们在讨论ArrayImp时,提到了在求值之后就不能进行修改。那么Matrix或者Batch类模板是否存在同样的问题呢?事实上,这两个模板有些特殊,即使是求值了之后再进行修改,其语义也是正确的。考虑一下原因。


1.  本章讨论了标签的用法。在我们经常使用的标准模板库(STL)中也存在标签的概念。STL将迭代器进行了划分,为不同的迭代器赋予了不同的标签(如双向迭代器、随机访问迭代器等)。在网络上搜索一下相关的概念,学习并了解STL中标签的用法,并于本章中标签的用法进行比较。

双向迭代器

双向迭代器是一种迭代器,它可以在容器中向前和向后遍历元素。它具备了向前迭代器的特性,可以使用递增运算符(++)向前移动,以及向后迭代器的特性,可以使用递减运算符(--)向后移动。

C++代码示例,展示了如何使用双向迭代器来遍历一个容器(例如vector)中的元素:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用双向迭代器向前遍历
    std::cout << "向前遍历:";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << " " << *it;
    }

    // 使用双向迭代器向后遍历
    std::cout << "\n向后遍历:";
    for (auto it = numbers.rbegin(); it != numbers.rend(); ++it) {
        std::cout << " " << *it;
    }

    return 0;
}

此代码演示了通过使用`begin()`和`end()`方法获取双向迭代器来遍历vector容器中的元素。第一个循环使用递增运算符向前遍历,第二个循环使用递减运算符向后遍历。请注意,`rbegin()`和`rend()`方法返回的是逆向的双向迭代器,以实现向后遍历。

随机访问迭代器

随机访问迭代器是一种迭代器,它具有在常量时间内跳转到容器中的任何位置的能力,并支持以常量时间进行算术运算(如加法和减法),以便在容器中定位元素。随机访问迭代器还可以使用指针算术运算符(例如`[]`)直接访问容器中的元素。

C++代码示例,展示了如何使用随机访问迭代器来遍历一个数组:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用随机访问迭代器遍历
    std::cout << "遍历数组:";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << " " << *it;
    }

    // 使用随机访问迭代器进行指针算术运算
    std::cout << "\n通过随机访问迭代器直接访问元素:";
    std::cout << " " << numbers[0];
    std::cout << " " << numbers[1];
    std::cout << " " << numbers[2];
    std::cout << " " << numbers[3];
    std::cout << " " << numbers[4];

    return 0;
}

此代码演示了如何使用`begin()`和`end()`方法获取随机访问迭代器来遍历vector容器中的元素。随机访问迭代器具有与指针相似的行为,可以使用递增运算符`++`和递减运算符`--`在容器中移动,并使用指针算术运算符`[]`直接访问元素。

STL中的标签用法

在STL(标准模板库)中,标签(Tag)是一种用于区分不同算法或函数重载的机制。标签通常以类型为参数的形式出现,以便在编译时根据标签来选择适当的函数或算法。标签可以是自定义的结构体、类或枚举类型。

STL标签的使用示例:1. 迭代器标签:STL中的算法通常接受一个迭代器范围来操作容器中的元素。迭代器标签用于指定迭代器的类型,以便算法知道如何处理它们。例如,`std::sort`算法有两个重载,一个接受随机访问迭代器,一个接受双向迭代器:

template <class RandomIt>
void sort(RandomIt first, RandomIt last);

template <class BidirIt>
void sort(BidirIt first, BidirIt last);

在这里,`RandomIt`和`BidirIt`就是迭代器标签,它们用于区分不同的重载版本。

2. 容器标签:STL中的一些算法和函数根据不同的容器特性进行优化。容器标签用于指定容器的类型,以便选择相应的实现。例如,`std::back_inserter`函数用于在容器的末尾插入元素:

template<class Container>
std::back_insert_iterator<Container> back_inserter(Container& c);

在这里,`Container`就是容器标签,它指定了`back_inserter`函数适用的容器类型。3. 数值标签:STL中的数值算法(如`std::accumulate`和`std::inner_product`)可以接受数值标签,以指定用于计算的数值类型。例如,`std::accumulate`算法有以下两个重载:

template<class InputIt, class T>
T accumulate(InputIt first, InputIt last, T init);

template<class InputIt, class T, class BinaryOp>
T accumulate(InputIt first, InputIt last, T init, BinaryOp op);

第一个重载的数值标签使用`T`,而第二个重载可以通过指定额外的`BinaryOp`参数来定义特定的数值计算操作。

这些示例展示了STL中标签的使用方式。标签可以帮助算法或函数在编译时选择正确的实现,提高代码效率和可读性。

本章中标签的用法进行比较,就留给你们思考!!!

2.  在本章中,我们讨论了使用函数参数或模板参数传递类别标签。STL将标签作为函数参数进行传递,这样做的一个好处是可以自动处理标签的继承关系。STL中的迭代器标签具有派生层次,比如前向迭代器是一种特殊的输入迭代器,这体现在标签体系中,是表示前向迭代器的标签forward_iterator_tag派生自input_iterator_tag。而对于本章所讨论的_distance实现来说,如果传入其中的第3个参数是前向迭代器,那么编译器会自动选择输入迭代器的版本进行计算。如果像本章所讨论的那样,使用模板参数来传递迭代器标签,则不能简单地通过std::is_same进行比较来实现类似的效果。请尝试引入新的元函数,在使用模板参数传递迭代器类别的算法中实现类似的标签匹配效果。更具体来说,实现的元函数应当具有如下调用方式:

template<typename TIterTag,typename _InputIterator,
            enable_if_t <FUN<TIterTag,
                                input_iterator_tag,
                                forward_iterator_tag,
                                bidirectional_iterator_tag>>*= nullptr>
inline auto _distance(_InputIterator b, _InputIterator e);

其中FUN是需要实现的元函数,上述调用表面,如果TIterTag是input_iterator_tag(输入迭代器),forward_iterator_tag(前向迭代器)或者bidirectional_iterator_tag(双向迭代器)之一,编译器就会选择当前的_distance版本。

为了实现通过模板参数传递迭代器标签并进行标签匹配的效果,可以使用SFINAE(Substitution Failure Is Not An Error)技术和模板特化。

首先,定义一个元函数 `FUN` ,用于检查一个迭代器标签是否匹配所需的标签:
 

template<typename T, typename U>
struct is_same_tag {
    static constexpr bool value = false;
};

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

template<typename TIterTag, typename TInputIter,
         typename = std::enable_if_t<is_same_tag<TIterTag, typename std::iterator_traits<TInputIter>::iterator_category>::value>>
struct FUN {
    static constexpr bool value = true;
};

然后,在 `_distance` 函数中使用该元函数进行标签匹配。根据传入的 `TIterTag` 参数的类型,选择相应的算法版本。

template<typename TIterTag, typename TInputIterator,
         typename std::enable_if_t<FUN<TIterTag, TInputIterator>::value>* = nullptr>
inline auto _distance(TInputIterator b, TInputIterator e) {
    // 实现具体的距离计算逻辑
    // ...
}

通过对 `TIterTag` 和 `std::iterator_traits` 的 `iterator_category` 进行比较,可以实现对迭代器标签进行匹配,并选择正确的 `_distance` 版本。

template<typename TIterTag, typename TInputIterator,
         typename = std::enable_if_t<FUN<TIterTag, TInputIterator>::value>>
inline auto _distance(TInputIterator b, TInputIterator e) {
    // 实现具体的距离计算逻辑
    // ...
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用输入迭代器标签进行计算
    auto dist = _distance<std::input_iterator_tag>(vec.begin(), vec.end());
    
    // 使用前向迭代器标签进行计算
    auto dist2 = _distance<std::forward_iterator_tag>(vec.begin(), vec.end());

    // 使用双向迭代器标签进行计算
    auto dist3 = _distance<std::bidirectional_iterator_tag>(vec.begin(), vec.end());

    return 0;
}

这样,根据传入的迭代器标签类型,编译器会自动选择 `_distance` 的适当版本进行距离计算。

3.  使用模板参数而非函数参数来传递标签信息还有另一个好处,我们不再需要提供标签类型的定义了。为了基于函数参数来传递标签信息,STL不得不引入类似下面的类型定义:

struct output_iterator_tag {};

但如果使用模板参数来传递标签,相应的类型定义就可以被省略:

struct output_iterator_tag;

分析一下为什么会这样

当使用模板参数而非函数参数来传递标签信息时,不需要提供标签类型的定义的原因如下:

1. 编译器根据传递给模板参数的具体类型来确定标签的类型。例如,使用模板参数 `T` 来传递迭代器标签,可以根据 `T` 的类型来确定具体是哪个标签,而不需要提前定义 `struct output_iterator_tag {}` 这样的类型。

2. 模板参数是一种编译时的机制,编译器可以根据模板实例化的具体类型来进行类型推断和选择代码路径。模板参数本身就代表了某个类型,因此不需要进行额外的类型定义。

3. 类型定义(例如 `struct output_iterator_tag {}`)在传递标签信息和进行类型匹配中起到了辅助作用。而对于模板参数来说,传递类型信息和进行类型匹配是自然的一部分,不需要专门定义一个结构体。

因此,当使用模板参数来传递标签信息时,不需要提供类似于 `struct output_iterator_tag {}` 这样的类型定义。编译器可以根据传递给模板参数的具体类型来确定标签的类型,从而避免了额外的类型定义和冗余的代码。

4.  STL提供了一个元函数is_base_of,用来判断某个类是否是另一个类的基类,使用这个元函数,修改第2章的_distance声明,使其更加简洁。基于修改后的元函数声明,再次考虑第3题:此时,我们是否需要迭代器标签的类型定义呢?为什么?

留给你们思考!!!

5.  在讨论DataCategory_的实现时,我们为其引入了一个名为helper的辅助元函数。它是声明在DataCategory_内部的。尝试将其提取到DataCategory_的外部。思考一下这种改进是否会像第一1章所讨论的那样,减少编译过程中所构造的实例数。尝试验证你的想法。

留给你们思考!!!

6.  本章介绍了MetaNN所使用的矩阵类Matrix,考虑如下声明:

vector<Matrix<int, DeviceTags::CPU>> a(3, {2, 5});

我们的本意是声明一个变量,包含3个矩阵。之后,我们希望对向量中的3个矩阵分别赋值。考虑一下这种做法是否行得通?如果不行,会有什么问题(提示:MetaNN中的矩阵是浅拷贝的)?

在MetaNN中,矩阵类 `Matrix` 是浅拷贝的,这意味着拷贝构造函数和拷贝赋值运算符只会复制矩阵的结构和元数据,而不会复制矩阵的数据本身。因此,尝试对向量中的每个矩阵进行赋值是有问题的。

在以下代码中:

vector<Matrix<int, DeviceTags::CPU>> a(3, {2, 5});

`vector` 将使用默认拷贝构造函数来创建 `a` ,其中每个元素都被拷贝了三次,但是这些拷贝的矩阵都指向相同的数据。

因此,如果尝试对向量中的每个矩阵进行分别赋值,由于所有矩阵共享相同数据内存,赋值操作将影响到所有矩阵。这可能导致意外结果,并且不符合最初的意图。

要分别对每个矩阵赋值,需要确保每个矩阵都拥有自己的独立数据。可以通过使用不同的矩阵对象来实现,或者通过手动创建每个矩阵的副本来确保数据的独立性。

7.  在子矩阵的讨论中,我们通过引入m_rowLen来确定两行的间距,除了这种方式,还可以标记连续两行中上一行结尾与下一行的开始之间的元素个数。考虑这种方式与本书所采用的方式之间的优劣。

留给你们思考!!!

8.  阅读并分析Batch、Array与Duplicate中针对标量的实现代码

留给你们分析!!!

9.  阅读并分析OneHotVector与ZeroMatrix的实现代码。

留给你们分析!!!

10.  ArrayImp的一个构造函数引入了元函数IsIterator来确保输入的参数是迭代器。能否去掉这个元函数,采用下面的函数声明:

template <typename TIterator>
ArrayImp(TIterator b, TIterator e)

为什么?

留给你们思考!!!

11.  我们在讨论ArrayImp时,提到了在求值之后就不能进行修改。那么Matrix或者Batch类模板是否存在同样的问题呢?事实上,这两个模板有些特殊,即使是求值了之后再进行修改,其语义也是正确的。考虑一下原因。

在MetaNN中,Matrix和Batch类模板与ArrayImp不同,即使在求值之后进行修改也是语义上合理的。这是因为Matrix和Batch是“动态计算”模板,其内部包含延迟计算机制。

1. Matrix类模板:Matrix在进行加法、乘法、转置等操作时,并不立即执行计算,而是构建了对应的计算图。只有在需要结果时,才会进行实际的计算(即求值)。因此,即使在求值之后对Matrix进行修改,只需重新求值计算即可,其语义仍然是正确的。

2. Batch类模板:Batch类模板适用于批量数据的处理,具有类似于Matrix的延迟计算机制。它可以表示由多个矩阵组成的批量数据,并支持各种批量操作。同样,即使在求值之后对Batch进行修改,也可以通过重新求值来重新计算批量操作的结果。

这种延迟计算的机制使得Matrix和Batch类模板更加灵活和高效,可以在保持数据一致性的前提下进行修改。当需要结果时,再进行计算和求值。这种设计允许用户在动态计算模型中进行灵活的修改和调整。

总结而言,Matrix和Batch类模板具有延迟计算机制,允许在求值之后进行修改,并通过重新求值来重新计算结果,保持语义上的正确性。这使得它们在表示动态计算模型和处理批量数据时更加灵活和强大。

12.  本章所讨论的数据结构中,有一些包含了EvalBuffer这样的数据成员,用于存储求值之后的结果。但像Matrix这样的类模板就没有包含类似的数据成员。思考一下原因。

Matrix类模板没有包含类似EvalBuffer的数据成员,是因为Matrix的求值结果直接存储在矩阵对象本身。

在MetaNN中,Matrix是动态计算模板,当进行加法、乘法、转置等操作时,并不立即执行计算,而是构建了对应的计算图。只有在需要结果时,才会进行实际的计算(即求值)。而计算的结果会直接存储在Matrix对象中,而不需要额外的数据成员来存储求值结果。

当Matrix进行求值之后,其内部的数据会被更新为求值的结果,这样就可以直接通过Matrix对象来访问和操作计算结果。因此,不需要像EvalBuffer那样的额外数据成员来存储求值结果。

这种设计使得Matrix的使用更加简洁和高效,避免了额外的内存开销和数据拷贝操作。求值结果直接存储在Matrix对象中,可以直接通过对象来访问,提高了代码的可读性和执行效率。

总结而言,Matrix类模板没有包含EvalBuffer类似的数据成员,是因为Matrix的求值结果直接存储在矩阵对象本身。这种设计使得Matrix的使用更加简洁高效,并且避免了额外的内存开销和数据拷贝操作。

11-21 19:35