在这一部分中,我们将完成基本的四则运算和由它们组合而成的初等函数的构建。你需要在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中对应元素的结果。具体来说,对于下面这个实现:
template<typename T, typename F>
auto map(const std::vector<T>& vec, F func) -> std::vector<decltype(func(std::declval<T>()))> {
std::vector<decltype(func(std::declval<T>()))> 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 引入的一个工具,用于在编译时模拟一个对象的“假实例”,以便在不实际构造对象的情况下推导类型。
decltype(func(std::declval<T>()))
在
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文件夹的绝对目录直接粘贴到这里,这个文件夹的目录应该形如
/home/hexu/learn/uc-modern-cpp-student/cc/build
可以切换到
build目录下,执行pwd命令来获取绝对路径。
好了,不出意外的话,就再也别动~/.bashrc了。现在还有一个frontend/framework/basis/test_task1.py文件。切换到目录frontend/framework/basis/,直接运行task1到task6的文件,如果没有任何报错,说明你已经完成了这一关!🎉