Intro to C++

PHS 7045: Advanced Programming

Authors
Affiliation

George G. Vega Yon, Ph.D.

The University of Utah

Thi Mui Pham, Ph.D.

Published

September 17, 2024

Modified

September 12, 2024

Note

These slides are adapted from a lecture by George G. Vega Yon, Ph.D. (link).

Introduction

Learning objectives:

  • Understand the basics of C++ programming: syntax, types, and classes.
  • Learn how to write a simple C++ program, compile it, and run it.
  • Understand the differences between C++ and R.

We will need a compiler:

  • Windows: You can download Rtools from here.

  • MacOS: It is a bit complicated… Here are some options:

    • CRAN’s manual to get the clang, clang++, and gfortran compilers here.

    • A great guide by the coatless professor here

  • If you don’t have compiler installed, you can join the class via posit.cloud.

Why C++?

When R is not enough

You profiled your code and vectorized what you could. It’s still slow. Now what?

  • Some problems can’t be vectorized: each step depends on the previous one.
    • MCMC samplers, agent-based models, stochastic simulations, recursive filters.
  • R is interpreted and dynamically typed; C++ is compiled and statically typed.
    • For loop-heavy code, speedups of 10–100x are common.

You already depend on C++

You already rely on C++ every day:

  • Stan, data.table, dplyr, ranger, xgboost, glmmTMB, epiworld
  • Rcpp is one of the most depended-upon packages on CRAN.

. . .

Learning C++ means you can write the fast part yourself and read the source of the packages you depend on.

A first program

Hello world

The program

1#include<iostream>

2int main() {
3  std::cout << "Hello world" << std::endl;
4  return 0;
}
1
The equivalent to library() in R. This is part of the standard library.
2
C++ is type-explicit, so we always declare what are the data types.
3
Like in R, we have namespaces. We access the cout function from std (standard library). Also, the code ends with semicolon (;).
4
Explicit return.

We can use g++ to compile the code (-std=c++14 is the C++14 standard):

g++ -std=c++14 hello-world.cpp -o hello-world
./hello-world
Hello world

How your code actually runs

Two words we just used

Compiler: a program that translates your .cpp source code into machine code your CPU can run directly.

  • In R there is no such step: R code is handed to an interpreter instead (we come back to what that means).
  • In C++ you pay this cost once, up front, and get a standalone executable (hello-world). That is a large part of where the speed comes from.
  • The compiler also checks your types before the program ever runs, so many bugs become compile errors instead of runtime surprises.

Standard: the official version of the C++ language, revised every few years.

  • C++11, C++14, C++17, C++20, C++23 — each adds features and syntax.
  • -std=c++14 tells the compiler which version of the language to read your file as. Code using a C++17 feature will not compile under -std=c++14.
  • We use C++14 because it is what R and Rcpp assume by default.

What is actually running your code?

Diagram by William Lau, Wikimedia Commons, CC BY-SA 4.0.

  • The CPU (central processing unit) is the part that actually does things: arithmetic, comparisons, moving data around.
  • It understands only machine code: a long list of very simple numbered instructions (add these two numbers, jump to that instruction).
  • Memory holds both your data and the instructions. This is the RAMrandom-access memory, “random access” meaning the CPU can reach any address directly, in any order, at the same cost.
  • RAM is the big, cheap memory (gigabytes of it), but it sits outside the CPU, and it is volatile: it empties when the machine powers off. Your files live on disk; a running program lives in RAM.
  • The CPU runs one simple cycle, billions of times per second: fetch an instruction -> decode it -> execute it.

So what is a “program”?

A program is, in the end, a sequence of machine-code instructions the CPU can fetch and execute.

  • The CPU cannot read hello-world.cpp. It cannot read R code either.
  • Something must stand between your human-readable code and those instructions.
  • That is the whole difference between C++ and R: C++ turns your code into machine code ahead of time; R hands your code to a program that was compiled already, and that program acts on it while it runs.

How C++ runs your code

cpp src hello-world.cpp source code cc compiler g++ / clang++ -std=c++14 src->cc  compile   exe hello-world machine code cc->exe err compile error cc->err  type errors caught here   cpu CPU fetch / decode / execute exe->cpu  run  

  • The compiler does the translating; the standard (-std=c++14) tells it which version of the language to read your file as.
  • Compile once, run many times: the CPU is handed exactly the machine code it knows how to fetch, decode and execute — nothing stands in between.

