### 第一部分:基本操作 #### 基本函数的构建 在这一部分中,我们将完成基本的四则运算和由它们组合而成的初等函数的构建。你需要在cc/operators中补全`ops.h`和`ops.cc`的内容。 **[TASK 1]** 在`ops.h`中,你需要补全以下函数的实现: - `mul`函数,输入为两个数`a`、`b`,输出为它们的乘积。 - `id`函数,将输入原样输出。 - `add`函数,输入为两个数`a`、`b`,输出为它们的和。 - `neg`函数,输入为`a`,输出为`-a`。 - `lt`函数,输入为两个数`a`、`b`,输出为`(float)(a < b)`。 - `eq`函数,输入为两个数`a`、`b`,输出为`(float)(a == b)`。 - `max`函数,输入为两个数`a`、`b`,输出为`a`和`b`中较大的那个。 它们都是模板函数,相信你已经注意到了,它们都被定义在`.h`文件中,而不是`.cc`文件中,这与C++的模板的实例化机制和编译模型有关。 模板的实例化机制:模板函数或模板类并不是真正的代码,而是一个“蓝图”或“模式”,编译器在编译时根据这个蓝图生成具体的代码。这个过程称为模板实例化。例如,当你使用一个模板函数时,编译器会根据你传递的类型参数生成一个具体的函数版本。这个生成的过程发生在编译时。 编译模型:C++采用的是分离编译模型,即每个源文件(.cc 或 .cpp 文件)是独立编译的。编译器在编译一个源文件时,只会看到该源文件及其包含的头文件中的内容。如果你将模板函数的定义放在源文件中,其他源文件在编译时无法看到模板的定义,因此无法生成对应的实例化代码。 另外,你应当还注意到了我们为这两个文件提供了名叫`operators`的命名空间(namespace)。主要是为了防止不同命名空间中的重名冲突。 **[TASK 2]** 在`ops.cc`中,你需要完成以下函数的实现: - `is_close`函数,输入为两个数`x`、`y`,输出为`(float)(abs(x - y) < epsilon)`。 - `sigmoid`函数,输入为`x`,为了方便计算,在输出时遵照下面的规则: $$ f(x) =\left\{\begin{matrix} \frac{1.0}{(1.0 + e^{-x})}, x\ge 0 \\ \frac{e^x}{(1.0 + e^{x})}, \mathrm{otherwise} \end{matrix}\right. $$ - `relu`函数,输入为`x`,输出为`x > 0.0 ? x : 0.0`。 - `inv`函数,输入为`x`,输出为`1.0 / x`。 - `inv_back`函数,用于计算$f(x)=\frac{1}{x}$的微分$f(x)\mathrm{d}x$,输入为`x`和`d`,输出为$-\frac{d}{x^2}$。 - `relu_back`函数,输入为`x`和`d`,输出为`x > 0.0 ? d*1.0 : 0.0`。 #### 函数式编程基础 实现`map`、`zipWith`和`reduce`。 `map`接受一个`std::vector`和一个函数作为输入,返回一个新的`std::vector`,其中每个元素都是输入函数应用于输入`std::vector`中对应元素的结果。具体来说,对于下面这个实现: ```cpp template auto map(const std::vector& vec, F func) -> std::vector()))> { std::vector()))> result; result.reserve(vec.size()); std::transform(vec.begin(), vec.end(), std::back_inserter(result), func); return result; } ``` 有几处可能让你感到疑惑的地方。 首先,这里的函数返回值居然和Python一样被后置了!`->` 是 C++11 引入的尾置返回类型语法。它的作用是将函数的返回类型放在函数参数列表之后,而不是放在函数名之前。在某些情况下,返回类型可能依赖于函数参数或模板参数,而这些信息在函数名之前是不可用的。尾置返回类型允许我们在函数参数列表之后推导返回类型。 > 例如,在`map`函数中,返回类型依赖于`func`的返回类型,而`func`的类型在函数名之前是未知的。使用尾置返回类型可以解决这个问题。 其次,我们使用了`std::declval`。`std::declval`是 C++11 引入的一个工具,用于在编译时模拟一个对象的“假实例”,以便在不实际构造对象的情况下推导类型。 ```cpp decltype(func(std::declval())) ``` > 在`map`函数中,我们需要推导`func`的返回类型。假设`func`是一个函数对象,接受`T`类型的参数并返回某种类型`R`,我们可以使用`std::declval`来模拟调用`func`的过程。 **[TASK 3]** 在`ops.cc`中,调用我们给出的`map`函数实现和你刚刚完成的`neg`函数,补全`negList`函数(大约需要1行代码)。 **[TASK 4]** 在`ops.h`中,仿照`map`函数,补全`zipWith`函数(大约需要10行代码)。`zipWidth`函数接受两个`vector`和一个函数`func`作为输入,要得到一个新的`vector`,这个`vector`中的元素都是两个`vector`逐元素进行函数`func`操作之后的结果。例如,对于`vec1 = [1, 2, 3]`,`vec2 = [5, 6, 7]`,`func`为`add`,那么将返回`[6, 8, 10]`。注意:在进行`zipWith`函数的实现时,你需要考虑输入的两个`std::vector`长度不一致的情况,对于这种情况,你简单地`throw`一个异常即可。 **[TASK 5]** 在`ops.cc`中,使用你实现的`zipWith`和`add`函数,实现`addLists`函数(大约需要1行代码)。 **[TASK 6]** 实际上你会发现`std::accumulate`(问一问LLM这个是个啥)就能够承担`reduce`函数的功能,因此你可以直接使用`std::accumulate`来实现`reduce`函数。这个任务需要你使用`reduce`函数实现`sumList`(将一个列表中的元素相加)和`prodList`(将一个列表中的元素相乘)函数(大约分别需要1行代码)。 #### 检查结果 做完了?很好,切换到`cc`,执行下面的语句来编译框架 ``` cmake -S . -B build cd build make ``` 现在,编辑系统环境变量 ``` echo 'export PYTHONPATH=/home/hce/uc-modern-cpp-student/cc/build' >> ~/.bashrc ``` 将??????替换为将刚刚生成的`build`文件夹的绝对目录直接粘贴到这里,这个文件夹的目录应该形如 ```Python /home/hexu/learn/uc-modern-cpp-student/cc/build ``` > 可以切换到`build`目录下,执行`pwd`命令来获取绝对路径。 好了,不出意外的话,就再也别动`~/.bashrc`了。现在还有一个`frontend/framework/basis/test_task1.py`文件。切换到目录`frontend/framework/basis/`,直接运行task1到task6的文件,如果没有任何报错,说明你已经完成了这一关!🎉