Skip to content

Commit 5c9b9f6

Browse files
committed
conditionally use features from CXX standard > 11 if available - REQUIRES PATCHED TESTHAT FOR CXX23
1 parent fceab1c commit 5c9b9f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+2958
-1115
lines changed

.Rbuildignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@
3232
^Rcppsampling$
3333
^cpp11sampling$
3434
^cpp4rsampling$
35+
^scripts$
36+
^cpp4rbenchmark$
37+
^cpp4rtest-check\.log$
38+
.*\.tar\.gz$

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: cpp4r
22
Title: Header-Only 'C++' and 'R' Interface
3-
Version: 0.3.1
3+
Version: 0.4.0
44
Authors@R: c(
55
person("Mauricio", "Vargas Sepulveda", , "[email protected]", role = c("aut", "cre"),
66
comment = c(ORCID = "0000-0003-1017-7574")),

Makefile

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
clean:
22
@Rscript -e 'devtools::clean_dll("cpp4rtest"); cpp4r::register("cpp4rtest")'
33

4+
install:
5+
@Rscript -e 'devtools::clean_dll("cpp4rtest"); devtools::install()'
6+
7+
docs:
8+
@Rscript -e 'devtools::document(); pkgsite::build_site()'
9+
410
test:
11+
@clear
12+
@echo "==============================="
513
@echo "Testing R code"
614
@Rscript -e 'devtools::document(); devtools::test(); devtools::install()'
7-
@echo "Testing C++ code"
8-
@Rscript -e 'devtools::clean_dll("cpp4rtest"); devtools::load_all("cpp4rtest"); devtools::test("cpp4rtest")'
15+
@echo "==============================="
16+
@/bin/bash -euo pipefail -c './scripts/test_loop.sh'
17+
18+
bench:
19+
@clear
20+
@Rscript -e 'devtools::install()'
21+
@clear
22+
@/bin/bash -euo pipefail -c './scripts/bench_loop.sh'
923

1024
check:
25+
@clear
26+
@echo "==============================="
1127
@echo "Checking R code"
12-
@Rscript -e 'devtools::install(); devtools::check()'
28+
@Rscript -e 'devtools::install(); devtools::check(error_on = "error")'
29+
@clear
30+
@echo "==============================="
1331
@echo "Checking C++ code"
14-
@Rscript -e 'devtools::check("cpp4rtest")'
15-
16-
site:
17-
@Rscript -e 'devtools::document(); pkgdown::build_site()'
18-
19-
install:
20-
@Rscript -e 'devtools::clean_dll("cpp4rtest"); devtools::install()'
32+
@/bin/bash -euo pipefail -c './scripts/check_loop.sh'
33+
@echo "==============================="
2134

2235
clang_format=`which clang-format-18`
2336

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# cpp4r 0.4.0
2+
3+
* Clearer documentation about the C++ workflow (i.e., how to use anticonf to specify a C++ standard)
4+
* Allows for default values like `my_fun(int x = 100)` to call `my_fun()` with the same result as `my_fun(100L)` from R
5+
16
# cpp4r 0.3.1
27

38
* Added support for implicit conversions for R lists

R/register.R

Lines changed: 128 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ register <- function(path = NULL, quiet = !is_interactive(), extension = c(".cpp
8989
cpp_function_registration <- glue::glue_collapse(cpp_function_registration, sep = "\n")
9090

9191
extra_includes <- character()
92-
if (pkg_links_to_rcpp(path)) {
93-
extra_includes <- c(extra_includes, "#include <cpp4r/R.hpp>", "#include <Rcpp.h>", "using namespace Rcpp;")
94-
}
9592

9693
pkg_types <- c(
9794
file.path(path, "src", paste0(package, "_types.h")),
@@ -230,8 +227,60 @@ generate_r_functions <- function(funs, package = "cpp4r", use_package = FALSE) {
230227
}
231228

232229
funs$package_call <- package_call
233-
funs$list_params <- vcapply(funs$args, glue_collapse_data, "{name}")
234-
funs$params <- vcapply(funs$list_params, function(x) if (nzchar(x)) paste0(", ", x) else x)
230+
231+
# Extract default values and create parameter lists
232+
funs$param_info <- lapply(funs$args, function(args_df) {
233+
if (nrow(args_df) == 0) {
234+
return(list(params = "", args = "", checks = ""))
235+
}
236+
237+
# Parse default values from the type column (they appear after '=')
238+
param_names <- args_df$name
239+
param_types <- args_df$type
240+
241+
# Extract defaults (format: "type name = value" becomes "value")
242+
defaults <- vapply(param_types, function(t) {
243+
if (grepl("=", t)) {
244+
sub(".*=\\s*", "", t)
245+
} else {
246+
""
247+
}
248+
}, character(1))
249+
250+
# Clean up types (remove default value parts)
251+
clean_types <- vapply(param_types, function(t) {
252+
trimws(sub("\\s*=.*$", "", t))
253+
}, character(1))
254+
255+
# Generate R function parameters with defaults
256+
params_with_defaults <- vapply(seq_along(param_names), function(i) {
257+
if (nzchar(defaults[i])) {
258+
# Convert C++ defaults to R defaults
259+
r_default <- convert_cpp_default_to_r(defaults[i])
260+
paste0(param_names[i], " = ", r_default)
261+
} else {
262+
param_names[i]
263+
}
264+
}, character(1))
265+
266+
# Generate type checking/coercion code
267+
checks <- vapply(seq_along(param_names), function(i) {
268+
generate_type_check(param_names[i], clean_types[i])
269+
}, character(1))
270+
checks <- checks[nzchar(checks)]
271+
272+
list(
273+
params = paste(params_with_defaults, collapse = ", "),
274+
args = paste(param_names, collapse = ", "),
275+
checks = if (length(checks) > 0) paste0("\t", checks, collapse = "\n") else ""
276+
)
277+
})
278+
279+
funs$list_params <- vapply(funs$param_info, function(x) x$params, character(1))
280+
funs$call_args <- vapply(funs$param_info, function(x) x$args, character(1))
281+
funs$type_checks <- vapply(funs$param_info, function(x) x$checks, character(1))
282+
283+
funs$params <- vcapply(funs$call_args, function(x) if (nzchar(x)) paste0(", ", x) else x)
235284
is_void <- funs$return_type == "void"
236285
funs$calls <- ifelse(is_void,
237286
glue::glue_data(funs, "invisible(.Call({package_names}{params}{package_call}))"),
@@ -256,20 +305,89 @@ generate_r_functions <- function(funs, package = "cpp4r", use_package = FALSE) {
256305
}
257306
}, funs$file, funs$line, SIMPLIFY = TRUE)
258307

259-
# Generate R functions with or without Roxygen comments
260-
out <- mapply(function(name, list_params, calls, roxygen_comment) {
308+
# Generate R functions with type checks and defaults
309+
out <- mapply(function(name, list_params, calls, roxygen_comment, type_checks) {
310+
body <- if (nzchar(type_checks)) {
311+
paste0("\n", type_checks, "\n\t", calls, "\n")
312+
} else {
313+
paste0("\n\t", calls, "\n")
314+
}
315+
261316
if (nzchar(roxygen_comment)) {
262-
glue::glue("{roxygen_comment}\n{name} <- function({list_params}) {{\n\t{calls}\n}}")
317+
glue::glue("{roxygen_comment}\n{name} <- function({list_params}) {{{body}}}")
263318
} else {
264-
glue::glue("{name} <- function({list_params}) {{\n {calls}\n}}")
319+
glue::glue("{name} <- function({list_params}) {{{body}}}")
265320
}
266-
}, funs$name, funs$list_params, funs$calls, funs$roxygen_comment, SIMPLIFY = TRUE)
321+
}, funs$name, funs$list_params, funs$calls, funs$roxygen_comment, funs$type_checks, SIMPLIFY = TRUE)
267322

268323
out <- glue::trim(out)
269324
out <- glue::glue_collapse(out, sep = "\n\n")
270325
unclass(out)
271326
}
272327

328+
# Helper function to convert C++ default values to R
329+
convert_cpp_default_to_r <- function(cpp_default) {
330+
cpp_default <- trimws(cpp_default)
331+
332+
# Handle common cases
333+
if (cpp_default == "true" || cpp_default == "TRUE") {
334+
return("TRUE")
335+
} else if (cpp_default == "false" || cpp_default == "FALSE") {
336+
return("FALSE")
337+
} else if (grepl("^[0-9]+L?$", cpp_default)) {
338+
# Integer literal
339+
return(paste0(sub("L$", "", cpp_default), "L"))
340+
} else if (grepl("^[0-9.]+[fF]?$", cpp_default)) {
341+
# Float/double literal
342+
return(sub("[fF]$", "", cpp_default))
343+
} else if (grepl('^".*"$', cpp_default) || grepl("^'.*'$", cpp_default)) {
344+
# String literal - keep as is
345+
return(cpp_default)
346+
} else if (cpp_default == "NULL" || cpp_default == "nullptr") {
347+
return("NULL")
348+
}
349+
350+
# Default: keep as-is and hope for the best
351+
cpp_default
352+
}
353+
354+
# Helper function to generate type checking/coercion code
355+
generate_type_check <- function(param_name, param_type) {
356+
# Map C++ types to R coercion functions
357+
if (param_type == "int" || grepl("^int[[:space:]]*$", param_type)) {
358+
return(glue::glue("{param_name} <- as.integer({param_name})"))
359+
} else if (param_type == "double" || grepl("^double[[:space:]]*$", param_type)) {
360+
return(glue::glue("{param_name} <- as.numeric({param_name})"))
361+
} else if (param_type == "bool" || grepl("^bool[[:space:]]*$", param_type)) {
362+
return(glue::glue("{param_name} <- as.logical({param_name})"))
363+
} else if (grepl("string", param_type, ignore.case = TRUE)) {
364+
return(glue::glue("{param_name} <- as.character({param_name})"))
365+
}
366+
367+
# Handle cpp4r matrix types - set proper storage mode
368+
if (grepl("integers_matrix", param_type)) {
369+
return(glue::glue("storage.mode({param_name}) <- \"integer\""))
370+
} else if (grepl("doubles_matrix", param_type)) {
371+
return(glue::glue("storage.mode({param_name}) <- \"double\""))
372+
} else if (grepl("logicals_matrix", param_type)) {
373+
return(glue::glue("storage.mode({param_name}) <- \"logical\""))
374+
}
375+
376+
# Handle cpp4r vector types - set proper storage mode as well
377+
if (grepl("^integers[^_]", param_type) || param_type == "integers") {
378+
return(glue::glue("storage.mode({param_name}) <- \"integer\""))
379+
} else if (grepl("^doubles[^_]", param_type) || param_type == "doubles") {
380+
return(glue::glue("storage.mode({param_name}) <- \"double\""))
381+
} else if (grepl("^logicals[^_]", param_type) || param_type == "logicals") {
382+
return(glue::glue("storage.mode({param_name}) <- \"logical\""))
383+
} else if (grepl("^strings[^_]", param_type) || param_type == "strings") {
384+
return(glue::glue("storage.mode({param_name}) <- \"character\""))
385+
}
386+
387+
# For other cpp4r types, don't add checks (they handle conversion internally)
388+
return("")
389+
}
390+
273391
extract_roxygen_comments <- function(file) {
274392
lines <- readLines(file)
275393

@@ -356,12 +474,6 @@ get_call_entries <- function(path, names, package) {
356474
res[seq(mid, end)]
357475
}
358476

359-
pkg_links_to_rcpp <- function(path) {
360-
deps <- desc::desc_get_deps(file.path(path, "DESCRIPTION"))
361-
362-
any(deps$type == "LinkingTo" & deps$package == "Rcpp")
363-
}
364-
365477
get_register_needs <- function() {
366478
res <- read.dcf(system.file("DESCRIPTION", package = "cpp4r"))[, "Config/Needs/cpp4r/register"]
367479
strsplit(res, "[[:space:]]*,[[:space:]]*")[[1]]

R/vendor.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
#' **you**. Bugfixes and new features in cpp4r will not be available for your
1313
#' code until you run `cpp_vendor()` again.
1414
#'
15-
#' @param path The directory to vendor the headers into
15+
#' @param path The directory with the vendored headers. It is recommended to use `"./src/vendor"`.
16+
#' The default is `NULL`.
1617
#' @return The path to the vendored code (invisibly).
1718
#' @export
1819
#' @examples
1920
#' # create a new directory
2021
#' dir <- paste0(tempdir(), "/", gsub("\\s+|[[:punct:]]", "", Sys.time()))
21-
#' dir.create(dir, recursive = TRUE)
22+
#' dir.create(dir, recursive = TRUE, showWarnings = FALSE)
2223
#'
2324
#' # vendor the cpp4r headers into the directory
2425
#' vendor(dir)
@@ -27,7 +28,7 @@
2728
#'
2829
#' # cleanup
2930
#' unlink(dir, recursive = TRUE)
30-
vendor <- function(path = "./src/vendor") {
31+
vendor <- function(path = NULL) {
3132
if (is.null(path)) {
3233
stop("You must provide a path to vendor the code into", call. = FALSE)
3334
}

README.Rmd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ knitr::opts_chunk$set(
1313
)
1414
```
1515

16-
# cpp4r
16+
# cpp4r: Header-Only 'C++' and 'R' Interface
1717

1818
<!-- badges: start -->
1919
[![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable)
@@ -32,6 +32,9 @@ compilation for code snippets.
3232
cpp4r can be used as a replacement for cpp11 in existing or new packages. Think of cpp11 and cpp4r as MySQL and MariaDB:
3333
they are almost identical, but cpp4r has some extra features.
3434

35+
cpp4r requires at least C++11. It is compatible with C++14, C++17, C++20, C++23 as well. The headers are written in
36+
a way that is compatible with all of these standards and newer features are used if the compiler supports them.
37+
3538
After discussing some [pull requests](https://github.com/pachadotdev/cpp11/pulls/pachadotdev) with Hadley Wickham from
3639
Posit, it was mentioned that I should create my own fork to add the following features:
3740

cpp4rsampling/DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ Description: ADD DESCRIPTION. TWO OR MORE LINES
1515
License: Apache License (>= 2)
1616
BugReports: https://github.com/USERNAME/PKGNAME/issues
1717
URL: https://WEBSITE.COM
18-
RoxygenNote: 7.3.2
18+
RoxygenNote: 7.3.3
1919
Encoding: UTF-8
2020
NeedsCompilation: yes
2121
VignetteBuilder: knitr
2222
LinkingTo: cpp4r
2323
Config/testthat/edition: 3
24+
SystemsRequirements: C++NN

cpp4rsampling/bench/run_all_benchmarks.R

Whitespace-only changes.

0 commit comments

Comments
 (0)