The other word: interpreter

Interpreter: a program that reads your code and carries out what it says, while your program runs.

  • The interpreter is itself an ordinary compiled program. R on your laptop is an executable written mostly in C, compiled once by whoever built it — just like hello-world was.
  • When you type sqrt(2), you hand that program a piece of text. It works out what the text means and calls the C function that does the square root.
  • You have been talking to it all along. The > prompt is the interpreter, sitting there waiting for its next instruction.

. . .

A compiler is a translator: it turns the whole book into another language once, and you read the translation. An interpreter is a performer: it is handed the script and acts it out, line by line, every time.

How R runs your code

r cluster_r the R interpreter: a C program, compiled long ago src script.R source code parse parse + byte-compile your code (once) src->parse  source()   cpu CPU fetch / decode / execute eval evaluate it parse->eval eval->cpu  R's own machine code   eval->eval  again, every time the line runs  

  • Your R code is never turned into machine code. The machine code that runs is R’s own — R is a program written in C, compiled once, long before you started. Your script is data to it.
  • No compile step and no executable of your own: the interpreter sits between your code and the CPU for as long as the program runs.
  • What repeats is not translation but evaluation. For s + sqrt(i), on every pass R must look up what sqrt and + mean right now, check the types of s and i, and allocate a place for the result — before doing the one instruction of actual arithmetic.
  • That overhead is where the 10–100x from the first slide comes from.

. . .

The interpreter never leaves: the arithmetic is one CPU instruction, and everything around it is a hundred more.

The same loop, two different journeys

R — interpreted

s <- 0
for (i in 1:n)
  s <- s + sqrt(i)

Per iteration, the interpreter must:

  • look up what sqrt and + mean right now,
  • check the types of s and i,
  • allocate a result,
  • and only then do the arithmetic.

All of that, n times.

C++ — compiled

double s = 0.0;
for (int i = 1; i <= n; ++i)
  s += std::sqrt((double) i);

The compiler settled all of that once, before the program ran:

  • s is a double, i is an int — fixed, no checking,
  • sqrt is one machine instruction,
  • no allocation.

The CPU just adds numbers.

. . .

The arithmetic itself is equally fast in both. R’s + and sqrt are C functions inside the R binary, and they end up at the very same CPU instruction the C++ version uses. The difference is never the math — it is everything R has to do around the math, n times over.

The same loop, two different journeys (cont’d)

With n = 10,000,000 on my laptop:

time
R (for loop) ~0.145 s
C++ (g++ -O2) ~0.008 s
  • Roughly 18x here — and this is R at its best: the loop is simple enough for R’s byte-code compiler to help.
  • The C++ program also had to be compiled first (~1 s). You pay that once; you save on every run, and on every one of the 10 million iterations.
  • This is also why the R packages from the first slide are written in C++: the loop lives in C++, and you call it from R.

Compilers

g++ vs clang++

Both are C++ compilers: same language, same standards, (mostly) the same flags.

  • g++ — GNU Compiler Collection (GCC). Default on Linux and with Rtools on Windows.

  • clang++ — LLVM’s compiler. Default on macOS, shipped with Xcode command line tools.

  • The commands are interchangeable for everything in this lecture:

    g++     -std=c++14 hello-world.cpp -o hello-world
    clang++ -std=c++14 hello-world.cpp -o hello-world

g++ vs clang++ (cont’d)

  • Careful on macOS: g++ is usually just a symlink to clang++. Check with

    g++ --version

    If it says “Apple clang”, you are using clang, no matter what you typed.

  • Practical differences you may hit:

    • Error messages: clang’s are generally more readable.
    • OpenMP: works out of the box with g++; on macOS clang needs libomp installed separately (relevant next week for parallel computing).
    • Minor differences in warnings and in which C++ standard is the default.
  • For R packages, you do not choose directly: R uses whatever is set in ~/.R/Makevars / R CMD config CXX.

    system("R CMD config CXX")

Two worked examples

Example: Computing the mean

