Skip to content

Fun with Cpp Syntax

Ben Prather edited this page Sep 3, 2024 · 8 revisions

If you're familiar with C, but new to C++, KHARMA uses a couple of C++-specific features which can be hard to pick up just by reading. This page gives very brief overviews of a few of these features, and more importantly names them so that you can find information about them as necessary to use/unbreak them.

Organization

Namespaces

Namespaces prefix related functions with the same word. That's it. They're a way of making sure that I can name functions generic things like get_state and they won't conflict with the completely different get_state function in another package, or in another library. This was done already in C just by naming all your functions e.g. gsl_X, just, the practice now has some official language support.

Namespaces are separated from their members in the same way as classes, using the ::.

The most common namespace is std, which is often just imported wholesale to avoid typing std:: before everything. KHARMA additionally imports the parthenon namespace, mostly for convenience, but also because namespace collisions with Parthenon may indicate collisions in functionality: if for some horrifying reason there was a parthenon::t global and a KHARMA::t global, we would likely want to know this before the inevitable timestepping issues showed up.

Templates

Templates are blocks of code which can be reinterpreted and recompiled as necessary, replacing a type or value (often called T) with a value chosen by the caller. That is, I can write a class like, say, std::vector, which handles lists. Lists of what? Anything! When I want to use a vector, I specify what that type should be when the code actually gets compiled, by instantiating e.g. a std::vector<Real>.

During the compile process[^1], the compiler caches the source code of a templated function. Whenever it is used (called, for functions, or instantiated, for classes) with a new type, the compiler finds and replaces the template variable T with the caller's argument, denoted in angle braces <>. Templates can be instantiated with template types -- take the function call Update::FluxDivergence<MeshData<Real>> in harm_driver.cpp. This call indicates that we are calling the templated function Update::FluxDivergence over a MeshData object (as opposed to a MeshBlockData object containing just one block), and further that the MeshData contains variables of the Real type (which resolves to double in KHARMA).

Templates similar to function macros, without incurring many of the downsides of longer macros (unpredictable behavior, lack of type checking, etc). While templates are generally used with arguments representing types, they can also have regular arguments (integers, etc) to make sure the compiler knows what all possible arguments will be at compile time. One of these less common templates is used for the GetFlux and reconstruct functions in KHARMA, to make sure there is no overhead of choosing the function dispatch at runtime.


[^1]: I have no idea if this is correct in its specifics, but some idea of the process was really helpful to me in reasoning about templates and the inevitable hard-to-parse errors that crop up when I make a mistake with them.

Variables

References

Sometimes variables are declared with e.g. int& k = j rather than int i = j. Both variables can be used normally, without any pesky * operators, or even pointers at all, really. But when I update k, I also update j -- I've created another name for the same variable underneath. This can avoid extra copies, passing references as arguments can help the compiler avoid creating local copies -- that is, where in a function f(int i, int j, int k), might add i, j, and k to the stack, declaring f(const int& i, const int& j, const int& k) much more explicitly states that these variables do not need local copies, and will not be modified inside the function, allowing the compiler the full range of possible options.

Coming from C, the general rule is that references act like pointers, with a compiler-added * operator on each usage. A quick intro to references with further info.

The auto keyword

Are you tired of compiler errors where you're trying to declare and assign a variable, but they're slightly different types, and rather than help you out the compiler just yells at you? Do you wonder why all that type inference code is even included in the compiler, if the only use is to tell you when you got it wrong?

Good news! You can now declare variables to have "whatever type I am assigning to them right now," using auto. This is most useful when dealing with lambda functions, which have truly nasty compound types that would be a pain to write out. But it's also a handy way of saying "the specific type name of this variable matters less than the particular thing it contains." Do I care whether my array of densities is a ParArray2D or a ParArrayND or even a VariablePack object? No, I care that it has densities and I can use rho(k,j,i) syntax to access them. Let the compiler handle the details!

The const keyword

Declaring a variable const is an indication that it will not change. This allows for some extra optimizations, and catches some possible bugs, so KHARMA uses it liberally where it is, or even might be, applicable. Note that when a reference or pointer is const, it means that the reference will not change, but the value may -- that is, values must be declared const separately from references.

When listed after member function declarations (for example, in coordinate_systems.hpp), const indicates that the function will not modify any member variables of the object. This helps when optimizing functions which will end up on the device side and be called often, like coordinate system transformations.

Initializers

You'll see some variables, usually arrays, initialized with {0} to zero their entire extent. IndexRange objects, which are just two-element structs, are sometimes initialized with their values as IndexRange{start, end}

Clone this wiki locally