Hello World & Building
Hello, World
Both write to standard output and both make you supply the newline.
cat is the one that prints exactly what you give it; print adds the [1] index marker that makes R output recognizable.cat("Hello, World!\n")#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}The habit to carry across is the opposite of the one R teaches: at an interactive prompt an expression's value is printed automatically, and in a script it is not. That is the most common reason an R example appears to do nothing. C++ has no auto-printing at all, so every value that reaches the screen does so because a line of code sent it there.
The build step, and what it buys
Both print the same number. The difference is the branch that never runs — uncomment the line on the right and the program stops existing, while R happily ships the same mistake.
# Rscript report.R — the interpreter reads and evaluates as it goes.
# A misspelled name inside a branch you never take is not an error
# until that branch runs.
total <- 0
for (value in c(3, 1, 4, 1, 5)) total <- total + value
if (total > 1000) {
print(mispelled_name) # never evaluated, never complained about
}
cat("total:", total, "\n")// g++ -std=c++23 -O2 main.cpp -o program
//
// Every branch is read before the program exists, including the ones
// that never run. Out comes machine code with no interpreter beneath.
#include <iostream>
#include <vector>
int main() {
int total = 0;
for (int value : {3, 1, 4, 1, 5}) total += value;
if (total > 1000) {
// std::cout << mispelled_name; // ← uncomment: it will not build
}
std::cout << "total: " << total << std::endl;
return 0;
}For an R programmer the compile step is the unfamiliar cost, and it is worth being realistic about it: the fast edit-and-look loop you have at the console is gone, and a real C++ build takes seconds to minutes rather than nothing. What you get is that every name, every type and every argument count is checked before anything runs, and that the result is machine code with no interpreter overhead per operation — which is the whole point of the Performance section at the end.
Everything Is a Vector
🚨 There is no scalar, and no implicit loop
This is the row the page turns on. In R,
x + y on two vectors is a single call into compiled code; in C++ there is no such operator and you write the loop — which is the same loop, made visible, running at the same speed.x <- c(1, 2, 3, 4)
y <- c(10, 20, 30, 40)
# Every operation is vectorized. This is ONE call into compiled code,
# not four interpreted iterations.
print(x + y)
print(x * 2)
print(sqrt(x))
# And a "scalar" is a vector of length one:
print(length(5))#include <cmath>
#include <iostream>
#include <vector>
int main() {
std::vector<double> x{1, 2, 3, 4};
std::vector<double> y{10, 20, 30, 40};
// No vectorized operators. You write the loop, and it costs nothing.
std::vector<double> sum(x.size());
for (std::size_t index = 0; index < x.size(); ++index) {
sum[index] = x[index] + y[index];
}
for (double value : sum) std::cout << value << ' ';
std::cout << std::endl;
// A scalar is genuinely a scalar. Nothing is a vector of length one.
double single = 5;
std::cout << single << std::endl;
return 0;
}The reframing worth doing early: R's vectorized operators already are the C++ loop, written once inside R's own C sources. So "vectorize it" and "write it in C++" are two ways of reaching the same machine code, and the reason to reach for C++ is the case where no vectorized formulation exists — a recursion, a simulation whose step depends on the last, a loop with early exit. Note also that a length-one vector is R's scalar, which is why
length(5) is 1 and why R has no distinction to lose.sapply becomes a loop or a range
Logical subsetting becomes
filter and sapply becomes transform, composed with the pipe operator — which reads left to right the way a magrittr or native pipe does.values <- c(1, 2, 3, 4, 5, 6)
evens <- values[values %% 2 == 0]
squares <- sapply(evens, function(value) value^2)
cat(squares, "\n")
cat(sum(squares), "\n")#include <iostream>
#include <numeric>
#include <ranges>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4, 5, 6};
auto squaresOfEvens = values
| std::views::filter([](int value) { return value % 2 == 0; })
| std::views::transform([](int value) { return value * value; });
int total = 0;
for (int value : squaresOfEvens) {
std::cout << value << ' ';
total += value;
}
std::cout << std::endl << total << std::endl;
return 0;
}Two differences worth knowing. The C++ version is lazy and materializes nothing:
squaresOfEvens is a recipe, and the work happens as the loop pulls values through, so there is no intermediate vector the way evens is one in R. And the lambda is checked at compile time against the element type, so an operation the type does not support is a build error rather than a run-time one. Add | std::ranges::to<std::vector>() when you actually want the vector.Copy-on-Modify
Copy-on-modify becomes copies you can see
The two programs print the same thing and get there differently. R defers the copy until something is modified; C++ copies at the assignment, every time, and you opt out with
&.first <- c(1, 2, 3)
second <- first # no copy yet — both names share one vector
second[1] <- 99 # NOW it copies, silently, because you modified it
cat(first, "\n")
cat(second, "\n")#include <iostream>
#include <vector>
int main() {
std::vector<int> first{1, 2, 3};
std::vector<int> second = first; // the copy happens HERE, always
second[0] = 99;
for (int value : first) std::cout << value << ' ';
std::cout << std::endl;
for (int value : second) std::cout << value << ' ';
std::cout << std::endl;
// To share instead of copy, say so:
std::vector<int>& alias = first;
alias[0] = 7;
std::cout << first[0] << std::endl;
return 0;
}R's copy-on-modify is a performance strategy that is invisible until it is not — the classic R slowdown is a loop that grows a vector, where each
x[i] <- value may copy the whole thing. C++ makes the copy explicit and therefore predictable, and gives you three ways to avoid it: a reference (&), a const reference for read-only access, and std::move to hand ownership over. The habit worth forming is const std::vector<double>& for any parameter you only read.Growing a vector, and reserving space
The same lesson in both languages, with the C++ version able to say it out loud:
reserve allocates the room up front, so the loop never reallocates and the capacity printed at the end is exactly what was asked for.# The classic R slowdown: the vector is reallocated as it grows.
values <- c()
for (index in 1:5) values <- c(values, index * index)
cat(values, "\n")
# The idiomatic fix is to allocate first:
preallocated <- numeric(5)
for (index in 1:5) preallocated[index] <- index * index
cat(preallocated, "\n")#include <iostream>
#include <vector>
int main() {
std::vector<int> values;
values.reserve(5); // one allocation, not several
for (int index = 1; index <= 5; ++index) {
values.push_back(index * index);
}
for (int value : values) std::cout << value << ' ';
std::cout << std::endl;
std::cout << "size " << values.size()
<< ", capacity " << values.capacity() << std::endl;
return 0;
}The difference is what happens when you forget. R's
c(values, index) allocates a new vector and copies on every iteration, which is quadratic; std::vector::push_back grows geometrically, so it is amortized constant and forgetting reserve costs a small constant factor rather than an order of magnitude. That is why "preallocate" is R advice and "reserve when you know the size" is merely a C++ nicety.Types
A variable has one type now
🚨 The R trap worth naming first:
42 is a double, and 42L is the integer. C++ has that distinction the other way round — 42 is an int and 42.0 is a double.count <- 42 # a double, despite looking like an integer
count <- "now a string" # perfectly legal
explicit_integer <- 42L
cat(class(42), class(42L), class(TRUE), class("x"), "\n")
# And the one that surprises people:
cat(0.1 + 0.2 == 0.3, "\n")#include <iostream>
#include <string>
int main() {
int count = 42;
// count = "now a string"; ← uncomment: the build fails here
double ratio = 0.5;
bool enabled = true;
std::string label = "x";
std::cout << count << ' ' << ratio << ' ' << enabled << ' ' << label << std::endl;
std::cout << (0.1 + 0.2 == 0.3) << std::endl; // the same answer
return 0;
}The floating-point surprise is shared, because both use IEEE 754:
0.1 + 0.2 != 0.3 in R, in C++ and everywhere else. What changes is that C++ has several integer sizes and no automatic promotion to a bigger one, so overflow is possible where R's doubles would simply lose precision — and 🚨 signed overflow in C++ is undefined behavior rather than a wrong number, which R has no equivalent of. Note also that bool prints as 1, not TRUE.NA has no equivalent
R has a missing value in every type and C++ has none. The nearest thing is
NaN, which works only for floating point and propagates the way NA does.values <- c(1, NA, 3)
cat(sum(values), "\n") # NA — it propagates
cat(sum(values, na.rm = TRUE), "\n") # 4
cat(is.na(values), "\n")
# NA is a first-class value in every vector type, and every function
# in base R has an opinion about it.#include <cmath>
#include <iostream>
#include <optional>
#include <vector>
int main() {
// There is no NA. The two workable stand-ins:
// NaN, for doubles only, and it propagates like NA does
std::vector<double> values{1, std::nan(""), 3};
double total = 0;
for (double value : values) total += value;
std::cout << total << std::endl; // nan
double totalSkippingMissing = 0;
for (double value : values) {
if (!std::isnan(value)) totalSkippingMissing += value;
}
std::cout << totalSkippingMissing << std::endl;
// std::optional<int>, for anything else — but it cannot live
// inside a plain vector of ints.
return 0;
}This is one of the sharper losses when porting statistical code, and it needs a design decision rather than a translation. The options are: use
NaN and be careful (fine for doubles, unavailable for integers and strings), carry a parallel vector of flags (which is what R does internally for integers), or use std::vector<std::optional<T>> and pay a byte per element plus a branch per access. Rcpp's NumericVector preserves R's NA properly, which is one of its quieter advantages over plain C++.Indexing & Recycling
🚨 Indexing starts at zero
🚨 Two traps in one row. Indexing starts at zero, and
values[-1] does not mean "everything but the first" — it reads memory before the vector, which is undefined behavior with no error message.values <- c(10, 20, 30, 40)
cat(values[1], "\n") # the FIRST element
cat(values[length(values)], "\n")
cat(values[-1], "\n") # everything EXCEPT the first
cat(values[c(TRUE, FALSE)], "\n") # logical subsetting, recycled#include <iostream>
#include <vector>
int main() {
std::vector<int> values{10, 20, 30, 40};
std::cout << values[0] << std::endl; // the first
std::cout << values.back() << std::endl; // the last
// values[-1] is not "drop the first" — it is undefined behavior,
// reading memory before the vector.
for (std::size_t index = 1; index < values.size(); ++index) {
std::cout << values[index] << ' ';
}
std::cout << std::endl;
return 0;
}Negative indexing, logical subsetting and
values[c(2, 4)] selection by index vector are all R conveniences with no C++ equivalent; each becomes an explicit loop or a range pipeline. And values[10] on a four-element vector is not an error either — .at(10) is the checked version that throws. An R programmer's instinct that an out-of-range index yields NA is the single most dangerous habit to carry across.Recycling has no equivalent, and that is a relief
Recycling is R behaving helpfully: the shorter vector repeats until the lengths match. C++ does nothing of the kind, so the modulo in the loop is you choosing recycling explicitly.
long_vector <- c(1, 2, 3, 4, 5, 6)
short_vector <- c(10, 20)
# The short one is RECYCLED to the length of the long one.
cat(long_vector + short_vector, "\n")
# And when the lengths do not divide evenly, R warns but still does it:
cat(suppressWarnings(long_vector + c(1, 2, 3, 4)), "\n")#include <iostream>
#include <vector>
int main() {
std::vector<int> longVector{1, 2, 3, 4, 5, 6};
std::vector<int> shortVector{10, 20};
// Nothing is recycled. If you want that behavior you write it,
// which means you decide what happens at the boundary.
std::vector<int> sum(longVector.size());
for (std::size_t index = 0; index < longVector.size(); ++index) {
sum[index] = longVector[index] + shortVector[index % shortVector.size()];
}
for (int value : sum) std::cout << value << ' ';
std::cout << std::endl;
return 0;
}Most R programmers have been bitten by this at least once — a length mismatch that should have been an error becomes a silently recycled result, and the warning for a non-dividing length is easy to miss in a long script. Writing the intent out is more code and it is code that cannot surprise you. The general shape of the trade on this page: R makes the common case short and the failure quiet, C++ makes both explicit.
Data Frames Become Structs
A data frame becomes a struct of vectors
A data frame is a list of columns, and the faithful C++ translation is a struct of vectors rather than a vector of structs — which keeps each column contiguous, exactly as R stores it.
people <- data.frame(
name = c("ada", "bob", "cyd"),
score = c(90, 75, 88),
stringsAsFactors = FALSE
)
print(people[people$score > 80, ])
cat(mean(people$score), "\n")#include <iostream>
#include <numeric>
#include <string>
#include <vector>
// A data frame is columns, so the C++ shape is a struct OF vectors —
// not a vector of structs, which would scatter each column's values.
struct People {
std::vector<std::string> name;
std::vector<double> score;
};
int main() {
People people{{"ada", "bob", "cyd"}, {90, 75, 88}};
for (std::size_t row = 0; row < people.name.size(); ++row) {
if (people.score[row] > 80) {
std::cout << people.name[row] << ' ' << people.score[row] << std::endl;
}
}
double total = std::accumulate(people.score.begin(), people.score.end(), 0.0);
std::cout << total / people.score.size() << std::endl;
return 0;
}That layout choice matters more than it looks. A struct of vectors means an operation on one column touches only that column's memory, which is why it is fast and why it matches how R, pandas and Arrow all store tabular data. A vector of structs is the natural object-oriented shape and is worse here for the same reason. What you lose is everything a data frame does for you — named columns by string, factors,
subset, merge, aggregate, printing — all of which become loops or a library such as Arrow.A list becomes a struct or a map
An R list is a heterogeneous, named, growable container. A C++ struct is a fixed set of typed fields decided when the program is compiled — so
settings$timeout <- 30 has no equivalent that adds a field.# An R list holds anything, with names, and grows as you please.
settings <- list(level = "info", retries = 3L, tags = c("a", "b"))
settings$timeout <- 30
cat(settings$level, settings$retries, settings$timeout, "\n")
cat(names(settings), "\n")#include <iostream>
#include <string>
#include <vector>
// The shape is decided at compile time. Fields have types and cannot
// be added later.
struct Settings {
std::string level;
int retries;
std::vector<std::string> tags;
int timeout;
};
int main() {
Settings settings{"info", 3, {"a", "b"}, 30};
std::cout << settings.level << ' ' << settings.retries
<< ' ' << settings.timeout << std::endl;
return 0;
}The two replacements, and when each applies: a struct when the fields are known (nearly always — and you get compile-time checking and a misspelled field as a build error), or
std::map<std::string, X> when the keys genuinely vary at run time, which requires every value to be the same type X. For genuinely mixed values there is std::variant, listing the alternatives in advance. The R habit of returning a list of loosely related things becomes a small named struct, which is an improvement in both languages.Functions
Arguments get types, and lose their names
Default values transfer; argument matching by name does not. R lets you write
describe(count = 3, name = "widget") in any order, and C++ has no such thing at all.describe <- function(name, count = 1, loud = FALSE) {
text <- paste(count, "x", name)
if (loud) toupper(text) else text
}
cat(describe("widget"), "\n", sep = "")
cat(describe("widget", loud = TRUE), "\n", sep = "")
cat(describe(count = 3, name = "widget"), "\n", sep = "")#include <algorithm>
#include <format>
#include <iostream>
#include <string>
// Default arguments exist but are POSITIONAL: you cannot skip one,
// and you cannot pass them by name.
std::string describe(const std::string& name, int count = 1, bool loud = false) {
std::string text = std::format("{} x {}", count, name);
if (loud) std::ranges::transform(text, text.begin(), ::toupper);
return text;
}
// Overloading covers "skip the middle one":
std::string describe(const std::string& name, bool loud) {
return describe(name, 1, loud);
}
int main() {
std::cout << describe("widget") << std::endl;
std::cout << describe("widget", true) << std::endl;
std::cout << describe("widget", 3) << std::endl;
return 0;
}What replaces it is overloading — several functions with one name, chosen by the argument types — plus, in serious code, a small struct of options passed as one argument, which is the C++ answer to a function with eight named parameters. R's partial matching (
describe(lo = TRUE)) has no counterpart either, and losing it is a mercy. The other change: an argument's type is checked, so describe(42) is a build error rather than a strange result.Arguments are evaluated before the call
Watch the order of the three lines. R evaluates the argument when the parameter is first used, so "entered the function" prints first; C++ evaluates it before the call, so "evaluating the argument" does.
# R arguments are LAZY: the expression is not evaluated until the
# function actually uses the parameter.
lazy <- function(value) {
cat("entered the function\n")
value
cat("used it\n")
}
lazy({ cat("evaluating the argument\n"); 42 })#include <iostream>
int expensive() {
std::cout << "evaluating the argument" << std::endl;
return 42;
}
// The argument is evaluated BEFORE the call. Always.
void eager(int value) {
std::cout << "entered the function" << std::endl;
(void)value;
std::cout << "used it" << std::endl;
}
int main() {
eager(expensive());
return 0;
}Lazy evaluation is what makes R's non-standard evaluation possible —
subset(data, score > 80) works because score > 80 arrives unevaluated and the function can inspect it. None of that transfers: a C++ function receives values, not expressions, and has no way to see the code that produced them. To defer work in C++ you pass a lambda and call it when you want it, which is explicit and considerably less magical.Errors
stop becomes throw
stop is throw and tryCatch is try/catch, with the C++ version selecting by exception type rather than by condition class. Catch by const reference.parse_count <- function(text) {
if (!grepl("^[0-9]+$", text)) stop("not a number: ", text)
as.integer(text)
}
result <- tryCatch(
parse_count("oops"),
error = function(condition) {
cat("caught: ", conditionMessage(condition), "\n", sep = "")
-1L
}
)
cat(result, "\n", sep = "")#include <iostream>
#include <stdexcept>
#include <string>
int parseCount(const std::string& text) {
for (char character : text) {
if (character < '0' || character > '9') {
throw std::invalid_argument("not a number: " + text);
}
}
return std::stoi(text);
}
int main() {
int result = -1;
try {
result = parseCount("oops");
} catch (const std::invalid_argument& error) {
std::cout << "caught: " << error.what() << std::endl;
}
std::cout << result << std::endl;
return 0;
}R's condition system is richer than it first appears —
warning, message, restarts and withCallingHandlers have no C++ equivalent, and neither does finally, whose job a destructor does instead. Going the other way, C++ exceptions cost real time when thrown and some codebases disable them entirely, so idiomatic modern C++ returns std::expected<T, E> for failures it expects and reserves exceptions for the genuinely exceptional.Rcpp: The Boundary
Rcpp, both halves
The
// [[Rcpp::export]] comment is the whole of the interface: Rcpp reads it, generates the wrapper, and the function appears in R with the right argument types. There is no header to write and no registration table.# What you write in R, once the C++ below is compiled:
#
# library(Rcpp)
# sourceCpp("fastmath.cpp") # compiles and loads it
#
# total_cpp(c(1, 2, 3, 4)) # 10
#
# sourceCpp compiles the file, generates the wrapper, and makes the
# function available as if it had been written in R. In a package the
# same thing happens at install time via compileAttributes().// fastmath.cpp — the whole file. There is no boilerplate beyond this.
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
double total_cpp(NumericVector values) {
double sum = 0;
for (int index = 0; index < values.size(); index++) {
sum += values[index];
}
return sum;
}
// The [[Rcpp::export]] comment is the entire interface declaration.
// NumericVector IS R's vector — no copy is made on the way in, and
// the R types (integer, character, list, logical) all have one.This is why Rcpp is on thousands of CRAN packages while the raw
.Call interface is on few. Two things worth noticing in six lines: NumericVector is R's actual vector rather than a copy of it, so passing a million elements costs nothing, and indexing it is zero-based even though the same vector is one-based in R — which is the single most common Rcpp bug.What crosses the boundary, and what it costs
🚨 This is the Rcpp trap that costs people an afternoon: a
NumericVector parameter is R's vector itself, so modifying it modifies the caller's data — breaking the copy-on-modify guarantee every R user relies on.# The three shapes, in increasing order of cost:
#
# NumericVector R's vector, in place. No copy. Modifying it
# modifies the caller's vector — which is NOT
# R semantics and surprises people.
#
# std::vector<double> a full copy on the way in and on the way out.
# Convenient, and O(n) each way.
#
# List / DataFrame structure preserved, element access checked.
#
# For a million-element vector the difference between the first two
# is the difference between free and two allocations.#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
void scale_in_place(NumericVector values, double factor) {
// 🚨 This modifies the CALLER'S vector. R's copy-on-modify does
// not apply here, so an R user who expected value semantics gets
// a surprise. clone() is how you opt back into copying.
for (int index = 0; index < values.size(); index++) {
values[index] *= factor;
}
}
// [[Rcpp::export]]
NumericVector scale_copy(NumericVector values, double factor) {
NumericVector result = clone(values);
for (int index = 0; index < result.size(); index++) {
result[index] *= factor;
}
return result;
}clone() is how you restore R's semantics, and it should be the default unless you have measured that the copy matters and documented that the function mutates. The wider rule for designing an Rcpp boundary: keep it coarse — one call doing a lot of work rather than many small ones — because each crossing has overhead, and prefer Rcpp's own vector types over std::vector for anything large, since they avoid the conversion entirely.When Rcpp is worth it, and when it is not
The test is not "is this slow" but "is this a loop R has to interpret". A vectorized expression is already compiled code, so rewriting it in C++ buys nothing; a scalar loop with a dependency between iterations is where the interpreter overhead lives.
# NOT worth rewriting — this is already one call into compiled code:
values <- runif(1e6)
total <- sum(values * 2)
cat(class(total), "\n")
# Worth rewriting — a loop whose step depends on the last one, which
# no vectorized formulation can express:
running_max <- function(values) {
result <- numeric(length(values))
best <- -Inf
for (index in seq_along(values)) {
best <- max(best, values[index])
result[index] <- best
}
result
}
cat(running_max(c(3, 1, 4, 1, 5)), "\n")#include <Rcpp.h>
using namespace Rcpp;
// The same running maximum, and this is the shape that pays: a tight
// scalar loop with a dependency between iterations. Expect something
// in the region of 50-100x over interpreted R for a loop like this.
//
// [[Rcpp::export]]
NumericVector running_max_cpp(NumericVector values) {
int count = values.size();
NumericVector result(count);
double best = R_NegInf;
for (int index = 0; index < count; index++) {
if (values[index] > best) best = values[index];
result[index] = best;
}
return result;
}The rough shape of the payoff, and it is worth being honest about both ends: a tight scalar loop typically runs 50 to 100 times faster, an already-vectorized operation runs about the same, and something dominated by memory allocation or by calls back into R may be slower. Profile with
profvis first and rewrite the one function that dominates — the R programmer's advantage here is that the boundary is cheap enough to move exactly one function across and leave everything else alone.Why The Loop Was Slow
What an interpreted loop actually costs
Both print the same number. The difference is what happens per iteration: R does a name lookup, a type check, an
NA check, a copy decision, an allocation and an operator dispatch, and C++ does an add and a compare.# Every iteration of this loop does far more than the arithmetic:
# look up the name, check the type, check for NA, decide whether to
# copy, allocate the result, and dispatch the operator.
total <- 0
for (index in 1:100000) total <- total + index
cat(total, "\n")
# The same work, vectorized — one call into C, and roughly 100x faster:
cat(sum(as.numeric(1:100000)), "\n")#include <iostream>
int main() {
// The loop compiles to a few machine instructions per iteration:
// an add and a compare. No lookup, no type check, no allocation,
// no dispatch.
long total = 0;
for (int index = 1; index <= 100000; ++index) total += index;
std::cout << total << std::endl;
return 0;
}That list is the answer to "why is my R loop slow", and it is also why the fix is usually vectorization rather than C++ — a vectorized call pays those costs once for the whole vector instead of once per element. Where C++ wins outright is the case vectorization cannot reach. It is worth knowing that R's own byte-compiler (on by default since R 3.4) removes some of this overhead, so the gap is smaller than it was, and still large.
Memory layout, and why it matters
An R numeric vector and a
std::vector<double> have the same layout — a contiguous run of doubles — which is the technical reason the Rcpp boundary can be free rather than a conversion.# An R numeric vector is a contiguous block of doubles with a header
# — the same layout C++ uses, which is why the boundary is cheap.
values <- c(1.5, 2.5, 3.5)
cat(typeof(values), length(values), "\n")
# A LIST is not: it is an array of pointers to separate objects, so
# iterating one touches scattered memory.
mixed <- list(1.5, "two", TRUE)
cat(typeof(mixed), length(mixed), "\n")#include <iostream>
#include <vector>
int main() {
// std::vector<double> is the same contiguous block R uses for a
// numeric vector — which is exactly why Rcpp can hand one over
// with no conversion.
std::vector<double> values{1.5, 2.5, 3.5};
std::cout << "contiguous: "
<< (&values[2] - &values[0] == 2) << std::endl;
std::cout << "bytes per element: " << sizeof(double) << std::endl;
std::cout << values.size() << std::endl;
return 0;
}The practical consequences run in both directions. Prefer atomic vectors over lists in R when the elements are all one type, for the same cache reasons a C++ programmer prefers
vector<double> over vector<shared_ptr<double>>. And when designing an Rcpp interface, pass numeric and integer vectors rather than lists or data frames where you can, because those are the ones that cross for nothing.