Download the program here.

#include<iostream> // To print
#include<vector>   // To use vectors

int main() {

  // Defining the data
  std::vector< double > dat = {1.0, 2.5, 4.4};
  
  // Making room for the output
  double ans = 0.0;

  // Looping through the data
  for (size_t i = 0; i < dat.size(); ++i)
    ans = ans + dat[i];

  ans = ans/dat.size();

  // Print out the value to the screen
  std::cout << "The mean of dat is " << ans << std::endl;

  // Returning
  return 0;

}
  • Loading the libraries
  • Creating a vector double with three values.
  • A for-loop that starts from zero and goes to the size of the vector, incrementing by one (++i).

To compile it, run the following command:

g++ -std=c++14 means.cpp -o means
./means
The mean of dat is 2.63333

Example: Computing the mean (take 2)

We can leverage modern C++ to make the code shorter with std::accumulate()

#include<iostream> // To print
#include<vector>   // To use vectors
#include<numeric>  // To use the accumulate function

int main() {

  // Defining the data
  std::vector< double > dat = {1.0, 2.5, 4.4};
  
  // Making room for the output
  double ans = std::accumulate(
    dat.begin(), dat.end(), 0.0
    );
  ans /= dat.size();

  // Print out the value to the screen
  printf("The mean of dat is %.2f\n", ans);

  // Returning
  return 0;

}
  • Using the numeric library that has the accumulate function
  • The std::accumulate function sums the elements of the vector.

To compile it, run the following command:

g++ -std=c++14 means2.cpp -o means2
./means2
The mean of dat is 2.63

Language fundamentals

Differences with R

Here are some differences between C++ and R:

Feature C++ R
Execution Compiled Interpreted
Type explicit? Yes No
Index starts at 0 1
for loop for (int i = 0; i < n; ++i) for (i in 1:n)
Line ending ; \n” (implicit)
  • Compiled: translated to machine code before running — faster execution. Interpreted: another program reads and carries out your code as it runs — interactive, but slower.

  • Type explicit: in C++, we always declare the type of the variables. In R, we don’t need to.

Fundamental types

Adapted from W3 Schools:

int my_num           = 5;       // Integer (whole number)
float my_float_num   = 5.99;    // Floating point number
double my_double_num = 9.98;    // Floating point number
char my_letter       = 'D';     // Character
bool my_boolean      = true;    // Boolean
std::string my_text  = "Hello"; // String

Vectors in C++ are similar to atomic vectors in R — all elements share one type:

std::vector< int > my_vector = {1, 2, 3, 4, 5};
std::vector< std::string > my_str_vector = {"a", "b", "c"};
std::vector< std::vector< int > > my_matrix = {{1, 2}, {3, 4}};

Basic data types

How much memory each type takes, and what fits in it:

Data type Size Description
bool 1 byte Stores true or false
char 1 byte A single character/letter/number, or ASCII value
int 2 or 4 bytes Whole numbers, no decimals
float 4 bytes Fractional numbers; ~6–7 decimal digits
double 8 bytes Fractional numbers; ~15 decimal digits

Adapted from W3 Schools.

Basic data types: why the sizes matter

  • In R you never see this: every number is a double (8 bytes) unless you work at it, and a “scalar” is really a length-1 vector with a header.

  • In C++ you choose, and the choice has consequences:

    • double vs float: 8 vs 4 bytes — half the memory, but only ~7 digits of precision.
    • A vector of 100 million ints is 400 MB; as doubles, 800 MB.
  • “2 or 4 bytes” for int is not a typo: sizes are implementation-defined. Check rather than assume:

    std::cout << sizeof(int) << std::endl;  // bytes
  • Smaller types also mean less data to move around: 1 million doubles is 8 MB, but only 4 MB as floats. For big arrays scanned repeatedly, that can matter more than the arithmetic itself.

. . .

This is a later concern. float buys speed by giving up precision (~7 digits vs ~15) — rarely a good trade in statistics. Default to double; worry about bytes when profiling says to.

Vectors in C++

  • Vectors make life easier, avoiding the need to manage memory.

  • Vectors store contiguous memory, allowing for fast access.

  • Vectors have many methods to manipulate the data:

