C++

第一个程序

#include <iostream>    //头文件
using namespace std;   //命名空间
int main(void)        //主函数
{
    cout << "Hellow world" << endl;
    system("pause");
    return 0;
}

1、头文件:可以认为头文件是你在调用函数时的一个桥梁。c++的程序是由一个又一个的函数组成的,比如说刚刚这个程序里就可以说包含了两个函数(其中一个函数包含在另一个函数里头),一个是主函数(main函数),还有一个是输出函数cout。在c++中,除了主函数及自定义的函数,其他的函数都是包含于某些头文件里的,它们被称为库函数,想要调用这些函数,只要在程序的开头写上包含该函数的头文件就可以了。比如说包含cout函数的头文件是(输入输出流),而我在函数中调用了cout这个函数,因此我需要在开头写上#include(包含输⼊输出流)即可。

2、命名空间:在程序中必须要写上这么一句话即可,是为防止不同人写的函数出现重名,然后将这些函数整合到一起会出现问题的情况。

3、主函数:这是整个程序最为核心,也是最重要的部分。因为它是系统默认的你这个程序运行的入口,换句话说,当你的源代码通过编译,成为一个程序,在计算机上运行时,它是从int main()(其中int 代表这个函数的返回值类型)这里开始的,一句一句往下走。如果没有这个主函数,程序就运行不了。

4、函数体:主函数声明后,下面有一对花括号({ }),花括号里面 所表达的就是你这个函数想要干什么。比如说这一个程序,他的main函数想要做的是输出hello world,因为它调用了一个输出函数 (cout)。有一个非常重要的点就是每一个函数体都必须在一对花括号内,只有这样,系统才能真正明白你想要表达什么(编译才能通过)。

5、返回值:本程序主函数的最后有一句话:return 0,它的意思是 主函数结束后向操作系统返回一个0值,也就是说,如果你的程序顺利地结束了,你的操作系统会得到一个0值,如果运行出错,那么得到的就也许是另外一个值了。

注释

方便自己和他人阅读,不会被程序执行。

//单行
/*多行注释*/

变量

作用:给一段指定的内存空间起名,方便操作这段内存。 语法:数据类型 变量名 = 初始值;

int a = 10;
cout << "a = "<< a << endl;

常量

define宏定义

#define 常量名 常量值

通常在文件上方定义,表示一个常量。

const修饰的变量

const 数据类型 常量名 = 常量值

通常在变量定义之前加关键字const,修饰该变量为常量,不可修改。

示例

#define day 7//是不可修改的值,一旦修改就会报错

const int month = 30;

数据类型

C++规定在创建一个变量或者常量的时候,必须要指定出相应的数据类型,否则无法给该变量分配内存空间。

数据类型:整型、sizeof关键字、实型(浮点型)、字符型、转义字符、字符串、布尔类型

Rcpp介绍

为了提高R程序的运行效率,可以尽量使用向量化编程,减少循环,尽量使用内建函数。对于效率的瓶颈,尤其是设计迭代算法时,可以采用编译代码,而Rcpp扩展包可以很容易地将C++代码连接到R程序中,并且支持在C++中使用类似于R的数据类型。

Rcpp可以很容易地把C++代码与R程序连接在一起,可以从R中直接调用C++代码而不需要用户关心那些繁琐的编译、链接、接口问题。可以在R数据类型和C++数据类型之间容易地转换。

安装

在用Rcpp开发之前,要先安装c++编译器

安装c++编译器

Windows用户需要安装Rtools包;Mac系统的用户可以从应用商店安装Xcode软件,Linux操作系统可以在操作系统命令行用如下命令安装编译软件:

sudo apt-get install r-base-dev

安装Rcpp

通过执行以下代码

install.packages("Rcpp")

Rcpp入门样例

用cppFunction()转换简单的C++函数—Fibnacci例子

考虑用C++程序计算Fibonacci数的问题。可以使用如下R代码,其中有一部分C++代码,用cppFunction转换成了R可以调用的同名R函数。

