The reference for this vignette is Wickham 2024.
This vignette and the next use the following R packages:
library(cpp4r)
library(tinydev)
You can create a new package in RStudio (or VSCode) by running:
pkg_template("~/mypkg")
This command will create a new folder containing the mypkg package.
The pkg_template() function automatically creates ./mypkg/R/mypkg-package.R with the following content:
#' @title Package Title
#' @description Some description. CRAN asks for at least one paragraph with two full sentences. Like this.
#' @useDynLib mypkg, .registration = TRUE
"_PACKAGE"
It also creates ./mypkg/src/main.cpp. The rest of src/ aims to provide an organization to keep the code
organized into different files according to their purpose.
Give a look at ./mypkg/src/01_plus_one.h, which contains the following C++ code:
/* roxygen
@title Plus 1 (C++)
@param x integer
@description It adds 1 to an integer value.
@export
@examples plus_one(1)
*/
[[cpp4r::register]]
int plus_one(int x) {
return x + 1;
}
This code is called by main.cpp and it makes sense to keep this organization when the codebase grows,
otherwise you can end up with a single large cpp file containing thousands of lines of code that will be harder to
maintain. With R packages, there is no need to create a main() function in C++, which is the entry point of a
standalone C++ program.
Unlike cpp11, cpp4r can document the C++ functions using Roxygen comments. The comments must be placed before the
[[cpp4r::register]] attribute.
Decorating a function with [[cpp4r::register]] only makes it callable via .Call() from R; it does not export or
document it. For example, this alone leaves sum undocumented and unexported:
[[cpp4r::register]] int sum(int a, int b) { return a + b; }
There are two ways to export a registered function:
/* roxygen ... */ comment block, including an @export tag, directly before the decorated function in the
C++ file, as shown above for plus_one(). register() copies it verbatim onto the generated R wrapper, so
running tinydev::pkg_document() afterwards will export the wrapper and document it as usual.C++ function a name you don’t want exposed directly (e.g. sum_), then write your own R wrapper that
calls it and add a normal #' Roxygen comment block with @export above that wrapper instead.Either way, register() and [[cpp4r::register]] alone never modify NAMESPACE; running tinydev::pkg_document(".")
(or hand-editing NAMESPACE) is what actually exports the function.
With cpp11, you would need to write a separate wrapper, like this:
[[cpp11::register]] int plus_one_(int x) {
return x + 1;
}
#' @title Plus 1 (C++)
#' @param x integer
#' @description It adds 1 to an integer value.
#' @export
#' @examples plus_one(1)
plus_one <- function(x) {
plus_one_(x)
}
With cpp4r you can type:
/* roxygen
@title Plus 1 (C++)
@param x integer
@description It adds 1 to an integer value.
@export
@examples plus_one(1)
*/
[[cpp4r::register]] int plus_one(int x) {
return x + 1;
}
The R version of the previous function is as follows:
#' Plus one (R)
#' @param x integer
#' @description It adds 1 to an integer value.
#' @export
#' @examples plus_one_r(1)
plus_one_r <- function(x) {
x + 1
}
To use the C++ function, you can register and document it by running:
cpp4r::register()
tinydev::pkg_document(".")
tinydev::pkg_load(".")
Which should print:
> plus_one(1)
[1] 2
Try plus_two(1) and plus_two(1.0). Why does 1 and 1.0 matter in C++?
A more complex function is one that returns the sign of a number:
[[cpp4r::register]] int sign_cpp(double x) {
if (x > 0) {
return 1;
} else if (x == 0) {
return 0;
} else {
return -1;
}
}
Add this function to a new file src/03_sign.h.
Here is the R version of the function:
sign_r <- function(x) {
if (x > 0) {
1
} else if (x == 0) {
0
} else {
-1
}
}
Do not forget to include the new header file in main.cpp.
Document both functions and compare their outputs.
To document and install the package as an R library, you can run the following functions:
cpp4r::register()
tinydev::pkg_document(".")
tinydev::pkg_install(".")
Afterward, you can access the functions by loading the package with library(mypkg).
Each time you need to make changes to the C++ code, you can run load_all() again to test and then reinstall the package.
It is good practice to include a license for your code. For example, you can use the Apache license by running:
tinydev::license_apache(".")
You also need to ignore files that are unnecessary for package installation. For example, to ignore the docs folder, add
^docs$ to .Rbuildignore. Your .Rbuildignore file can include the following lines, which you can edit manually to exclude
specific files and directories:
^\.vscode$
^LICENSE\.txt$
`^docs$`
Wickham H, Bryan J, Barrett M, Teucher A (2024). “Automate Package and Project Setup.” https://usethis.r-lib.org/.