my_vector.push_back(6); // Add an element
my_vector.pop_back();   // Remove the last element
my_vector.size();       // Number of elements
my_vector[0];           // Access the first element
my_vector.at(0);        // Access the first element (safer)

Vectors in C++: Looping

Looping through vectors can be done in different ways:

// Suppose we have this:
std::vector< int > my_vector = {1, 2, 3, 4, 5};

// Typical loop
for (int i = 0; i < my_vector.size(); ++i) {
  std::cout << my_vector[i] << std::endl;
}

// Using vector's iterators (begin and end)
// and the auto keyword
for (auto i = my_vector.begin(); i != my_vector.end(); ++i) {
  std::cout << *i << std::endl;
}

// Using range-based for loop (with the auto keyword)
for (auto i: my_vector) {
  std::cout << i << std::endl;
}
  • The typical loop, access elements by index.
  • Using iterators: i is an iterator (pointer-like), and *i is the value it refers to.
  • The range-based for loop, simpler and cleaner.
  • auto: “compiler, work out the type for me” — here std::vector<int>::iterator and int. Deduced at compile time; the variable still has one fixed type.
  • auto i gives you a copy. Use auto & i to modify the vector, and const auto & i to read big objects without copying them.

Important keywords

Types can go accompanied by keywords:

1const int x = 5;
2double fun(int x)
3double fun(const int x)
4double fun(int & x)
5double fun(const int & x)
6double fun(int * x)
1
const: the value of x cannot be changed. Trying to modify it will result in a compilation error.
2
x is passed by copy (not ideal for large objects). It can be modified inside the function.
3
x is still a copy, but it cannot be modified.
4
&: passing by reference. Ideal for large objects. It can be modified.
5
const &: passing by reference, but cannot be modified.
6
*: passing by pointer. The value can be modified. NOT RECOMMENDED FOR C++

Important keywords: Example with pointers

The following code (pointers.cpp) illustrates how these keywords work:

#include <cstdio> // For the std version of printf

void set_x_copy(int x, int y) {x = y;};
void set_x(int * x, int y) {*x = y;};
void set_x_ref(int & x, int y) {x = y;};
// This would generate an error
// void set_x_ref(const int & x, int y) {x = y;};

int main() {

    int x = 0;

    set_x_copy(x, 3);
    std::printf("x = %d\n", x);
    set_x(&x, 2);
    std::printf("x = %d\n", x);
    set_x_ref(x, 1);
    std::printf("x = %d\n", x);

    return 0;

}
  • set_x_copy: the x inside the function is a new int at a different address, initialised from the caller’s value. x = y writes there, the caller’s x is never touched — and it is destroyed on return.
  • set_x: x is a pointer — its own variable, whose value is an address. &x at the call site means “the address of x”; *x = y means “write y to whatever lives at that address”.
  • set_x_ref: x is a reference — an alias, a second name for the caller’s memory, with no storage of its own. x = y writes straight to it.
  • Note the argument is written the same way as in the copy case — plain x, not &x. Only the pointer version shows up at the call site. Here the names help you, but in real code (update(v)) nothing at the call tells you whether v can be modified: you have to read the signature.
  • Passing by reference is the preferred way in C++.
  • With const int & x the compiler refuses to build it: you promised not to modify what x refers to.

To compile and run the code:

g++ -std=c++14 pointers.cpp -o pointers
./pointers
x = 0
x = 2
x = 1

What is actually in memory?

Picture main’s x sitting at address 0x100, holding 0. Each call does something different with it:

refs cluster_ref set_x_ref(int & x, int y) cluster_copy set_x_copy(int x, int y) cluster_ptr set_x(int * x, int y) na x in main() ma 0 at 0x100 na->ma nx x in set_x_copy() mx 3 at 0x200 nx->mx na3 x in main() m3 2 at 0x100 na3->m3 nx3 x in set_x() p3 0x100 at 0x300 nx3->p3 p3->m3 *x na2 x in main() m2 1 at 0x100 na2->m2 nx2 x in set_x_ref() nx2->m2