library(Rcpp)
cppFunction(code='
  int fibonacci(const int x){
    if(x < 2) return x;
    if(x > 40) return -1; 
    // 太大的x值会占用过多计算资源
    return ( fibonacci(x-1) + fibonacci(x-2) );
  }
')
print(fibonacci(5))
## [1] 5

编译、链接、导入是在后台由Rcpp控制自动进行的,不需要用户去设置编译环境,也不需要用户执行编译、链接、导入R的工作。在没有修改C++程序时,同一R会话期间重新运行不必重新编译。

Rcpp属性

Rcpp属性(attributes)用来简化把C++函数变成R函数的过程。做法是在C++源程序中加入一些特殊注释,利用其指示自动生成C++与R的接口程序。属性是C++11标准的内容,现在的编译器支持还不多,所以在Rcpp支持的C++程序中写成了特殊格式的注释。

Rcpp属性有如下优点: 降低了同时使用R与C++的学习难度; 取消了很多繁复的接口代码; 可以在R会话中很简单地调用C++代码,不需要用户自己考虑编译、连接、接口问题; 可以先交互地调用C++,成熟后改编为R扩展包而不需要修改界面代码。

Rcpp属性的主要组成部分如下: 在C++中,提供Rcpp::export标注要输出到R中的C++函数。 在R中,提供sourceCpp(),用来自动编译连接保存在文件或R字符串中的C++代码,并自动生成界面程序把C++函数转换为R函数。

在R中编译链接C++代码

sourceCpp()函数可以用code=指定一个R字符串,字符串的内容是C++源程序, 其中还是用特殊注释//[[Rcpp::export]]标识要导出的C++函数。 如

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
  NumericVector iters(NumericVector x){
    int n = x.size();
    NumericVector y(n);

    y[0] = x[0];
    double sign=-1;
    for(int t=1; t<n; t++){
      sign *= -1;
      y[t] = sign*y[t-1] + x[t];
    }

    return y;
  }
')
print(iters(1:5))
## [1] 1 3 0 4 1

这个例子说明C++程序可以直接写在R程序文件内(保存为R多行字符串),用sourceCpp()函数编译。Rcpp包为C++提供了一个NumericVector数据类型,用来存储数值型向量。用成员函数size()访问其大小,用方括号下标访问其元素。C程序中定义的函数可以返回NumericVector数据类型,将自动转换为R的数值型向量。特殊的注释//[[Rcpp::export]]用来指定哪些C++函数是要转换为R函数的。这叫做Rcpp属性(attributes)功能。

在C++源程序中指定要导出的C++函数

直接把C++代码写在R源程序内部的好处是不用管理多个源文件,缺点是当C++代码较长时,不能利用专用C++编辑环境和调试环境,出错时显示的错误行号不好定位,而且把代码保存在R字符串内,C++代码中用到字符时需要特殊语法。所以,稍复杂的C++代码应该放在单独的C++源文件内。

假设上面的iters函数的C++代码单独存入了一个iters.cpp源文件中,内容如下:

#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector iters(NumericVector x){
  int n = x.size();
  NumericVector y(n);
  
  y[0] = x[0];
  double sign=-1;
  for(int t=1; t<n; t++){
    sign *= -1;
    y[t] = sign*y[t-1] + x[t];
  }
  
  return y;
}

用如下的sourceCpp()函数把C++源文件中代码编译并转换为R可访问的同名函数, 测试调用:

sourceCpp(file='iters.cpp')
print(iters(1:5))

Rcpp提供的C++数据类型

RObject类

Rcpp包为C++定义了NumericVector,IntegerVector,CharacterVector, Matrix等新数据类型,可以直接与R的numeric,charactor,matrix对应。 Rcpp最基础的R数据类型是RObject,这是NumericVector,IntegerVector等的基类,通常不直接使用。RObject包裹了原来R的C API的SEXP数据 结构,并且提供了自动的内存管理,不再需要用户自己处理建立内存和消除内存的工作。RObject存储数据完全利用R C API的SEXP数据 结构,不进行额外的复制。

RObject有如下导出类: IntegerVector: 整数向量; NumercicVector: 数值向量; LogicalVector: 逻辑向量; CharacterVector: 字符型向量; GenericVector: 列表; ExpressionVector: 表达式向量; RawVector: 元素为raw类型的向量。 IntergerMatrix, NumericMatrix: 整数值或数值矩阵。

在R向量中,如果其元素都是同一类型(如整数、双精度数、逻辑、字符型),则称为原子向量。Rcpp提供了IntegerVector,NumericVector,LogicalVector,CharacterVector等数据类型与R的原子向量类型对应。在C++中可以用[]运算符存取向量元素,也可以用STL的迭代器。用.begin(),.end()等界定范围,用循环或者accumulate等STL算法处理整个向量。

IntegerVector类

