3. References, Pointers, and Lifetime
In Chapter 2, you learned how to pass a copy of a value to a function (pass by value). In this chapter, you will learn references and pointers, which let you use the original variable without making a copy, so that you can trace which variable is being read or written and how long that variable remains usable. In MISO, configuration and grids are received by reference and kept, and lightweight “Views” that hold the location of an array through a pointer are passed to computations.
If you only want to check what the symbols mean, you can also use the Symbol Quick Reference.
Contents
1. Copies and references
1.1 Copying a value and references
Distinguish between starting from the same value and using the same variable.
double a = 1.0;
double b = a; // b is a separate variable holding a copy of a
double &r = a; // r is another name (reference) for a
b = 2.0; // only b becomes 2.0
r = 3.0; // r is a itself, so a becomes 3.0
// a = 3.0, b = 2.0, r = 3.0
The r in double &r = a; is not a new double variable; it is another name given to a. Such an alias is called a reference, and in a declaration you put & after the type. double& r and double &r mean the same thing, and the MISO source consistently uses the double &r style (& next to the name).
A reference must be bound to its target when it is declared. You cannot create one without a target, as in double &r;. Also, you cannot rebind it to a different variable later. If you write r = b; in the state above, it only assigns the value of b to a.
1.2 Pass by reference
If you make a function parameter a reference, the function can use the caller’s variable itself.
void set_copy(double value) { // pass by value: value is a copy
value = 5.0;
}
void set_original(double &value) { // pass by reference: value is an alias of the caller's variable
value = 5.0;
}
double c = 1.0;
set_copy(c); // c stays 1.0
set_original(c); // c becomes 5.0
The caller writes set_copy(c) and set_original(c), which look the same. The difference lies in double value versus double &value on the side that defines the function. Receiving a parameter by reference in this way is called pass by reference.
In Fortran, arguments are normally treated as the caller’s variable itself, and assigning to one inside a subroutine changes the caller’s variable. A Fortran argument is closer to C++ pass by reference than to pass by value. Just as you distinguish intent(in), intent(out), and intent(inout) in Fortran, in C++ you express how an argument is received through its type (Section 1.5).
1.3 Output parameters
A function has only one return value, but reference parameters let you pass multiple results back to the caller. This is the same usage as a Fortran subroutine writing its results to intent(out) arguments.
void sum_and_difference(double a, double b, double &total, double &difference) {
total = a + b; // writes to the caller's total
difference = a - b; // writes to the caller's difference
}
double total = 0.0;
double difference = 0.0;
sum_and_difference(3.0, 1.0, total, difference);
// total is 4.0, difference is 2.0
a and b receive the values used in the computation by value, and total and difference receive, by reference, the places where the results are written. A parameter that receives a result is called an output parameter.
A real MISO example — the whole of symmetric_index in miso/include/miso/boundary_condition.hpp, which we saw with switch in Chapter 2.
__host__ __device__ inline void symmetric_index(int i, int i_total, int i_margin,
int &i_ghst, int &i_trgt,
Side side) {
switch (side) {
case Side::INNER:
i_ghst = i;
i_trgt = 2 * i_margin - i - 1;
break;
case Side::OUTER:
i_ghst = i_total - i_margin + i;
i_trgt = i_total - i_margin - i - 1;
break;
}
}
The leading __host__ __device__ is a marker that the function can be called from both CPU and GPU (Chapter 9), and in a CPU build it is replaced by nothing (the preprocessor, in Chapter 2). i, i_total, i_margin, and side are inputs passed by value, and int &i_ghst and int &i_trgt are output parameters. The index of the ghost cell and the index of the cell whose value is copied are written into the two int variables prepared by the caller.
#include <iostream>
#include <miso/core.hpp>
int main() {
const int i_total = 8; // number of grid points including ghost cells
const int i_margin = 2; // width of the ghost cells
for (int i = 0; i < i_margin; ++i) {
int i_ghst = -1;
int i_trgt = -1;
miso::boundary_condition::symmetric_index(i, i_total, i_margin, i_ghst,
i_trgt, miso::Side::OUTER);
std::cout << i << ": i_ghst = " << i_ghst << ", i_trgt = " << i_trgt << "\n";
}
}
0: i_ghst = 6, i_trgt = 5
1: i_ghst = 7, i_trgt = 4
You can build this with the CMake setup from Chapter 1 that links MISO::MISO (because miso/core.hpp includes the MPI and yaml-cpp headers). You can see that at the outer boundary, the values at positions 5 and 4, mirrored across the boundary, are copied into ghost cells 6 and 7.
1.4 const references for reading
To use the original variable without copying it while making sure the function does not modify it, add const and write const T & (T is a placeholder name that stands for a type).
double twice(const double &value) {
return 2.0 * value; // reading is allowed
// value = 5.0; // error: cannot modify through a const reference
}
const double &value is “a reference only for reading a double.” It corresponds to Fortran’s intent(in). What is forbidden is modification through this reference; it does not make the caller’s variable itself read-only. You can also pass a number directly to a const reference, as in twice(1.0) (Section 5). You cannot pass a number to a double & meant for modification.
The catch (const std::runtime_error &e) in Chapter 2 is also a way of receiving the caught exception by const reference.
A real MISO example — merge_yaml in miso/include/miso/config.hpp is a function that adds the missing items from the defaults defaults to the contents of the configuration file target (the declaration).
inline void merge_yaml(YAML::Node &target, const YAML::Node &defaults) {
YAML::Node is a yaml-cpp type that represents a YAML tree. target, the destination being added to, is a YAML::Node & (modified), and the default values defaults are a const YAML::Node & (read only). The declaration alone tells you which parameter changes.
Time in miso/include/miso/time.hpp receives the configuration as const Config &config and reads the end time and other values from it (this uses a mechanism called a class constructor, explained in Chapter 4).
Time(const Config &config)
1.5 Choosing how to receive parameters
What the function needs to do |
Declaration |
Closest in Fortran |
|---|---|---|
Use a value that is cheap to copy, such as a single number |
|
|
Read data that is expensive to copy |
|
|
Modify the caller’s variable |
|
|
If you only read a single int or double, pass by value is the basic choice. Copying one number is cheap, and using a reference does not necessarily make it faster. If you only read a type whose contents can be large, such as std::string, std::vector, or Config, use const T &. If you modify it, use T & even for a single number. This division is also recommended in C++ Core Guidelines F.16 and F.17.
You can also add const to a pass-by-value parameter, as in const T value. This is a marker that “this copy is not modified inside the function” and has no effect on the caller.
A real MISO example — Time::update in miso/include/miso/time.hpp receives the time step dt as const Real dt (a copy that is not modified).
void update(const Real dt) {
time += dt;
n_step++;
};
Model in demo/mhd2d_vortex/src/main.cpp receives the configuration as Config &config.
Model(Config &config) : ModelBase(config), eos(config), ic(eos), bc(), src() {}
It has no const because it passes the received config to the base class ModelBase (Chapter 5) and keeps it there as a modifiable reference (Section 5). The list after : is member initialization (Chapter 4). For now, it is enough to read it as “pass config along as it is, without copying.”
2. Pointers
2.1 Addresses and pointers
A variable is placed somewhere in memory. The value that represents that location is called an address, and a variable that holds an address is called a pointer.
double a = 1.0; // variable holding a number
double *p = &a; // pointer p holding the address of a; we say "p points to a"
Part |
Meaning |
|---|---|
|
The type “pointer to |
|
The name of the pointer variable |
|
An expression that takes the address of |
To use the value that a pointer points to, write *p in an expression (dereference).
double copied = *p; // reads the value of a, which p points to; copied is 1.0
*p = 2.0; // modifies a, which p points to; a becomes 2.0
a holds the number 1.0, and p holds “the location where that 1.0 is placed.” You do not need to memorize the numerical value of an address; just trace which variable it points to.
Fortran also has the pointer attribute, but a Fortran pointer simply refers to the target value when you write its name. A C++ pointer is an address when you write p and the target value when you write *p, and you must distinguish between the two.
2.2 * and & change meaning between declarations and expressions
Both & and * appear in references and pointers. The same symbol means different things in a declaration and in an expression.
double a = 1.0;
double &r = a; // & in a declaration: makes r a reference (alias) to a
double *p = &a; // * in a declaration: makes p a pointer. & in an expression: address of a
*p = 2.0; // * in an expression: assigns to what p points to (a). a and r are both 2.0
r = 3.0; // assigns to a. a and *p are both 3.0
Where it is written |
Example |
Meaning |
|---|---|---|
|
|
|
|
|
The address of |
|
|
|
|
|
The value that |
|
|
Multiplication, bitwise AND |
r is another name for a, so &r is also the address of a, and &a, &r, and p all represent the same location. If you read the double *p in a declaration as “a p such that writing *p gives a double,” you can see the connection with *p in an expression.
There is one more point to note in declarations. * and & attach to each name, not to the type.
double *p, q; // p is a pointer, q is just a double
double *x = nullptr, *y = nullptr; // x and y are both pointers
A real MISO example — GridView in miso/include/miso/grid.hpp holds the coordinate arrays through pointers (excerpt).
/// @brief coordinate in x/y/z direction
Real *x = nullptr, *y = nullptr, *z = nullptr;
All three are Real *, and * is written for each name. nullptr is explained in Section 2.4. What GridView points to is covered in Section 4.
The * in double *p and the * in *p are the same symbol because Ritchie, the designer of C, decided that “a declaration should resemble the form in which the variable is used in an expression.” double *p can be read as the declaration “*p is a double.” Source: D. M. Ritchie, The Development of the C Language (the “Embryonic C” section).
References are a feature added in C++. Stroustrup gives operator overloading (writing a matrix expression A + B without copying large data) as the main reason for introducing references. The same section also gives an example of using a const reference to pass a value to a Fortran function. Source: B. Stroustrup, A History of C++: 1979–1991, Section 3.3.4. However, we found no explanation by the designer in these sources of why & was chosen as the symbol for references. That the existing & was used because the feature is closely related to addresses is a guess.
Whether you attach the * of a declaration to the type, as in double* p, or to the name, as in double *p, is a matter of formatting style, and the meaning is the same. Stroustrup says he prefers int* p, which emphasizes the type, and recommends declaring one variable per line. Source: Stroustrup, C++ Style and Technique FAQ. MISO uses the double *p form through its clang-format configuration (.clang-format).
2.3 Re-pointing
Unlike a reference, a pointer can later be changed to point to a different variable.
double a = 1.0;
double b = 4.0;
double *p = &a; // p points to a
p = &b; // changes what p points to, to b. The values of a and b do not change
*p = 5.0; // p now points to b, so b becomes 5.0. a stays 1.0
p = &b; is an assignment that changes the address the pointer holds, and *p = 5.0; is an assignment that changes the value it points to. You can tell which one is being changed by the presence of *.
2.4 nullptr
nullptr (the null pointer) is a pointer value that means it points nowhere. It corresponds to the state after nullify(p) in Fortran, in which associated(p) is false.
double *q = nullptr; // points to nothing yet
if (q != nullptr) { // use it only when it points to something
*q = 1.0;
}
You must not read or write *q while it is nullptr. The result is undefined behavior (the language does not guarantee what happens); the program does not necessarily stop immediately, and the computation may continue with wrong values. A pointer declared in a function with just double *q; does not automatically become nullptr. If you cannot decide an initial value, initialize it with nullptr.
A real MISO example — Array3DView in miso/include/miso/array3d.hpp initializes the pointer to the data with nullptr (excerpt).
T *data_ = nullptr;
T is a template argument that represents the element type (such as Real) (Chapter 6). Here you can read it as Real *data_ = nullptr;.
2.5 Functions that take pointers
When a function receives a pointer, it can write a value through the address of the caller’s variable. This does the same job as a reference parameter, expressed in the C style.
void get_rank(int *rank) { // receives the address to write to
*rank = 3; // writes to what it points to
}
int rank = -1;
get_rank(&rank); // pass the address of rank; rank becomes 3
The differences from a reference parameter are that the caller explicitly gives the address as &rank, and that you can also pass nullptr. For a function newly written in C++, the basic choice is a reference parameter if the destination always exists.
A real MISO example — MPI was designed as C functions, so it returns results through pointer parameters. The following is an excerpt from mpi::Env::setup in miso/include/miso/env.hpp.
int is_mpi_initialized = false;
MPI_Initialized(&is_mpi_initialized);
// ...
MPI_Comm_rank(world::comm, &world::rank);
MPI_Comm_size(world::comm, &world::size);
MPI_Comm_rank writes its own rank number to the address passed as the second argument. &world::rank is the address of the variable rank in the namespace miso::mpi::world that we saw in Chapter 2. In Fortran’s call MPI_Comm_rank(comm, rank, ierr), the argument is modified as it is, but in C MPI you pass an address (MPI is covered in Chapter 8).
mpi::Env in the same file has the following two lines (these are class constructors, explained in Chapter 4).
Env() { setup(nullptr, nullptr); }
Env(int &argc, char **&argv) { setup(&argc, &argv); }
setup is declared as void setup(int *argc, char ***argv) and passes its arguments as they are to MPI_Init(argc, argv). When the object is created without arguments, nullptr expresses that “there are no command-line arguments”; when it receives argc and argv of main, it passes their addresses. char **&argv is “a reference to the variable argv of type char **.” char ** is covered in Section 4.
3. Position of const
A pointer involves two things, the pointer itself (where it points) and the value it points to, and const lets you choose which one is fixed. Using * as the dividing line, look at whether const is on the left or the right.
A
constto the left of*applies to the target.const double *pis “a pointer to adoublethat is only for reading,” and*p = 2.0;is not allowed.A
constto the right of*applies to the pointer itself.double *const pis “a pointer whose target cannot be changed,” andp = &b;is not allowed.
double a = 1.0;
double b = 4.0;
const double *p = &a; // only reads what it points to
// *p = 2.0; // error: assignment of read-only location '* p'
p = &b; // OK: p itself is not const
double *const q = &a; // fixes what it points to
*q = 2.0; // OK: a becomes 2.0
// q = &b; // error: assignment of read-only variable 'q'
Declaration |
|
|
|---|---|---|
|
Allowed |
Allowed |
|
Not allowed |
Allowed |
|
Allowed |
Not allowed |
|
Not allowed |
Not allowed |
const double *p and double const *p mean the same thing (in both, const is to the left of *). const double &r means “read only through this reference,” just like const double *p. A reference cannot be rebound in the first place, so there is nothing corresponding to a const on the right side. In every case, directly modifying the original variable a with a = 5.0; is not prohibited.
A real MISO example — Array1DView in miso/include/miso/array1d.hpp has two functions that return the beginning of the data (excerpt).
/// @brief Return pointer to the data.
__host__ __device__ T *data() noexcept { return data_; }
/// @brief Return const pointer to the data.
__host__ __device__ const T *data() const noexcept { return data_; }
The first returns a pointer T * that may be used for modification, and the second returns a read-only pointer const T *. There are two with the same name, and the second is chosen in read-only contexts. The trailing const that decides which one is chosen is covered in Chapter 4, and noexcept (a marker that the function does not throw exceptions) is also covered in Chapter 4. For now, it is enough to read the difference between the return types T * and const T * as the difference between “can be modified” and “read only.”
4. Arrays and pointers
A built-in C++ array is a sequence of values of the same type placed contiguously in memory.
double raw[4] = {0.0, 1.0, 2.0, 3.0}; // 4 doubles; indices 0 to 3
double *p = raw; // the array name becomes the address of the first element raw[0]
std::cout << *(p + 2) << " " << p[2] << "\n"; // both are raw[2], so 2 2
When you use the array name
rawin an expression, in most situations it becomes the address of the first element (&raw[0]).p + 2is “the address 2 elements pastp” (pointer arithmetic). It counts in numbers of elements, not bytes.p[i]means the same as*(p + i). Indices start from 0 because the first element is “0 places ahead.”
Therefore, when you pass an array to a function, what is actually passed is only the address of the first element, and the number of elements is not conveyed. Pass the number of elements as a separate parameter.
double sum(const double *values, int n) { // address of the first element and the number of elements
double total = 0.0;
for (int i = 0; i < n; ++i) {
total += values[i];
}
return total;
}
sum(raw, 4); // 0 + 1 + 2 + 3 = 6
sum(raw + 1, 2); // 2 elements from raw[1]: 1 + 2 = 3
If you pass raw + 1, you can pass the part that starts in the middle of the array. This is similar to writing call sub(a(2), 2) in Fortran to pass from the middle. Because it is a const double *, sum cannot modify the array.
It is also common to represent a range by a pair of “the address of the first element” and “the address one past the last element.” The range from p to p + n is the same as 0 <= i < n.
std::vector also stores its elements contiguously, and v.data() gives the address of the first one.
#include <algorithm> // std::fill
std::vector<double> v(5, 1.0); // 5 copies of 1.0
std::fill(v.data(), v.data() + v.size(), 0.0); // fills [begin, begin + 5) with 0.0
A real MISO example 1 — util::clear_array in miso/include/miso/utility.hpp fills an array with 0 (the template and using T = ... lines belong to Chapter 6, so read T as Real).
// @brief Clear array (zero-fill)
template <typename VectorLike> inline void clear_array(VectorLike &arr) {
using T = std::decay_t<decltype(*arr.data())>;
std::fill(arr.data(), arr.data() + arr.size(), T(0));
}
It fills the range from arr.data() to arr.data() + arr.size() with T(0) (the value 0 as the element type). It has the same form as the std::vector example above.
A real MISO example 2 — Array3DView in miso/include/miso/array3d.hpp represents a three-dimensional array with a pointer data_ to the beginning of one-dimensionally arranged data and the size in each direction, shape_[3]. The part that returns an element when you write a(i0, i1, i2) is as follows (the mechanism of operator() is in Chapter 4; the assert line is omitted).
return data_[(i0 * shape_[1] + i1) * shape_[2] + i2];
From the three indices, it computes “how many elements from the beginning” and reads the one-dimensional data with data_[...]. When the last index i2 increases by 1, the element is 1 place away, and when i1 increases by 1, it is shape_[2] places away. Elements are contiguous along the last index, which is the opposite of Fortran (contiguous along the first index). How this layout affects computation speed is covered in Chapter 7.
A real MISO example 3 — Grid::view in miso/include/miso/grid.hpp passes the address x.data() of the first element of the std::vector coordinates x (Chapter 2) to GridView (excerpt).
return GridView<Real>(i_total, j_total, k_total, is, js, ks, i_margin,
j_margin, k_margin, min_dxyz, x.data(), y.data(),
z.data(), dx.data(), dy.data(), dz.data(), dxi.data(),
dyi.data(), dzi.data());
The x of GridView is the Real *x we saw in Section 2.2. The grid.x[i] that we wrote in the initial condition in Chapter 2 therefore indexed this pointer to read elements of the std::vector held by Grid. GridView does not hold a copy of the coordinates; it is a lightweight type that holds only their location. In MISO, a type that holds only a location without owning the data is called a View (Chapter 4).
char **argv of main — char * is a pointer to the first character and represents a C string (a sequence of characters with a terminating character '\0' at the end). char **argv is “a pointer to char *,” that is, the beginning of an array of strings. argv[i] is the string of the i-th command-line argument. In parse_config_filepath in Chapter 2, std::string_view arg(argv[i]); read the i-th argument.
5. Variable lifetime and scope
A variable declared inside a function can be used from the place of declaration to the end of the enclosing { ... }. This range is called its scope. The object behind an ordinary variable disappears when execution leaves that block. The period during which a variable exists is called its lifetime.
double *p = nullptr; // p is in the outer block
{
double local = 1.0; // local exists only inside this block
p = &local; // stores the address of local in p
} // the lifetime of local ends. p remains
// do not read or write *p here
Even if a pointer or reference remains, the lifetime of its target is not extended. A pointer or reference to a variable whose lifetime has ended is called a dangling pointer or dangling reference. Reading or writing through it is undefined behavior. Because it may happen to read a plausible value, it is a hard-to-notice mistake.
In Fortran as well, the values of a subroutine’s local variables (those without the save attribute) are not guaranteed after the subroutine returns. In C++, the same thing happens in a block inside a for or if.
Returning a reference or pointer to a variable inside a function is the same mistake.
double &bad() {
double local = 1.0;
return local; // local is destroyed when the function returns
}
warning: reference to local variable 'local' returned [-Wreturn-local-addr]
g++ gives a warning with -Wall, but it cannot detect every case. When you bring a value back, return it by value, as in double good() (C++ Core Guidelines F.43).
If you pass a number or a computed result to a const reference parameter, a temporary object holding that value is created. The 1.0 in twice(1.0) exists until the end of that statement (up to the ;). Do not save that reference somewhere inside the function to use it later.
Variables declared in the same block disappear in the reverse order of declaration. Variables declared later disappear first, so it is safe for a later variable to hold a reference to an earlier variable.
A real MISO example 1 — ModelBase in miso/include/miso/mhd_model_base.hpp keeps the configuration Config as a reference member (excerpt).
Config &config;
A variable of a type that has a reference member must not outlive the referent (here, the Config variable). In main in demo/mhd2d_vortex/src/main.cpp, config is declared first and model later (Chapter 2).
Config config(config_path.value_or("./config.yaml"));
// Run simulation
Model model(config);
model.run();
At the end of main, model, declared later, disappears first, and then config disappears. While model is using it, config always exists.
A real MISO example 2 — Integrator in miso/include/miso/mhd_integrator_host.hpp also holds the grid as a reference member (excerpt).
/// @brief Spatial grid
Grid<Real, backend::Host> &grid;
Integrator uses the grid at every time integration step but does not hold a copy of it. The grid itself is grid in ModelBase, and Integrator is placed so that it does not outlive it. <Real, backend::Host> are template arguments (Chapter 6).
A real MISO example 3 — GridView in Section 4 only holds pointers to the elements of the std::vector in Grid. Therefore, a GridView can be used only while the original Grid exists. Also, when the number of elements of a std::vector is increased (with push_back and so on), it may be reallocated to a different place, after which the old data() address can no longer be used. MISO does not change the number of coordinate elements after creating the grid, so this problem does not occur. The relationship between an array that owns data and a View that points to it is covered in detail in Chapter 4.
6. new, delete, and smart pointers
Sometimes you want to allocate memory of the required size at run time, free from the lifetime of a block. The basic C++ tools are new and delete. They correspond to Fortran’s allocate and deallocate.
int n = 4;
double *raw = new double[n]; // allocates n doubles and puts the address of the first one in raw
for (int i = 0; i < n; ++i) {
raw[i] = i;
}
delete[] raw; // release it yourself (delete[] for arrays)
raw = nullptr;
Memory allocated with new remains until you delete it. Writing these directly makes the following mistakes likely.
Forgetting to free (a memory leak). If you
returnmidway or an exception is thrown, execution never reaches thedeleteline.Using memory after it has been freed, or freeing the same memory twice.
Using
delete(without[]) on memory allocated withnew[].
A Fortran allocatable array is freed automatically when it leaves its scope, but C++ pointers have no such mechanism. For that reason, in modern C++ the basic approach is not to write new and delete directly but to use types that free memory automatically (C++ Core Guidelines R.11).
std::vector: If you just want a sequence of elements, use this first (Chapter 2).std::unique_ptr(<memory>): a pointer that is the sole owner of the allocated memory. When the lifetime of theunique_ptrvariable ends, it automatically callsdelete. It cannot be copied.std::shared_ptr(<memory>): a pointer shared by multiple owners. It counts the owners and callsdeletewhen the last owner disappears.
#include <memory>
int n = 4;
std::unique_ptr<double[]> u = std::make_unique<double[]>(n); // n elements, initialized to 0
u[1] = 1.5;
std::shared_ptr<double> s1 = std::make_shared<double>(2.0);
std::shared_ptr<double> s2 = s1; // shares the same double
*s2 = 3.0; // *s1 is also 3.0
// s1.use_count() is 2 (two owners)
// u, s1, s2 are released automatically at the end of the block. No delete needed
The <> in <double[]> and similar forms holds template arguments (Chapter 6). make_unique and make_shared are functions that perform the allocation and the creation of the pointer in one step.
A real MISO example — MISO’s arrays Array1D to Array4D use new[] and delete[] internally. The following is an excerpt from Array1D<T, backend::Host> in miso/include/miso/array1d.hpp.
Array1D(int nx0) : size_(nx0) {
assert(size_ > 0);
data_ = new T[size_];
}
~Array1D() { delete[] data_; }
The first is the part executed when the array is created (the constructor), and it allocates nx0 elements. The second is the part executed automatically when the lifetime of the array variable ends (the destructor), and it frees the allocated memory. By enclosing the pair of new and delete inside the array type, the user does not have to write delete, and, as with std::vector, the memory is freed when the lifetime ends. This mechanism (RAII) is covered in Chapter 4. In Array1D<T, backend::CUDA> for the GPU, GPU memory is allocated and freed at the same places (Chapter 9).
The MISO library does not use std::unique_ptr. std::shared_ptr is used in just one place, miso/include/miso/mhd_resistivity.hpp, which holds the work area for reduction on the GPU. Memory allocation is gathered in the array types, and everywhere else, data is handed over through references and Views.
7. Exercises
Exercise 1 After running the following code in order, write the values of a, b, r, and *p. Also explain which lines changed a.
double a = 1.0;
double b = a;
double &r = a;
double *p = &a;
b = 2.0;
r = 3.0;
*p = 4.0;
p = &b;
*p = 5.0;
Exercise 2 In symmetric_index in miso/include/miso/boundary_condition.hpp, explain what happens to the output of the check program in Section 1.3 if you change int &i_ghst to int i_ghst (you do not need to actually make the change).
Exercise 3 Explain the following three function declarations in terms of what each can do through its parameters (read, modify, or change the target).
void f1(const double *values, int n);
void f2(double *const values, int n);
void f3(const YAML::Node &defaults);
Exercise 4 Explain the problem with the following function and fix it correctly.
int *make_counts(int n) {
int counts[100];
for (int i = 0; i < n; ++i) {
counts[i] = 0;
}
return counts;
}
Exercise 1 In the end, a = 4.0, b = 5.0, r = 4.0, and *p = 5.0. The b created by b = a; is an independent copy. r = 3.0; makes a 3.0, and *p = 4.0; makes it 4.0 (because p points to a). p = &b; makes p point to b, and *p = 5.0; changes b. r is still another name for a, so it is 4.0.
Exercise 2 i_ghst is passed by value, so assignments inside the function reach only the copy. The caller’s i_ghst stays at its initial value -1, and i_ghst in the output is -1 on both lines. i_trgt is still a reference, so 5 and 4 are printed.
Exercise 3 f1 only reads what values points to and cannot modify it (const is to the left of *). Inside the function, it can change values itself to another address (this does not affect the caller). f2 can modify what it points to but cannot change values itself to another address (const is to the right of *). f3 only reads defaults without copying it. This is the same as the second parameter of merge_yaml.
Exercise 4 counts is an array inside the function, so its lifetime ends when the function returns. The returned address becomes a dangling pointer (Section 5). Also, if n exceeds 100, it writes outside the array. Fix it to return a std::vector by value.
#include <vector>
std::vector<int> make_counts(int n) {
std::vector<int> counts(n, 0); // n elements, all 0
return counts;
}
Even if you return a std::vector by value, the elements are normally not copied one by one (it is optimized so that only the contents are moved; moves are covered in Chapter 4).
8. Summary of symbols
Notation |
Kind |
Meaning |
|---|---|---|
|
Declaration |
|
|
Declaration |
A reference that only reads through |
|
Declaration |
|
|
Expression |
The value that |
|
Expression |
|
|
Declaration |
Only reads the target. |
|
Declaration |
|
|
Value |
A pointer that points nowhere |
|
Parameter |
Pass by value (copy) |
|
Parameter |
Pass by reference. Can modify the caller’s variable |
|
Parameter |
Only reads, without copying |
A list that also includes other symbols (::, <>, ->, and so on) is in the Symbol Quick Reference. For details of the rules for references and pointers, see the C++17 draft standard N4659: references, pointers, and initialization of references.