The RcppBankAccount
R package provides an example of using Rcpp Modules
to expose C++ classes and their methods to R. The example hails from
working with a BankAccount
class that is a staple of tutorials on C++ classes.
To install the package, you must first have a compiler on your system that is compatible with R. For help on obtaining a compiler consult either macOS or Windows guides.
With a compiler in hand, one can then install the package from GitHub by:
# install.packages("remotes")
remotes::install_github("coatless-rd-rcpp/rcpp-modules-bank-account")
library("RcppBankAccount")
This guide focuses on providing an implementation along the path suggested
in Rcpp Modules Section 2.2: Exposing C++ classes using Rcpp modules.
In particular, the focus is to expose a pure C++ class inside of R without
modifying the underlying C++ class. Largely, this means that the C++ class
must be "marked up" for export using RCPP_MODULE( ... )
macro in a separate
file.
RcppBankAccount
├── DESCRIPTION # Package metadata
├── NAMESPACE # Function and dependency registration
├── R
│ ├── RcppExports.R
│ ├── BankAccount-exports.R # Exporting Rcpp Module's BankAccount into R
│ └── BankAccount-pkg.R # NAMESPACE Import Code for Rcpp Modules
├── README.md # Implementation Overview
├── RcppBankAccount.Rproj
├── man
│ ├── RcppBankAccount-package.Rd
│ └── BankAccount.Rd
└── src
├── Makevars # Enable C++11
├── RcppExports.cpp
├── BankAccount.cpp # Class Implementation
├── BankAccount.h # Class Definition
└── BankAccount_export.cpp # Exporting the C++ Class with RCPP_MODULE
Inside of src/BankAccount.h, the definition of the C++ class is written with an inclusion guard. The definition is a "bare-bones" overview of what to expect. The meat or the implementation of the class is given in the src/BankAccount.cpp file.
// BankAccount.h
#ifndef BankAccount_H
#define BankAccount_H
class BankAccount
{
public:
// Constructors
BankAccount();
BankAccount(int starting_balance);
// Modifiers
void deposit(int amount);
void withdraw(int amount);
// Accessor
int get_current_balance();
private:
// Member variables
int current_balance;
};
#endif /* BankAccount_H */
In src/BankAccount.cpp, the meat behind the C++ class is implemented. The "meat" emphasizes how different methods within the class should behave. By
// BankAccount.cpp
// Required to use stop()
#include <Rcpp.h>
// Retrieve the definition of our BankAccount class
#include "BankAccount.h"
// Constructors ----
BankAccount::BankAccount() {
current_balance = 250;
}
BankAccount::BankAccount(int starting_balance) {
current_balance = starting_balance;
}
// Modifiers ----
void BankAccount::deposit(int amount) {
current_balance += amount;
}
void BankAccount::withdraw(int amount) {
if (current_balance >= amount) {
current_balance -= amount;
} else {
Rcpp::stop("Current balance is %s so we cannot withdraw %s without going negative.", current_balance, amount);
}
}
// Accessors ----
int BankAccount::get_current_balance() {
return current_balance;
}
With the class definition and implementation in hand, the task switches to exposing
the definition into R by creating src/BankAccount_export.cpp.
Within this file, the Rcpp Module is defined using the RCPP_MODULE( ... )
macro.
Through the macro, the class' information must be specified using different
member functions of Rcpp::class_
. A subset of these member functions is
given next:
- Constructor:
.default_constructor()
- Exposes the default class constructor that does not have any parameters.
.constructor<PARAMTYPE1, PARAMTYPE2>()
- Exposes a constructor with recognizable C++ data types.
- e.g.
double
,int
,std::string
, and so on.
- Methods:
.method("FunctionName", &ClassName::FunctionName)
- Exposes a class method from
ClassName
given byFunctionName
.
- Exposes a class method from
.property("VariableName", &ClassName::GetFunction, &ClassName::SetFunction )
- Indirect access to a Class' fields through getter and setter functions.
- Fields:
.field("VariableName", &ClassName::VariableName, "documentation for VariableName")
- Exposes a public field with read and write access from R
.field_readonly("VariableName", &Foo::VariableName, "documentation for VariableName read only")
- Exposes a public field with read access from R
// BankAccount_export.cpp
// Include Rcpp system header file (e.g. <>)
#include <Rcpp.h>
// Include our definition of the BankAccount file (e.g. "")
#include "BankAccount.h"
// Expose (some of) the Student class
RCPP_MODULE(RcppBankAccountEx){
Rcpp::class_<BankAccount>("BankAccount")
.default_constructor()
.constructor<int>()
.method("deposit", &BankAccount::deposit)
.method("withdraw", &BankAccount::withdraw)
.property("get_current_balance", &BankAccount::get_current_balance);
}
In the R/BankAccount-exports.R file, write the load
statement for the module exposed via the RCPP_MODULE( ... )
macro. In addition,
make sure to export the class name so that when the package is loaded anyone
can access it via new()
.
# Export the "BankAccount" C++ class by explicitly requesting BankAccount be
# exported via roxygen2's export tag.
#' @export BankAccount
loadModule(module = "RcppBankAccountEx", TRUE)
To register the required components for Rcpp Modules, the NAMESPACE
file
must be populated with imports for Rcpp
and the methods
R packages.
In addition, the package's dynamic library must be specified as well.
There are two ways to go about this:
- Let
roxygen2
automatically generate theNAMESPACE
file; or - Manually specify in the
NAMESPACE
file.
The roxygen2
markup required can be found in R/RcppBankAccount-package.R.
#' @useDynLib RcppBankAccount, .registration = TRUE
#' @import methods Rcpp
"_PACKAGE"
Once the above is run during the documentation generation phase, the
NAMESPACE
file will be created with:
# Generated by roxygen2: do not edit by hand
export(BankAccount)
import(Rcpp)
import(methods)
useDynLib(RcppBankAccount, .registration = TRUE)
Make sure to build and reload the package prior to accessing methods.
At this point, everything boils down to constructing an object from the class
using new()
from the methods
package. The new()
function initializes a
C++ object from the specified C++ class and treats it like a
traditional S4 object.
##################
## Constructor
# Use a default constructor
jjb_bank = new(BankAccount)
# Supply values to the constructor
pete_bank = new(BankAccount, starting_balance = 1000)
##################
## Setters
jjb_bank$deposit(10)
jjb_bank$withdraw(20)
# This will generate an error
jjb_bank$withdraw(10000)
# Error in jjb_bank$withdraw(10000) :
# Current balance is 240 so we cannot withdraw 10000 without going negative.
##################
## Getters
jjb_bank$get_current_balance
# [1] 240
GPL (>= 2)