在R中通常不严格区分整数与浮点实数,但是在与C++交互时,C++对整数与实数严格区分,所以RCpp中整数向量与数值向量是区分的。

在R中,如果定义了一个仅有整数的向量,其类型是整数(integer)的,否则是数值型(numeric)的,如:

x <- 1:5
class(x)
## [1] "integer"
y <- c(0, 0.5, 1)
class(y)
## [1] "numeric"

Rcpp可以把R的整数向量传递到C++的IntegerVector中,也可以把C++的IntegerVector函数结果传递回R中变成一个整数向量。也可以在C++中生成IntegerVector向量,填入整数值。

IntegerVector示例1:返回完全数

如果一个正整数等于它所有的除本身以外的因子的和,称这个数为完全数。如 6=1+2+3 28=1+2+4+7+14 是完全数。

任务:用C++程序输入前4个完全数偶数,返回到R中。这4个数为6, 28, 496, 8182。 程序:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
IntegerVector wholeNumberA(){
    IntegerVector epn(4);
    epn[0] = 6;    epn[1] = 28;
    epn[2] = 496;  epn[3] = 8182;

    return epn;
}
')
print(wholeNumberA())
## [1]    6   28  496 8182

对IntegerVector类型的x,访问单个下标i格式为x[i],下标从0开始计算,与R向量下标从1开始的规则不同。

从例子看出,可以在C++中建立一个IntegerVector,需指定大小,可以逐个填入数值,直接返回IntegerVector到R即可。

IntegerVector示例2:输入整数向量

任务:用C++编写函数,从R中输入整数向量,计算其元素乘积(与R的prod()函数功能类似)。程序如下:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
IntegerVector prod1(IntegerVector x){
    int prod = 1;
    for(int i=0; i < x.size(); i++){
        prod *= x[i];
    }
    return wrap(prod);
}
')
print(prod1(1:5))
## [1] 120

为了获得向量的元素个数, 用x.size()这种方法。 从程序看出, 用IntegerVector从R中接受一个整数值向量时, 不需要显式地转换。 把一个C++整数值返回给R时, 可以用IntegerVector返回,因为返回值原来是一个C++的int类型, 所以需要用Rcpp::wrap()转换一下。在sourceCpp中可以省略Rcpp::部分。

NumericVector类

NumericVector类在C++中保存双精度型一维数组,可以与R的实数型向量(class为numeric)相互转换。这是用C++程序与R交互时最常用到的数据类型。x.size()返回元素个数。

示例1:计算元素次方的和

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
double ssp(
    NumericVector vec, double p){
  double sum = 0.0;
  for(int i=0; i < vec.size(); i++){
    sum += pow(vec[i], p);
  }
  return sum;
}
')
ssp(1:4, 2)
## [1] 30

从程序看出,用Rcpp属性编译时,C++函数的输入与返回值类型转换有如下规则:R中数值型向量在C++中可以用NumericVector接收; R中单个的实数在C++中可以用double来接收;为了返回单个的实数, 可以直接返回double,也可以返回NumericVector类型, 或者从一个double型用wrap()转换。C++中如果返回NumericVector, 在R中转换为数值型向量。

示例2:clone函数

在自定义R函数时,输入的自变量的值不会被改变,相当于自变量都是局部变量。如果在自定义函数中修改了自变量的值,实际上只能修改自变量的一个副本的值。如

x <- 100
f <- function(x){
  print(x); x <- 99; print(x)
}
c(f(x), x)
## [1] 100
## [1] 99
## [1]  99 100

但是,在用Rcpp编写R函数时,因为RObject传递的是指针,并不自动复制自变量值,所以修改自变量值会真的修改原始的自变量变量值。如:

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f2(NumericVector x){
  x[0] = 99.0;
  return(x);
}
')
x <- 100
c(f2(x), x)
## [1] 99 99

可见自变量的值被修改了。当然,对这个问题而言,因为输入的是一个标量,只要函数自变量不是NumericVector类型而是用double类型, 则自变量值会被复制,达到值传递的效果,自变量值也就不会被真的修改。如

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f3(double x){
  x = 99.0;
  return(wrap(x));
}
')
x <- 100
c(f3(x), x)
## [1]  99 100

示例3:向量子集