What is actually in memory? (cont’d)

  • set_x_copy(int x, int y) — the parameter x is a brand-new int at a different address (say 0x200), initialised with a copy of the caller’s value. x = y writes 3 to 0x200; nothing at 0x100 is touched, so main’s x is still 0. On return, the parameter is destroyed.

  • set_x(int * x, int y) — the parameter x is a pointer: a separate variable (at, say, 0x300) whose value is an address. &x at the call means “the address of x”, so the caller passes 0x100 and the parameter holds 0x100. *x means “the thing at the address stored in x”, so *x = y writes 2 to 0x100.

    • The pointer itself is destroyed on return — that does not matter, since what it pointed at was modified.
  • set_x_ref(int & x, int y) — the parameter x is a reference: an alias, a second name for the same memory at 0x100, with no storage of its own. x = y writes 1 straight to 0x100.

    • The argument is written exactly as in the copy case — plain x, not &x. Only the pointer announces itself at the call site, so for the other two you must read the signature to know what will happen.

. . .

  • Cost: copying a std::vector<double> with a million elements copies 8 MB; a reference or a pointer copies an address (8 bytes).
  • In R you never choose — arguments always behave like copies. In C++ you decide, and const & gives you both: no copy, and a promise not to modify.

Classes in C++

Example class (you can download the file here):

1#ifndef PERSON_HPP
#define PERSON_HPP

#include<string>
#include<iostream>

class Person {
2private:
    std::string name;
    int age;
    double height;

3public:
  // Constructor
4  Person(std::string n, int a, double h) {
    name = n;
    age = a;
    height = h;
  };

  // Default constructor
  Person() : name("Unknown"), age(0), height(0.0) {};

  // Destructor
5  ~Person() {
    std::cout <<
6      this->name + " destroyed" <<
      std::endl;
  };

  // Getters and setters
7  std::string get_name() { return name; };
  void set_name(std::string n) { name = n; };
};

#endif
1
The #ifndef + #define + #endif is the include guard. Avoids multiple inclusions.
2
Private members: only accessible within the class.
3
Public members: accessible from outside the class.
4
Constructor: initializes the object.
5
Destructor: called when deleting the object.
6
Internal elements can be accessed with this->.
7
Access: methods to access and modify private members.

Classes in C++ (cont.)

Using the class (you can download the file here):

#include<string>
#include<iostream>
#include "person.hpp"

int main() {
  Person p1; // Default constructor
  Person p2("John", 30, 1.80); // Other constructor

  std::cout << p1.get_name() << std::endl;
  std::cout << p2.get_name() << std::endl;

  return 0;
}

Compiling and executing the program:

g++ -std=c++14 person.cpp -o person
./person
Unknown
John
John destroyed
Unknown destroyed

Notice that the destructor is called when p1 and p2 go out of scope (in reverse order).

Classes in C++: Declaration and Implementation

  • A good practice is to separate the declaration (bones) from the implementation (meat).

  • Looking at an extract of the class Person:

// ---------------------------------------
// Declarations: Arguments and data types
// ---------------------------------------
class Person {
private:
    std::string name;
    int age;
    double height;

public:
  // Constructor
  Person(std::string n, int a, double h);

  // Getters and setters
  std::string get_name();
};

// ---------------------------------------
// Implementation: Body of the functions
// ---------------------------------------
inline Person::Person(std::string n, int a, double h) {
  name = n;
  age = a;
  height = h;
};

inline std::string Person::get_name() {
  return name;
};

Overloading

  • In C++, we can have multiple functions with the same name, but different arguments. This is called overloading.

  • The compiler will choose the correct function based on the arguments. Both of these functions are valid:

int add_int(int x, int y) {
  return x + y;
}

double add_double(double x, double y) {
  return x + y;
}

float add_float(float x, float y) {
  return x + y;
}
int add(int x, int y) {
  return x + y;
}

double add(double x, double y) {
  return x + y;
}

float add(float x, float y) {
  return x + y;
}

Templates

  • In C++, we can use templates to create functions or classes that can work with any data type.

  • This is useful when we want to create a function that works with int, double, float, etc.

int add(int x, int y) {
  return x + y;
}

double add(double x, double y) {
  return x + y;
}

float add(float x, float y) {
  return x + y;
}
1template<typename T>
T add(T x, T y) {
  return x + y;
}

