编写Rcpp包02:与R交互

一般而言,要用Rcpp写扩展包,通常是用于处理在R中处理比较慢的过程,或者调用一些在R中无法使用的能力,以提高程序运行的效率。既然这样,必然需要给C++程序传入一些参数,然后将计算结果返回给R。如果R和C++各玩各的,那就没有什么意思了。这篇文章将以斐波那契数列为切入点,介绍R与C++进行交互的方法。

我们主要实现以下三个斐波那契数列相关的函数:

- 数列第n项的值
- 数列的前n项列表
- 数列前n项的(加权)和

完整的实现可参考仓库 [HPDell/tutorial-Rcpp/02-DemoRcpp2](https://github.com/HPDell/tutorial-Rcpp/tree/master/02-DemoRcpp2) ,这里将代码分解开进行介绍。

# 数列第n项的值 -- 标量类型的参数与返回值

我们在分析一个Rcpp包的时候,往往先从最外层的R函数开始,一层层阅读,以理解整个包的设计,这样有助于先建立一个总体的概念再去了解细节的实现。但是在写Rcpp包的时候,根据我的经验,最好还是先从最底层的C++函数开始编写,再层层封装。好处有以下几个:

- 在具体实现C++函数的时候,比较容易确定所需的参数及其类型;
- 通过实现C++函数,可以明确参数需要哪些约束条件,并选择适当的处理位置;
- 可以通过一定方法在不编写R函数的时候对C++函数进行测试,甚至进行调试;
- 将C++与R的设计部分解耦,有助于日后将C++模块单独抽取成为一个库。

因此本文也将按照这个顺序进行介绍。

## 库函数

和上一篇一样,库函数是指没有写在 `RcppExports.cpp` 文件中的C++函数,是功能具体实现的函数。我们将自动生成的文件 `rcpp_hello_world.cpp` 重命名为 `fibonacci.cpp` 并删除 `rcpp_hello_world()` 函数,新建函数 `fibonacci_item()`

```cpp
// [[Rcpp::export]]
int fibonacci_item(size_t n) {
    int a0 = 0;
    int a1 = 1;
    int an = a1;
    switch (n)
    {
    case 0:
        an = 0;
        break;
    case 1:
        an = 1;
        break;
    default:
        for (size_t i = 2; i <= n; i++)
        {
            a0 = a1;
            a1 = an;
            an = a0 + a1;
        }
        break;
    }
  
    return an;
}
```

这也许不是一个最佳实现,但是可以体现C++编程的特点。注意第一行的注释,有这个注释存在,函数才能导出被Rcpp发现。这个函数中操作的都是C++基本类型,没有涉及Rcpp定义的类型。

## C++接口函数

C++接口函数的主要工作是将R的数据类型转换为C++基本类型或Rcpp以及其他库所提供的类型。一般而言,我们使用一个 `SEXP` 类型作为R与C++交互的中间类型,即使用这个类型的对象承接来自R的数据,并将结果转换为这个类型的对象返回给R。

我们在 `RcppExports.cpp` 文件中定义库函数的接口 `_DemoRcpp_fibonacci_item()`

```cpp
int fibonacci_item(size_t n);
RcppExport SEXP _DemoRcpp_fibonacci_item(SEXP nSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter<size_t>::type n(nSEXP);
    rcpp_result_gen = Rcpp::wrap(fibonacci_item(n));
    return rcpp_result_gen;
END_RCPP
}
```

在这个函数中,我们使用一个模板类 `Rcpp::traits::input_parameter<T>::type` 来指定将 `SEXP` 类型转换为何种具体的数据类型 `T` ,也就是模板参数。这个 `type` 类型其实是 `Rcpp::InputParameter<T>` 类型的别名,定义在 `struct Rcpp::traits::input_parameter` 结构体中,而查看 `Rcpp::InputParametert` 类型的定义,可以发现其构造函数就是接受一个 `SEXP` 类型的参数并将其保存在私有变量中,然后在这个类中提供了一个重载了的类型转换运算符 `T` :

```cpp
inline operator T() { return as<T>(x) ; }
```

这样就将变量在 `SEXP` 类型与 `T` 类型之间做显式或隐式类型转换了。在这个例子中,传入 `fibonacci_item()` 的参数需要一个 `size_t` 类型的参数,但是我们接受的参数 `nSEXP` 是一个 `SEXP` 类型的参数,但是我们创建了一个变量 `n` 作为中间变量,它可以隐式地转换为 `size_t` 类型,这样这个参数就可以当作 `size_t` 类型去使用了。

编写完了接口函数,我们将其添加到 `CallEntries` 中,

```cpp
static const R_CallMethodDef CallEntries[] = {
    {"_DemoRcpp_fibonacci_item", (DL_FUNC) &_DemoRcpp_fibonacci_item, 1},
//   ^                                      ^                         ^
//   R调用名称                               函数名                    参数个数
    {NULL, NULL, 0}
};
```

这样就完成了C++接口函数的编写。

## R接口函数与包函数

我们在 `RcppExports.R` 中继续封装,添加下面这个函数

```R
.fibonacci_item <- function(n) {
    .Call(`_DemoRcpp_fibonacci_item`, n)
}
```

这里 `.Call()` 函数的第一个参数就是上面提到的 `CallEntries` 中记录的调用名称,后面直接跟上所有需要传入的参数即可。

在包函数中,我们只需要调用这个R接口函数

```R
fibonacci_item <- function(n) {
   .fibonacci_item(n)
}
```

编译我们的包,就可以使用这个函数了。

```R
library(DemoRcpp)
fibonacci_item(10)
# [1] 89
```

## 参数校验

可以发现,在C++函数中,参数 `n` 的类型 `size_t` 表示一个非负整数(无符号长整型)。但是在R函数中,参数 `n` 是任意类型。这是我们就需要对 `n` 进行参数校验,在不符合条件时停止调用。这就是参数校验过程。修改后我们的包函数就变成了这个样子。

```R
fibonacci_item <- function(n) {
   if (!(is.numeric(n) || is.integer(n))) {
      stop("Parameter n must be numeric or integer.")
   } else if (n < 0) {
      stop("Parameter n must be a positive number.")
   }
   
   .fibonacci_item(n)
}
```

但事实上,我们总共写了四个函数,理论上为了保证健壮性,每个函数都有可能需要校验。如果每个函数都写校验,不仅写起来非常麻烦,事实上也容易拖慢运行速度。因此其实可以省略一些,尽量将校验的层级提高。比如这个包里面,除了包函数以外,其他都是内部函数,不会被其他包或库调用,而且这些函数也只会在一个地方被调用,所以就在包函数里面做参数校验就可以了。但是如果接口函数在多个函数中被调用了,或者库函数有可能被其他库调用了,那么还是给这些接口函数与库函数添加参数校验比较好。但总体上而言,具体在什么位置进行参数校验,还是要具体情况具体分析,结合一定的开发经验,最终确定其位置。

# 向量类型的参数与返回值

之前的例子中,我们只介绍了C++原生类型的参数。但是在R中,向量是使用更为广泛的一种数据类型,那么这种类型如何在Rcpp中进行对接呢?

## 数列的前n项列表 -- 向量类型返回值

Rcpp提供了一系列的类型用于表示向量

- 浮点型 `NumericVector`
- 整型 `IngeterVector`
- 字符串型 `CharacterVector`
- 布尔型 `LogicalVector`

详细用法可参考 [Data types](https://teuder.github.io/rcpp4everyone_en/070_data_types.html#vector-and-matrix) 中的介绍。

如果我们要返回一个向量,例如数列前 n 项的列表,就可以使用 `IntegerVector` 作为函数的返回值,例如

```cpp
// [[Rcpp::export]]
IntegerVector fibonacci_seq(size_t n) {
    IntegerVector fib_seq(n);
    switch (n)
    {
    case 0:
        break;
    case 1:
        fib_seq[0] = 1;
        break;
    case 2:
        fib_seq[0] = 1;
        fib_seq[1] = 1;
        break;
    default:
        fib_seq[0] = 1;
        fib_seq[1] = 1;
        for (size_t i = 2; i < n; i++)
        {
            fib_seq[i] = fib_seq[i-1] + fib_seq[i-2];
        }
        break;
    }
  
    return fib_seq;
}
```

接口函数和之前的没有什么太大区别,直接将 `IntegerVector` 包装成一个 `SEXP` 类型的返回值

```cpp
IntegerVector fibonacci_seq(size_t n);
RcppExport SEXP _DemoRcpp_fibonacci_seq(SEXP nSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter<size_t>::type n(nSEXP);
    rcpp_result_gen = Rcpp::wrap(fibonacci_seq(n));
    return rcpp_result_gen;
END_RCPP
}
```

这样我们就可以在C++中创建向量,返回给R。

## 数列前n项的加权和 -- 向量类型的参数

如果要接受向量作为参数,也是一样的。例如我们写函数 `fibonacci_sum()` 计算前数列前n项的加权和,权重参数为 `weights` ,就可以这样实现

```cpp
// [[Rcpp::export]]
double fibonacci_sum(size_t n, NumericVector weights) {
    IntegerVector fib_seq = fibonacci_seq(n);
    double sum = 0.0;
    for (size_t i = 0; i < n; i++)
    {
        sum += fib_seq[i] * weights[i];
    }
    return sum;
}
```

C++接口函数可以实现为

```cpp
double fibonacci_sum(size_t n, NumericVector weights);
RcppExport SEXP _DemoRcpp_fibonacci_sum(SEXP nSEXP, SEXP weightsSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter<size_t>::type n(nSEXP);
    Rcpp::traits::input_parameter<NumericVector>::type weights(weightsSEXP);
    rcpp_result_gen = Rcpp::wrap(fibonacci_sum(n, weights));
    return rcpp_result_gen;
END_RCPP
}
```

我们使用 `Rcpp::traits::input_parameter<NumericVector>::type` 类型的变量做中间变量转换。

## 使用C++标准库中的类型

上述方法在编写库函数的时候,有时会比较麻烦,因为这样会将库函数与R进行绑定,能够使用的方法就限制在了Rcpp提供的方法中。如果我们可以使用C++标准库类型,例如 `vector<double>`,那么会有很多库可以协助我们进行处理。Rcpp也提供了两个转换的函数 `as()`和`wrap()` ,前者用于将Rcpp类型转换为标准库类型,后者将标准库类型转换为Rcpp类型。虽然使用的是专门的转换函数,而不是重载类型转换运算符,但如果参数是标准库类型,Rcpp依然会隐式地调用转换函数,而且也可以直接从 `SEXP` 类型转换。

我们将之前写的 `fibonacci_seq()` 和 `fibonacci_sum()` 函数进行改写,使之使用标准库类型声明函数,而不使用Rcpp类型。

```cpp
// [[Rcpp::export]]
vector<int> fibonacci_seq_std(size_t n) {
    vector<int> fib_seq(n);
    switch (n)
    {
    case 0:
        break;
    case 1:
        fib_seq[0] = 1;
        break;
    case 2:
        fib_seq[0] = 1;
        fib_seq[1] = 1;
        break;
    default:
        fib_seq[0] = 1;
        fib_seq[1] = 1;
        for (size_t i = 2; i < n; i++)
        {
            fib_seq[i] = fib_seq[i-1] + fib_seq[i-2];
        }
        break;
    }
  
    return fib_seq;
}

// [[Rcpp::export]]
double fibonacci_sum_std(size_t n, vector<double> weights) {
    vector<int> fib_seq = fibonacci_seq_std(n);
    double sum = 0.0;
    for (size_t i = 0; i < n; i++)
    {
        sum += fib_seq[i] * weights[i];
    }
    return sum;
}
```

C++接口函数就分别改为了

```cpp
// fibonacci_seq
std::vector<int> fibonacci_seq_std(size_t n);
RcppExport SEXP _DemoRcpp_fibonacci_seq(SEXP nSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter<size_t>::type n(nSEXP);
    rcpp_result_gen = Rcpp::wrap(fibonacci_seq_std(n));
    return rcpp_result_gen;
END_RCPP
}

// fibonacci_sum
double fibonacci_sum_std(size_t n, std::vector<double> weights);
RcppExport SEXP _DemoRcpp_fibonacci_sum(SEXP nSEXP, SEXP weightsSEXP) {
BEGIN_RCPP
    Rcpp::RObject rcpp_result_gen;
    Rcpp::RNGScope rcpp_rngScope_gen;
    Rcpp::traits::input_parameter<size_t>::type n(nSEXP);
    Rcpp::traits::input_parameter<std::vector<double> >::type weights(weightsSEXP);
    rcpp_result_gen = Rcpp::wrap(fibonacci_sum_std(n, weights));
    return rcpp_result_gen;
END_RCPP
}
```

这样我们所写地库函数就可以不再局限于Rcpp包,甚至完全脱离Rcpp依赖也是可能的。仓库 [HPDell/tutorial-Rcpp/02-DemoRcpp2s](https://github.com/HPDell/tutorial-Rcpp/tree/master/02-DemoRcpp2s) 中的 `fibonacci.cpp` 就是完全脱离了Rcpp的实现,并且将相关函数的声明移动到了头文件里,并在 `RcppExports.cpp` 文件中包含了这个头文件,以实现函数的导入。但是有一点需要注意,如果用这种方式,导入的头文件最好放在 `RcppExports.cpp` 的开头,至少要在 `Rcpp.h` 之前导入,否则会引发一些奇怪的问题。这一点并没有得到广泛验证,我也不知道是为什么,也许和Rcpp的实现有关。

# 参考

:octocat: 仓库 [HPDell/tutorial-Rcpp](https://github.com/HPDell/tutorial-Rcpp)

📖 文档 [Rcpp for everyone](https://teuder.github.io/rcpp4everyone_en/)