C++中Rcpp支持的向量和列表,仍可以用正整数向量作为下标。 例如, 下面的C++函数求指定的元素子集的和:

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
double sumsub(
  NumericVector x, IntegerVector i){
  NumericVector y = x[i-1];
  double s = 0.0;
  int n = y.size();
  for(int j=0; j<n; j++)
    s += y[j];
  
  return s;
}
')
x <- 1:10
sumsub(x, c(1, 3, 5))
## [1] 9

注意在程序中将输入的下标向量减了1,以适应从R规则到C++规则的变化。 Rcpp在输入和输出时会自动在整型和双精度型之间转换, 比如上面例子中输入的x是整型的,要求输入NumericVector是双精度型的, Rcpp会自动转换。 Rcpp也支持逻辑下标, 比如,下面在C++定义的函数可以取出向量的正值的子集:

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector get_positive(NumericVector x){
  NumericVector y = x[x > 0];

  return y;
}
')
x <- c(1, -1, 2, -2)
get_positive(x)
## [1] 1 2

Rcpp也支持按元素名访问向量元素或向量子集。

NumericMatrix类

NumericMatrix x(n,m)产生一个未初始化的矩阵,元素为double类型。 x.nrow()返回行数,x.ncol()返回列数,x.size()返回元素个数。 下标从0开始计算。为了访问Xij,用x(i,j)的格式:注意,不是x[i,j]也不是x[i][j]。 为了访问x的第i行,用x.row(i)或x(i,); 为了访问x的第j列,用x.column(j)或x(,j)。 transpose(x)返回x的转置。 NumericMatrix::zeros(n)返回n阶元素为0的方阵。 NumericMatrix::eye(n)返回n阶单位阵。

示例1:计算矩阵各列模的最大值

输入一个矩阵,计算各列的向量模,并返回模的最大值。

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
double testfun(NumericMatrix x){
  int nr = x.nrow();
  int nc = x.ncol();
  double y, y1;

  y = 0.0;
  for(int j=0; j<nc; j++){
    y1 = 0.0;
    for(int i=0; i<nr; i++){
      y1 += pow(x(i,j), 2);
    }
    if(y1 > y) y = y1;
  }
  y = sqrt(y);

  return y;
}
')
x <- matrix(c(1, 2, 4, 9), 2, 2)
print(x)
##      [,1] [,2]
## [1,]    1    4
## [2,]    2    9
print(testfun(x))
## [1] 9.848858

示例2:访问列子集

可以用x(,j)这样的格式将矩阵x的第j列取出为普通向量,并可以赋值修改。也可以用x.column(j),用法相同。x(i,)取出第i行为一个普通向量。 比如,下面的C++函数将输入的矩阵的每一行计算累加和:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericMatrix testfun(NumericMatrix x){
  NumericMatrix y = clone(x);
  for(int j=1; j<x.ncol(); j++){
    y(_,j) = y(_,j-1) + x(_,j);
  }

  return y;
}
')
x <- matrix(c(1, 2, 4, 9), 2, 2)
print(x)
##      [,1] [,2]
## [1,]    1    4
## [2,]    2    9
print(testfun(x))
##      [,1] [,2]
## [1,]    1    5
## [2,]    2   11

CharacterVector类

CharacterVector类型可以与R的字符型向量相互交换信息,在C++中其元素为字符串。 字符型缺失值在C++中为R_NAString。R的字符型向量也可以转换为std::vector。 如:

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
CharacterVector f5(){
  CharacterVector x(3);
  x[0] = "This is a string";
  x[1] = "Test";
  x[2] = R_NaString; 
  return(x);
}
')
f5()
## [1] "This is a string" "Test"             NA

Named类

R中的向量、矩阵、数据框可以有元素名、列名、行名。 这些名字可以借助Named类添加。

例如:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f6(){
  NumericVector x = NumericVector::create(
    Named("math") = 82,
    Named("chinese") = 95,
    Named("English") = 60);
  return(x);
}
')
f6()
##    math chinese English 
##      82      95      60

“Named(元素名)”可以简写成“_元素名”。 如:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f6b(){
  NumericVector x = NumericVector::create(
    _["math"] = 82,
    _["chinese"] = 95,
    _["English"] = 60);
  return(x);
}
')

List类型

Rcpp提供的List类型对应于R的list(列表)类型, 在C++中也可以写成GenericVector类型。 其元素可以不是同一类型, 在C++中可以用方括号和字符串下标的格式访问其元素。

