Search code examples
c++rrcpp

Length of Rcpp::List differs between Windows and Mac


I am checking the length of an Rcpp::List object, and notice that I get different results on Windows than on Mac.

C++ code

Here is my C++ code, in the file "test.cpp". I have a Data class, whose constructor takes an Rcpp::List and extracts its element named "constraints". Next, I have a function foo which constructs a Data element, extracts the length of constraints, and returns it.

#include <Rcpp.h>
using namespace Rcpp;

struct Data {
  Data(List data) :
  constraints { as<List>(data["constraints"]) } {}
  ~Data() = default;
  
  const List constraints;
};

// [[Rcpp::export]]
int foo(List dat) {
  Data dd {dat};
  int result = dd.constraints.length();
  return result;
}

Results running in R on Mac

Here is my session:

> sessionInfo()
R version 4.3.2 RC (2023-10-30 r85440)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Sonoma 14.1.2

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/Oslo
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] vctrs_0.6.4       cli_3.6.1         knitr_1.45        rlang_1.1.2       xfun_0.41         processx_3.8.2   
 [7] purrr_1.0.2       styler_1.10.2     glue_1.6.2        clipr_0.8.0       htmltools_0.5.7   ps_1.7.5         
[13] fansi_1.0.5       rmarkdown_2.25    R.cache_0.16.0    evaluate_0.23     tibble_3.2.1      fastmap_1.1.1    
[19] yaml_2.3.7        lifecycle_1.0.4   compiler_4.3.2    fs_1.6.3          Rcpp_1.0.11.2     pkgconfig_2.0.3  
[25] rstudioapi_0.15.0 R.oo_1.25.0       R.utils_2.12.3    digest_0.6.33     R6_2.5.1          reprex_2.0.2     
[31] utf8_1.2.4        pillar_1.9.0      callr_3.7.3       magrittr_2.0.3    R.methodsS3_1.8.2 tools_4.3.2      
[37] withr_2.5.2   

Next, I source the C++ code:

Rcpp::sourceCpp("test.cpp")

Giving foo a list whose constraints element is NULL, gives length zero as expected:

> data <- list(a = rnorm(100), constraints = NULL, b = letters)
> foo(data)
[1] 0

Next, giving it a list whose constraints element has 26 letters, returns length 26:

> data <- list(a = rnorm(100), constraints = letters, b = letters)
> foo(data)
[1] 26

Results running R on Windows

I run the same R code on Windows, but obtain different results.

Here is my session info:

> sessionInfo()
R version 4.3.2 (2023-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19045)

Matrix products: default


locale:
[1] C

time zone: Europe/Oslo
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] compiler_4.3.2           magrittr_2.0.3           R6_2.5.1                
 [4] cli_3.6.1                tools_4.3.2              RcppArmadillo_0.12.6.6.0
 [7] rstudioapi_0.15.0        Rcpp_1.0.11.6            brio_1.1.3              
[10] testthat_3.2.0           rlang_1.1.2            

I source the script again:

> Rcpp::sourceCpp("test.cpp")

Here is the result when constraints is NULL. Surprisingly, it returns length 1.

> data <- list(a = rnorm(100), constraints = NULL, b = letters)
> foo(data)
[1] 1

Here is the result when constraints has 26 letters. Again it returns length 1.

> data <- list(a = rnorm(100), constraints = letters, b = letters)
> foo(data)
[1] 1

Question

Is this expected? Am I doing something here which causes undefined behavior, and hence gives different results with different compilers/platforms?

How should I change the code to get the behavoir I want, which is that the length should be zero when it is NULL and nonzero otherwise?

Updated after answer

Based on Dirk Eddelbuettel's answer below, I realize that it's best to refactor my code such that it does not contain such a deep level of nesting. In any case, I think it's worthwhile to include here that Eddelbuettel's test returns the same on Windows as on Mac:

> Rcpp::cppFunction("int ll(List L) { return L.length(); }")
> ll(NULL)
[1] 0
> ll(letters)
[1] 26

Solution

  • I suspect this code:

    constraints { as<List>(data["constraints"]) }
    

    on Linux initializes constraints directly with as<List>(data["constraints"]), and on Windows initializes it as a list containing one item, which is as<List>(data["constraints"]).

    This {} initialization syntax is normally used to initialize with a list of things, via std::initializer_list. It might not be doing that on Linux, due to a slightly different version of R, the compiler, or the compiler options.

    Change { and } to ( and ) in this part, to force it to initialize constraints directly with as<List>(data["constraints"]).