sourceCpp 函數

前面我們介紹過以 cppFunction 函數將 C++ 程式碼內崁在 R 程式碼中,這樣的方式對於簡短的小程式而言非常方便,但是如果需要開發比較複雜的 C++ 程式時,把大量的 C++ 程式碼貼在 cppFunction 函數中,會讓程式碼難以閱讀,在開發上也會很不方便。

遇到比較複雜的程式架構時,我們可以將 C++ 程式碼獨立出來,另外儲存成一個 .cpp 檔案,然後使用 sourceCpp 函數來編譯並載入 .cpp 檔案中的 C++ 程式碼,這樣的作用跟 cppFunction 函數相同,但是可以讓大型的程式更好管理。

當我們將 C++ 的程式碼獨立出來時,必須引入 Rcpp.h 並且設定 Rcpp 這個 namespace,也就是在檔案的開頭加上這兩行:

#include <Rcpp.h>
using namespace Rcpp;

對於那些需要在 R 中被呼叫的 C++ 函數之前,加上這一行註解:

// [[Rcpp::export]]

請注意這一行註解中包含的空白不可以省略。

我們將前面計算總和的 C++ 範例程式改成獨立的一個 cpp_sum.cpp 檔,內容如下:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
double cpp_sum(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total;
}

接著以 sourceCpp 編譯並載入使用:

sourceCpp("cpp_sum.cpp")
cpp_sum(1:10)
[1] 55

我們也可以將 R 的程式碼內崁在 C++ 的註解之中,其語法為:

/*** R
# 這是 R 的程式碼
*/

放在這裡的 R 程式碼會以 source(echo = TRUE) 的方式來執行,所以我們不需要特別以 print 等函數來輸出,這個功能對於開發階段的測試與除錯會有一些幫助。以下是計算總和的範例:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
double cpp_sum(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total;
}

/*** R
library(microbenchmark)
x <- runif(1e3)
microbenchmark(
  sum(x),
  cpp_sum(x)
)
*/

將這段 C++ 程式碼儲存在 cpp_sum_embedded_r.cpp 檔案中,然後同樣以 sourceCpp 編譯並載入:

sourceCpp("cpp_sum_embedded_r.cpp")
> library(microbenchmark)
> x <- runif(1e3)
> microbenchmark(
+   sum(x),
+   cpp_sum(x)
+ )
Unit: microseconds
       expr   min    lq    mean median     uq    max neval
     sum(x) 1.136 1.161 1.53268 1.2245 1.2485 29.916   100
 cpp_sum(x) 2.689 2.738 3.12136 2.7840 2.8880 23.875   100

當 C++ 的函數被載入之後,內崁的 R 程式碼也會一併被執行。

範例程式碼

這裡對於一些常見的程式結構提供了 R 與 C++ 的對照範例程式碼,對於 C++ 不熟新的使用者可以參考使用。

if 判斷式

這是使用 if 判斷式計算絕對值的 R 函數:

r_abs <- function(x) {
  if (x > 0) {
    x
  } else {
    -x
  }
}

C++ 版本則為:

double cpp_abs(double x) {
  if (x > 0) {
    return x;
  } else {
    return -x;
  }
}

switch 判斷式

這是一段 R 語言的 switch 判斷式範例程式碼:

r_int2str <- function(x) {
  switch(as.character(x),
    "1" = "one",
    "2" = "two",
    "oops"
  )
}

C++ 版本則為:

std::string cpp_int2str(int x) {
  switch(x) {
    case 1:
      return "one";
    case 2:
      return "two";
    default:
      return "oops";
  }
}

for 迴圈

這是使用 for 迴圈計算平均值的 R 函數:

r_mean <- function(x) {
  n = length(x)
  total = 0
  for(i in 1:n) {
    total <- total + x[i]
  }
  total / n
}

C++ 版本則為:

double cpp_mean(NumericVector x) {
  int n = x.size();
  double total = 0;
  for(int i = 0; i < n; ++i) {
    total += x[i];
  }
  return total / n;
}

while 迴圈

這是使用 while 迴圈計算階乘的 R 函數:

r_factorial <- function(x) {
  result = x
  while (x > 2) {
    x <- x - 1
    result <- result * x
  }
  result
}

C++ 版本則為:

int cpp_factorial(int x) {
  int result = x;
  while (x > 2) {
    x -= 1;
    result *= x;
  }
  return result;
}

nextbreak

這是使用 nextbreak 列出奇數的 R 函數:

r_odd <- function(x) {
  i <- 0
  result = integer()
  while ( TRUE ) {
    i <- i + 1
    if (i > x) break
    if (i %% 2 == 0) next
    result = c(result, i)
  }
  result
}

C++ 版本則為:

IntegerVector cpp_odd(int x) {
  int i = 0;
  IntegerVector result;
  while ( true ) {
    i += 1;
    if (i > x) break;
    if (i % 2 == 0) continue;
    result.push_back(i);
  }
  return result;
}

IntegerVectorpush_backstd::vectorpush_back 用法相同,可將新元素從後方加入向量中。