例如,下面的函数输入一个列表, 列表元素vec是数值型向量, 列表元素multiplier是数值型标量, 返回一个列表, 列表元素sum为vec元素和, 列表元素dsum为vec元素和乘以multiplier的结果:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
List f7(List x){
  NumericVector vec = as<NumericVector>(x["vec"]);
  double multiplier = as<double>(x["multiplier"]);
  double y = 0.0, y2;
  for(int i=0; i<vec.length(); i++){
    y += vec[i];
  }
  y2 = y*multiplier;
  return(List::create(Named("sum")=y,
    Named("dsum")=y2));
}
')
f7(list(vec=1:5, multiplier=10))
## $sum
## [1] 15
## 
## $dsum
## [1] 150

上面的程序用了Rcpp::List::create()当场生成List类型, 因为用Rcpp属性功能编译所以可以略写Rcpp::。

Rcpp的DataFrame类

Rcpp的DataFrame类用来与R的data.frame交换信息。

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
DataFrame f8(){
  IntegerVector vec = 
    IntegerVector::create(7,8,9);
  std::vector<std::string> s(3);
  s[0] = "abc"; s[1] = "ABC"; s[2] = "123";
  return(DataFrame::create(
    Named("x") = vec,
    Named("s") = s));
}
')
f8()
##   x   s
## 1 7 abc
## 2 8 ABC
## 3 9 123

Rcpp的Function类

Rcpp的Function类用来接收一个R函数, 并且可以在C++中调用这样的函数。 示例如:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
SEXP dsort(Function sortFun, SEXP x){
  return sortFun(x, Named("decreasing", true));
}
')
dsort(sort, c('bb', 'ab', 'ca'))
## [1] "ca" "bb" "ab"

程序用Function对象sortFun接收从R中传递过来的排序函数, 实际调用时传递过来的是R的sort函数。

在C++中调用R函数时, 有名的自变量用“Named(自变量字符串, 自变量值)”的格式给出。

程序中的待排序的向量与排序后的向量都用了SEXP来说明, 即直接传送原始R API指针, 这样可以不管原来类型是什么, 在C++中完全不进行类型转换或复制。

从运行例子看出数值和字符串都正确地按照降序排序后输出了。

R函数可以不作为C++的函数自变量传递进来, 而是直接调用R的函数。 R的许多函数都已经在Rcpp名字空间中输出了, 所以可以直接调用R的各个向量化的函数, 包括随机数函数。 例如, 下面的C++函数返回一个各列为随机数的数值型矩阵:

library(Rcpp)
sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
NumericMatrix rngCpp(const int N) {
  NumericMatrix X(N, 4);
  X(_, 0) = runif(N);
  X(_, 1) = rnorm(N);
  X(_, 2) = rt(N, 5);
  X(_, 3) = rbeta(N, 1, 1);
  return X;
}')

Rcpp的Environment类

R的环境是分层的,可以逐层查找变量名对应的内容。 Rcpp的Environment类用来与R环境对应。 可以利用Environment来定位R的扩展包中的函数或数据, 例如下面的程序片段在C++中定位了stats扩展包中的 rnorm函数并进行了调用:

Environment stats("package:stats");
Function rnorm = stats["rnorm"];
return rnorm(3, Named("sd", 100.0));

RCPP数据类型和C++数据类型的转换

通过as()和wrap()函数转换

  • as<CPP>(RCPP):将RCPP数据类型(RCPP)转换成C++数据类型(CPP)

  • wrap(CPP):将C++数据类型(CPP)转换成RCPP数据类型(RCPP)

RCPP C++ as wrap
Vector vector,list,deque + +
List,DataFrame vector<vector>,list<vector>,etc + +
NamedVector map,unodered_map - +
Vector set,unodered_set - +
  • 接下来这个例子展示了RCPP::Vector与C++序列容器(如:std::vector,std::list,std::deque,etc)的互相转换
NumericVector   rcpp_vector = {1,2,3,4,5};

// Conversion from Rcpp::Vector to std::vector  
std::vector<double>  cpp_vector = as< std::vector<double> >(rcpp_vector);

// Conversion from std::vector to Rcpp::Vector  
NumericVector v1 = wrap(cpp_vector);
  • 接下来这个例子展示了C++中的二维容器与RCPP中DataFrameList类型的互相转换

using namespace std;
// [[Rcpp::export]]
// A two-dimensional vector with all element vectors of equal length
// can be converted to a DataFrame

vector<vector<double>> cpp_vector_2d_01 = {{1,2},{3,4}};
DataFrame df = wrap(cpp_vector_2d_01);