2template<>
float add(float x, float y) {
  std::cout<< "This is a float!" << std::endl;
  return x + y;
}
1
Template declaration (the generic type is T).
2
Specialization for float.

Templates (cont.)

Classes can also be templated (defined in template_class.cpp):

#include<iostream>

template<typename T>
class MyAdder {
private:
  T x;
  T y;
public:
  MyAdder(T x, T y) : x(x), y(y) {};

  T add() {
    return x + y;
  };
};

int main() {
  MyAdder<int> a(1, 2);
  MyAdder<double> b(1.0, 2.0);

  std::cout << a.add() << std::endl;
  std::cout << b.add() << std::endl;

  return 0;
}
  • The class is templated. The value T can be any type.
  • The template can be used any time we specify the type.
  • The class is instantiated with int and double. The compiler will generate two classes during compilation.
g++ -std=c++14 template_class.cpp -o template_class
./template_class
3
3

Compared with R

Simulating pi

  • One way to estimate \(\pi\) is to simulate points in a square and count how many are inside a circle.

  • The following is an optimized R function to do this:

my_pi_sim <- function(n) {
  xy <- matrix(runif(n*2, min=-1, max=1), ncol = 2)
  message(
    sprintf(
      "pi approx to: %.4f",
      mean(sqrt(rowSums(xy^2)) <= 1) * 4
    )
  )
}

set.seed(331)
my_pi_sim(1e6)
pi approx to: 3.1393

Let’s see why this works, and then how to do it in C++.

Why does throwing darts at a square give us pi?

  • Draw the unit circle (radius \(r = 1\)) inside the square \([-1, 1] \times [-1, 1]\).

  • The square has area \(2 \times 2 = 4\). The circle has area \(\pi r^2 = \pi\).

  • If we pick a point uniformly at random in the square, every location is equally likely, so the probability of landing in the circle is just the ratio of the areas:

\[ P(\text{point inside circle}) \;=\; \frac{\text{area of circle}}{\text{area of square}} \;=\; \frac{\pi}{4}. \]

  • We cannot compute that probability directly (it contains the \(\pi\) we are after), but we can estimate it: throw \(n\) points and count the hits.

From a proportion to an estimate of pi

  • Let \(\hat p = (\text{number of hits}) / n\). By the law of large numbers \(\hat p \to \pi/4\) as \(n\) grows, so

\[ \hat\pi = 4 \hat p = 4 \times \frac{\text{number of hits}}{n}. \]

  • That is exactly the * 4 in the R code and the 4.0*pi_approx/n_sims in the C++ code.

  • Deciding if a point is inside: the point \((x, y)\) is in the unit circle when its distance to the origin is at most 1, i.e. \(\sqrt{x^2 + y^2} \le 1\).

  • How accurate? Each point is a Bernoulli trial, so \(\text{SE}(\hat\pi) = 4\sqrt{p(1-p)/n} \approx 1.64/\sqrt{n}\). The error shrinks like \(1/\sqrt{n}\): with \(n = 5 \times 10^6\) we get about \(\pm 0.0007\), i.e. roughly three correct decimals. One extra digit costs 100 times more points — which is why we care about speed.

Simulating pi in C++

#include <vector>
1#include <random>
2#include <cmath>
#include <cstdio>
int main() {
  
  // Setting the seed
3  std::mt19937 rng_engine;
  rng_engine.seed(123);

4  std::uniform_real_distribution<double> dist(-1.0, 1.0);

  // Number of simulations
  size_t n_sims = 5e6;

  // Defining the data
  double pi_approx = 0.0;
  for (size_t i = 0u; i < n_sims; ++i)
  {

    // Generating a point in the unit square
    double x = dist(rng_engine);
    double y = dist(rng_engine);

    double d = std::sqrt(
5        std::pow(x, 2.0) + std::pow(y, 2.0)
        );

    // Checking if the point is inside the unit circle 
    if (d <= 1.0)
      pi_approx += 1.0;

  }

  printf("pi approx to %.4f\n", 4.0*pi_approx/n_sims);

  return 0;

}
1
Library for random numbers and stats distributions.
2
std::sqrt/std::pow live in <cmath>, printf in <cstdio>.
3
Random number engine (used in comb. with the distributions).
4
Uniform distribution between -1 and 1.
5
std::pow is the power function.
g++ -std=c++14 pi.cpp -o pi
./pi
pi approx to 3.1420

Extended example: Writing a summary class

Writing a summary class

  • The task is to write a class that computes the mean, standard deviation, minimum and maximum of a vector.

  • The class should be a template class so it can deal with double and int.

Full program

You can download the full C++ code here and the header file here:

cat("```cpp\n")

```{.r .cell-code}
cat(readLines("summary.hpp"), sep = "\n")
Warning in readLines("summary.hpp"): incomplete final line found on
'summary.hpp'

#ifndef SUMMARY_HPP #define SUMMARY_HPP

#include #include #include #include

template class Summarizer { private: const std::vector* dat = nullptr; double n;

public: // Constructors Summarizer(const std::vector & dat_);

// Calculators
double mean() const;
double sd() const;
T min() const;
T max() const;

// Printer
void print() const;

};

template inline Summarizer::Summarizer(const std::vector & dat_) { dat = &dat_; n = dat->size(); };

template inline double Summarizer::mean() const { return std::accumulate( dat->begin(), dat->end(), 0.0 ) / n; };

template inline double Summarizer::sd() const { double m = mean(); double sum = 0.0; for (auto & i: *dat) sum += std::pow(i - m, 2.0); return std::sqrt(sum / (dat->size() - 1)); };

template inline T Summarizer::min() const { T min = (dat)[0]; for (std::size_t i = 1u; i < dat->size(); ++i) if ((dat)[i] < min) min = (*dat)[i]; return min; };

template inline T Summarizer::max() const { T max = (dat)[0]; for (std::size_t i = 1u; i < dat->size(); ++i) if ((dat)[i] > max) max = (*dat)[i]; return max; };

template<> inline void Summarizer::print() const { std::printf(“Summary for double data”); std::printf(“Mean : %.2f”, mean()); std::printf(“SD : %.2f”, sd()); std::printf(“Min : %.2f”, min()); std::printf(“Max : %.2f”, max()); };

template<> inline void Summarizer::print() const { std::printf(“Summary for int data”); std::printf(“Mean : %.2f”, mean()); std::printf(“SD : %.2f”, sd()); std::printf(“Min : %d”, min()); std::printf(“Max : %d”, max()); };

#endif

cat("```\n")

## Details: Declaration of the class

```cpp
template<typename T>
class Summarizer {
private:
    const std::vector<T>* dat = nullptr;
    double n;

public:
    // Constructors
    Summarizer(const std::vector<T> & dat_);

    // Calculators
    double mean() const;
    double sd() const;
    T min() const;
    T max() const;

    // Printer
    void print() const;
    
};

Details: The constructor

template<typename T>
inline Summarizer<T>::Summarizer(const std::vector<T> & dat_) {
    dat = &dat_;
    n = dat->size();
};
  • The implementation of the constructor is done outside of the function.

  • The inline keyword is used to tell the compiler to insert the code in the place where the function is called (more efficient).

  • Here, data is passed by reference and then the pointer is stored.

Details: The mean function

template<typename T>
inline double Summarizer<T>::mean() const {
    return std::accumulate(
        dat->begin(), dat->end(), 0.0
        ) / n;
};
  • The function is declared as const to tell the compiler that the function does not modify the object (the class itself).

  • The mean function uses the std::accumulate function.

  • Since dat is a pointer to a vector, we can access the members of dat via the -> operator (otherwise it would be using a . operator).

Running the example

#include "summary.hpp"

int main() {
    // Some data
    std::vector< double > dat = {1.0, 2.5, 4.4};
    std::vector< int > dat2 = {1, 2, 3, 4, 5};

    // Summarize the data
    Summarizer<double> s_double(dat);
    s_double.print();

    Summarizer<int> s_int(dat2);
    s_int.print();

    return 0;

}
g++ -std=c++14 summary.cpp -o summary
./summary
Summary for double data
Mean : 2.63
SD   : 1.70
Min  : 1.00
Max  : 4.40
Summary for int data
Mean : 3.00
SD   : 1.58
Min  : 1
Max  : 5