// A two-dimensional vector with different length of element vectors
// can be converted to a list

vector<vector<double>> cpp_vector_2d_02 = {{1,2},{3,4,5}};
List li = wrap(cpp_vector_2d_02);
  • 接下来的例子展示的是将C++中的std::map<key,value>std::unordered_map<key,value>数据类型转换成命名的Rcpp::Vector数据类型(key是元素的名字,value是元素的值)
library(Rcpp)

sourceCpp(code='
#include<Rcpp.h>
#include<map>
#include<unordered_map>
using namespace Rcpp;

// [[Rcpp::export]]
List std_map(){
  std::map<std::string, double> map_str_dbl;
  
  map_str_dbl["E"] = 5;    
  map_str_dbl["A"] = 1;
  map_str_dbl["C"] = 3;    
  map_str_dbl["D"] = 4;
  map_str_dbl["B"] = 2;
  
  std::unordered_map<std::string, double> umap_str_dbl;

  umap_str_dbl["E"] = 5;    
  umap_str_dbl["A"] = 1;
  umap_str_dbl["C"] = 3;    
  umap_str_dbl["D"] = 4;
  umap_str_dbl["B"] = 2;
  
  List li = List::create(
              Named("std::map",map_str_dbl),
              Named("std::unordered_map", umap_str_dbl)
                        );
  
  return(li);
}
')
std_map()
## $`std::map`
## A B C D E 
## 1 2 3 4 5 
## 
## $`std::unordered_map`
## B D C A E 
## 2 4 3 1 5

as()与wrap()的隐含调用

当C++中赋值运算的右侧表达式是一个R对象或R对象的部分内容时,可以隐含地调用as()将其转换成左侧的C++类型。

当C++中赋值运算的左侧表达式是一个R对象或其部分内容时, 可以隐含地调用wrap()将右侧的C++类型转换成R类型。

详见Rcpp属性。 # Rcpp糖

在C++中,向量和矩阵的运算通常需要逐个元素进行,或者调用相应的函数。

Rcpp通过C++的表达式模板(expression template)功能,可以在C++中写出像R中对向量和矩阵运算那样的表达式。

这称为Rcpp糖(sugar)。

R中的很多函数如sin等是向量化的,Rcpp糖也提供了这样的功能。

Rcpp糖提供了一些向量化的函数如ifelse, sapply等。比如,两个向量相加可以直接写成x + y而不是用循环或迭代器(iterator)逐元素计算;

若x是一个NumericVector,用sin(x)可以返回由x每个元素的正弦值组成的NumericVector

简单示例

比如,函数

\[f(x,y)=\begin{cases} x^2 & x<y \\ -y^2 & x>=y \end{cases}\] 如下的程序可以在C++中定义一个向量化的版本:

library(Rcpp)

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f11(NumericVector x, NumericVector y){
  return ifelse(x < y, x*x, -(y*y));
}
')
f11(c(1, 3), c(4,2))
## [1]  1 -4

上面简单例子中,x < y是向量化的,x * x, y * y, -(y * y)都是向量化的。

ifelse也是向量化的。

向量化的运算符

向量化的四则运算

  • Rcpp糖向量化了+, -, *, /。 设x, y都是NumericVector, 则x + y,x - y,x * y,x * y将返回对应元素进行四则运算后的NumericVector变量。

  • 向量与标量运算,如x + 2.0, 2.0 - x, x * 2.0, 2.0 / x将返回标量与向量每个元素进行四则运算后的NumericVector变量。

NumericVector res = x * y + y / 2.0;
NumericVector res = x * (y - 2.0);
NumericVector res = x / (y * y);

向量化的二元逻辑运算

Rcpp糖扩展了两个元素的比较到两个等长向量的比较,以及一个向量与一个同类型标量的比较,结果是一个同长度的LogicalVector,比较运算符包括<, >, <=, >=, ==, !=

向量化的一元逻辑运算

  • 对数值型向量或返回数值型向量的表达式前面加负号,可以返回每个元素取相反数的结果。

  • 对逻辑型向量或返回逻辑型向量的表达式前面加叹号,可以返回每个元素取反的结果。

用Rcpp访问数学函数

在C和C++代码中可以调用R中的函数,但是格式比较复杂,而且原来不支持向量化。 Rcpp糖则使得从C++中调用R函数变得和在R调用函数格式类似。

R源程序中提供了许多数学和统计相关的函数, 这些函数可以编译成一个独立的函数库, 供其它程序链接使用,函数内容在R的Rmath.h头文件中有详细列表。

Rcpp糖把R中的数学函数在C++中向量化了,输入一个向量, 结果是对应元素为函数值的向量。自变量类型可以是数值型或整数型。 这些数学函数包括abs, exp, floor, ceil, pow等。 在Rcpp中支持的C++源程序中调用这些函数, 最简单的一种方法是直接在Rcpp 名字空间中使用和R中相同的函数名和调用方法, 类似sqrt这样的函数允许输入一个向量, 对向量的每个元素计算。

比如,下面的程序输入一个向量,计算相应的ceil函数值:

library(Rcpp)

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f10(NumericVector x){
  NumericVector y(x.size());
  y = ceil(x);
  return y;
}
')
f10(c(-2.3, 1.9, 3.2))
## [1] -2  2  4

R中的rxxxx类的函数可以产生各种分布的随机数向量,随机数向量与当前种子有关。 Rcpp属性会自动地维护随机数发生器的状态使其与R同步。 如:

library(Rcpp)

sourceCpp(code='
#include <Rcpp.h>
using namespace Rcpp;

//[[Rcpp::export]]
NumericVector f15(){
  NumericVector x = rnorm(10, 0.0, 1.0);
  return x;
}
')
round(f15(), 2)
##  [1]  0.23 -0.88  1.44  1.49 -0.12  0.92  0.95  0.34 -0.73  0.01

RcppArmadillo

Armadillo C++ Library是一种C++的线性代数库(矩阵数学)以取得良好的平衡速度与易用性。有整数,浮点,和复杂的数字支持,以及获取子集,进行三角和统计的功能。

RcppArmadillo包利用rcpp将R与C++结合了起来。

首先,安装RcppArmadillo包

install.packages("RcppArmadillo")

常用数据类型

Data Type Mathematics
mat Matrix矩阵
vec/colvec Column Vector列向量
rowvec Row Vector行向量
cube Cube 3维矩阵
field

基本创建方式

mat X(n_rows,n_cols);
mat X(n_rows,n_cols,fill_type);

vec X(n_lem);
vec X(n_lem,fill_type);

其中fill_type是可选的,可以是以下选择

fill_type 描述
fill::zeros 设置所有元素为0
fill::ones 设置所有元素为1
fill::eye 将矩阵对角线元素设置为1,其他元素为0
fill::randu 随机生成服从0-1平均分布的元素
fill::randn 随机生成服从[0,1]正态分布的元素

也可以通过函数创建,如:

vec v = randu<vec>(5);
mat x = randu<mat>(5,5);

元素的初始化:

vec v = {1,2,3};
mat A = {{1,3,5},
         {2,4,6}}

元素访问:

元素访问 描述
(n) 对于vec和rowvec,访问第n个元素。对于mat和field,首先把矩阵的下一列接到上一列之下,从而构成一个长列向量,并访问第n个元素。
(i,j) 对于mat和二维field,访问第(i,j)个元素。

子集函数访问:

函数 描述
X.diag(k) 访问矩阵X的第k个对角线(k是可选的,主对角线为k=0,上对角线为k>0,下对角线为k<0)
X.row(i) 访问矩阵X的第i行
X.col(i) 访问矩阵X的第i列
X.rows(a,b) 访问矩阵X从第a行到第b行的子矩阵
X.cols(c,d) 访问举证X从第c列到第d列的子矩阵

其他详细内容可参考Armadillo操作手册

简单线性回归例子

下面是一个用RcppArmadillo编写的线性回归程序:

#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]

// [[Rcpp::export]]
Rcpp::List fastLm(const arma::mat& X, 
                  const arma::colvec& y) {
    int n = X.n_rows, k = X.n_cols;
        
    arma::colvec coef = arma::solve(X, y);    // fit model y ~ X
    arma::colvec res  = y - X*coef;           // residuals

    // std.errors of coefficients
    double s2 = std::inner_product(res.begin(), res.end(), res.begin(), 0.0)/(n - k);
                                                        
    arma::colvec std_err = arma::sqrt(s2 * arma::diagvec(arma::pinv(arma::trans(X)*X)));  

    return Rcpp::List::create(Rcpp::Named("coefficients") = coef,
                              Rcpp::Named("stderr")       = std_err,
                              Rcpp::Named("df.residual")  = n - k);
}