Data Types

Numeric Datatypes

TDycore relies on the PETSC <https://www.mcs.anl.gov/petsc/> library, and adopts many of its conventions. In particular, the floating point precision of TDycore is determined by its underlying PETSc library. The following numeric data types are supported by PETSc and used by TDycore:

  • PetscScalar: A floating point number at the supported level of precision (single or double), that is real-valued or complex-valued, depending on how PETSc is configured

  • PetscReal: A real-valued floating point number at the supported level of precision

  • PetscInt: An integer whose size is determined by PETSc.

Use these types for numeric calculations within TDycore.

Floating point comparisons

In general, avoid comparing two floating point numbers for equality. Such comparisons depend on the representation of the supported precision and the magnitudes of the quantities under comparison. Instead, use one of the following functions::

// Returns true iff |x - y| < epsilon, where epsilon is a global tolerance
// that determines the equivalence of floating point numbers in TDycore.
PetscBool RealsEqual(PetscReal x, PetscReal y);

// Returns true iff |x - y| < tolerance.
PetscBool RealsNearlyEqual(PetscReal x, PetscReal y, PetscReal tolerance);

Pointers

When declaring a pointer variable, place the asterisk next to the variable name, not the type name::

PetscInt *int_ptr;

This allows you to declare several variables on one line consistently::

PetscInt *i_ptr, *j_ptr;

Structs / PODs

In the TDycore library, a “struct” is a C struct containing data that can be freely exposed, with no invariants and no restrictions on values. Structs have no behavior and internal state to manage. They are defined in header files so that their data members are visible and accessible. Examples of structs in the TDycore library are the MaterialProp and CharacteristicCurve types, which represent sets of material properties and parameterized curves (a saturation function, for example), respectively.

Since a struct is a simple container without behavior (a “Plain Old Datatype”, or “POD”), no associated functions are needed. However, such functions can be provided if they make the struct/POD more convenient to use.

Classes

A “class” in TDycore is a struct whose definition is private–not exposed within the public interface defined by the set of header files intended for use by external developers. This means that a developer cannot directly manipulate the fields within a “class”. Instead, one manipulates a class using its interface–a set of functions associated with that class.

Define class bodies in source files only, unless their internal structure is intended to be explicitly exposed to developers. “Typedef” your class type so the struct keyword can be omitted from its type.

The struct and functions defining a class are governed by a few simple conventions described below. PETSc uses similar conventions.

Class Type (Struct)

The struct representing the class type has the same name as the class itself. The type of the class itself is a pointer to its underlying struct. The underlying struct is declared with a _p suffix. Then the class is declared as a typedef to a pointer to the struct type. For example, if you want to declare a “washing machine” class, first declare its underlying struct (without defining its body)::

typedef struct TDyWashingMachine_p;

in an appropriate header file. Then define the class itself::

typedef TDyWashingMachine_p* TDyWashingMachine;

Class Constructor(s)

Typically, a class has a single constructor function named after the class, with Create following the class name. A constructor takes a number of arguments for initializing the class, plus a final argument that stores a pointer to a newly-allocated instance of the class. For example, consider the following constructor for our TDyWashingMachine class::

PetscErrorCode TDyWashingMachineCreate(PetscInt numCents, TDyWashingMachine *wm);

This constructor creates a WashingMachine instance that costs the given number of cents to wash a load of laundry. The wm argument stores the new instance. The constructor returns an integer-valued error code described in the section on functions.

Sometimes it’s convenient to provide more than one constructor, or a constructor that converts another datatype to a given instance of a class. In these cases, name each constructor so that it briefly conveys its purpose. For example, a constructor that creates a deep copy of an existing washing machine might be declared:

PetscErrorCode TDyWashingMachineClone(TDyWashingMachine *other, TDyWashingMachine **wm);

A constructor function takes any arguments it needs to completely initialize an variable of that class type, and returns a pointer to such an initialized variable. We refer to these variables as objects.

Class Destructor

A destructor function frees the resources allocated to a class by its constructor. Define a single destructor function for each class. The destructor function is named after the class with a Destroy suffix, accepts a pointer to the instance of the class to be destroyed, and returns an error code indicating whether an error was encountered. For example::

PetscErrorCode TDyWashingMachineDestroy(TDyWashingMachine* wm);

Class Functions / Methods

Functions associated with a class are sometimes referred to as methods (some object-oriented programming languages make a bigger distinction between these concepts). The name of a class method begins with the name of the associated class, followed by a descriptive name for the method itself. For example::

// Washes a load of laundry, changing its state from DIRTY to CLEAN.
PetscError TDyWashingMachineWash(TDyWashingMachine* wm, Laundry* load);

A method can perform a task involving the instance and other data provided as arguments, as shown above. In this case, it returns a PetscErrorCode indicating success or failure. A method can also provide access to data within the instance of the class, returning that data::

// Returns the cost (in cents) of washing a load.
PetscInt TDyWashingMachineCost(TDyWashingMachine* wm);

If you’re familiar with contemporary object-oriented programming languages like C++ and Java, you can define methods in very similar ways (as long as you don’t wander too far into inheritance and other “polymorphic” techniques). If it’s practical, lead the list of parameters with input values, and place output parameters at the end.

Polymorphism in C

Polymorphism is the idea that a variable’s instance or type determines its behavior. TDycore adopts the same approach to polymorphism that PETSc uses: behavior is associated with the instance of a class, and not its type. This approach to polymorphism is sometimes called “prototype polymorphism,” and is used in some other programming languages such as Lua and Objective C.

A polymorphic class in TDycore has an “abstract” interface with a virtual table that dispatches calls to a set of predetermined methods that implements its behavior.

In total, a polymorphic class is defined by:

  1. A class type struct possessing a context pointer for an instance

  2. A methods “virtual table” struct consisting of a set of function pointers matching the interface for the class

  3. A constructor function that creates a descendant object using a context pointer, a methods table, and any other data needed

  4. Any other functions needed to implement a destructor and/or methods for the polymorphic class

These elements aren’t all exposed via the TDycore public API. Instead, TDycore exposes a “registration function” for each polymorphic type that allows end users to create named instances of types. For an example of how this is done, take a look at PETSc’s KSPRegister <https://www.mcs.anl.gov/petsc/petsc-current/docs/manualpages/KSP/KSPRegister.html> function, which can be used to implement custom linear solvers for use with PETSc’s matrix and vector types.

A virtue of this approach is that a single instance (represented by a context pointer) can assume many different roles as a subtype of several base classes, using several different virtual tables. In a sense, this ability resembles that of the interface idiom in the Java and C# programming languages, avoiding the difficulties of multiple inheritance one encounters in C++.

Header Files

In general, there should be a header file for each significant type that possesses behaviors in TDycore. Header file names are all lowercase. In some cases, a single function (unrelated to a type) may occupy a header file, and that header file would be named after the function. In others, a header file may contain a set of related functions, and its name should concisely reflect the purpose of those functions.

Self-Contained Headers

Header files are self-contained and have a .h suffix. A “self-contained” header file can be included in a translation unit without regard for rules relating to the order of its inclusion, or for other headers that are “understood” to be included when it is used.

Briefly, a TDycore header file * is located in the include/tdycore subdirectory of the TDycore repo * requires header guards * should include all the files that it needs * should not require any particular symbols to be defined

Header File Location

To make it easier to deploy TDycore as part of a larger application, we place most header files in a tdycore subdirectory within the include directory of the repository. There is a high-level header file called tdycore.h within the include directory that includes all the basic headers within this tdycore/ subdirectory.

This means that headers and source files that reference specific TDycore headers must include the tdycore/ directory as part of the header file’s path. For example, if you want to use TDycore’s I/O subsystem in a source file, you would place the following near the top of the file::

#include <tdycore/tdyio.h>

Alternatively, you can rely on the high-level TDycore header to bring in the I/O subsystem::

#include <tdycore.h>

Because TDycore stores its headers in a separate directory from source files, we use angle brackets to include these headers, and not quotes.

Header Guards

A header file uses #define guards to prevent multiple inclusion. The format of the guard is <HEADER_BASE_NAME>_H, e.g.:

#ifndef TDYCORE_H
#define TDYCORE_H

“C++” guards that use the extern "C" specification are not necessary for C++ interoperability, since TDycore has a high-level header safe for inclusion in C++ programs.

Including Headers within TDycore Source Code

Any header files included in a header or source file should be included in the following order:

  1. The header file corresponding to the source file (if applicable)

  2. TDycore library headers

  3. Third-party library headers

  4. System-level headers

Including files in this order makes it obvious when a TDycore header can’t be included without prerequisites.

Public and Private Headers, Structs and Classes

There are three types of header files in the TDycore library.

  1. Public headers: these headers form the public application programming interface (API) for TDycore, and live at the top level of the include/ directory of the TDycore source tree. All functions and types contained in these headers may be called by software that uses TDycore.

  2. Private headers: these headers contain implementation details, and are not part of the public API for TDycore. As such, they are not supported for usage by external software, and their contents may change without warning.

  3. Fortran headers: these headers expose an interface for using the TDycore library within Fortran programs. They form the public Fortran API for TDycore.

Recall that a TDycore struct is a container for data that has no associated behavior and may be freely manipulated by developers. Structs are declared and defined within public header files. TODO: example?

In contrast, a class is a data structure with behaviors and invariants. It is implemented by a pointer to a struct whose fields are hidden from developers. Its behaviors are implemented by a set of functions that form its interface. A class struct is declared in a public header file, but its body іs defined in a private header file. Meanwhile, the functions that make up the interface for a class are declared in public header files and defined in source files. TODO: example?

Functions

Any function that is part of TDycore’s API is declared within a public header file and implemented in a source file. More than one function may “live” in the same source file. Prepend each public function’s declaration with PETSC_EXTERN to make it available to external callers.

A function may be “inlined” using the PETSC_STATIC_INLINE macro. Functions with no arguments are declared with void in their argument list, in accordance with the C standard.

Functions that implement functionality internal to TDycore may be declared in a private header file, or may be declared static and implemented within a single source file, if they are used only within that file.

Global variables

In general, avoid global variables in header files, apart from constants (which are preferred to macros, since they can be checked by the compiler). Mutable global variables should be restricted to translation units in which they are manipulated, and should be declared as static. If you must expose a global resource, design an appropriate interface so that it can be properly managed.

Other Symbols

Use inlined functions instead of macros where possible. Similarly, use constants instead of macros where possible.

Scoping

Internal Functions

A function that is used only within a single translation unit should be declared with the PETSC_INTERN macro. This prevents its name from appearing in the list of exported symbols for the TDycore library.

Local Variables

Declare a local variable as close as possible to where it is used, and not at the beginning of a function body. Declaring variables where they are used makes it easier to identify issues involving that variable.

Initialize a variable when you declare it wherever practical.

Scoping Operators

If a function has a large number of localized variables that perform work, curly braces can be used to create a local scope containing these variables. This eases the process of debugging functions by eliminating these variables from portions of the function that don’t use them.

Functions

Functions not associated with classes follow very similar guidelines to methods: input arguments come before output arguments. A function that performs an operation instead of returning a value should return a PetscErrorCode that indicates whether the operation succeeded or failed.

Function declarations in header files should not have named parameters. This makes them somewhat easier to maintain.

Length of a Function Body

There is no formal limit to the length of a TDycore function implementation. If breaking up a function into separate functions is practical, feel free to do so. However, creating lots of ancillary structure just to break up a long function can be counterproductive. Use your judgement.

A function may be poorly designed if it is difficult to break up. On the other hand, if the function performs a complicated task with lots of tightly-coupled steps, attempting to break it up may make it even more confusing.

At the end of the day, arguments about the optimal length of a function are aesthetic. These arguments often exert strange and unnatural pressures on code development. At worst, they encourage people to write code with few comments, lots of side effects, and/or excessive numbers of tightly-coupled “sub-functions.” Your mileage may vary.

Memory Management

For simplicity, TDycore uses PETSc’s memory allocation functions:

  • PetscMalloc <https://www.mcs.anl.gov/petsc/petsc-current/docs/manualpages/Sys/PetscMalloc.html>

  • PetscMalloc1 <>https://www.mcs.anl.gov/petsc/petsc-current/docs/manualpages/Sys/PetscMalloc1.html>

  • PetscNew <https://www.mcs.anl.gov/petsc/petsc-current/docs/manualpages/Sys/PetscNew.html>

  • PetscFree <>https://www.mcs.anl.gov/petsc/petsc-current/docs/manualpages/Sys/PetscFree.html>

Prefer these to the standard C malloc and free functions. This gives PETSc more information about how much memory is used, and how it is used.

Naming

Types

Names of structs, classes, and enumerated types follow the “camel case”, consisting of one or more words with no delimiters, each word beginning with a capital letter followed by lower-case letters. Each type has a TDy prefix to indicate that they belong to the TDycore library. Abbreviations are allowed if their meaning is reasonably clear. For example: TDyMesh, TDyRegion.

Functions

Function and “method” names also use “camel case” with a TDy prefix, and should clearly indicate their purpose, with abbreviations allowed when their meaning is clear. Methods that implement behaviors for classes should begin with the name of the class, as discussed above.

Variables and Fields

Variables (local or global, including fields in structs and classes) follow the “snake-case” convention, in which names consist of lower-case words separated by underscores. Exceptions can be made if it makes code clearer. For example, capital letters and/or abbreviations may help a variable representing a quantity resemble a mathematical symbol whose role is clear from the context in which it is used. Use your judgement. Examples of good variable names are mat_prop, mesh, model, precond, integ, and xc.

Constants, Enums, Macros

Constants, fields within enumerated types, and preprocessor macros should use all capital letters with words separated by underscores. If these appear in header files, they should have descriptive names that are unique within the library.

Comments and Code Markup

Use C++ style comments (//), which have been supported in C since the C99 standard. C-style comments (/* */) may be used sparingly when the C++ style is less convenient.

To formally document a type or a function in a public header file, use Doxygen’s markup:

http://www.doxygen.org/

Because we omit function parameters in declarations, we document functions in their definitions in source files and not in header files.

Above each type or function definition, describe the entity briefly and clearly. Build the Doxygen documentation to get an idea of what documentation typically looks like. We use Doxygen’s /// delimiters for code comments, and @ for Doxygen-specific commands.

A type should be documented with a description of its purpose and usage, just above its declaration. Structs should have one-line descriptions above each of their fields.

Each function or class method should have a description (1-2 sentences) above its definition in the proper source file. In addition, use the following markup to annotate the function/method signature:

  • For each parameter (argument) for the function, an entry like the following:::

    @param [INTENT] PARAM_NAME A description of the parameter

    Here, INTENT is in, out, or inout.

  • If the return value needs an explanation, use::

    @returns A description of the return value

Typically, you don’t need any documentation markup for types and functions that aren’t part of the public API. Commenting your implementation code is always helpful, of course.

An Example

To see how Doxygen markup looks in practice, let’s return to the TDyWashingMachine we used in our discussion of classes.

Here’s a documented constructor function, with Doxygen markup placed in the source file in which the function is implemented::

/// Creates a washing machine object that costs the given number of cents
/// to wash a load of clothes.
/// @param [in] numCents The number of cents required to wash a load
/// @param [out] wm A pointer that stores the newly-created instance of
///                 the washing machine.
/// @returns 0 on success, or a non-zero error code on failure.
PetscErrorCode TDyWashingMachineCreate(PetscInt numCents, TDyWashingMachine *wm) {
  ...
}

Here’s the destructor function::

/// Destroys an existing washing machine object, releaseing all resources
/// allocated to it.
/// @param [inout] wm A pointer to the existing washing machine object.
/// @returns 0 on success, or a non-zero error code on failure.
PetscErrorCode WashingMachineDestroy(WashingMachine* wm) {
  ...
}

Arguably, you can skip documenting the parameter, since all destructors accept exactly one argument: the object to be destroyed. However, it’s easy enough to include for consistency.

Finally, here are examples of markup for the methods we discussed earlier::

/// Washes a load of laundry, changing its state from DIRTY to CLEAN.
/// @param [in] wm A pointer to an existing washing machine object.
/// @param [inout] load A load of laundry whose state will be changed from
///                     DIRTY to CLEAN. If load is already CLEAN, this
///                     function has no effect.
/// @returns 0 on success, or a non-zero error code on failure.
PetscError TDyWashingMachineWash(TDyWashingMachine* wm, Laundry* load) {
  ...
}

/// Returns the cost (in cents) of washing a load.
/// @param [in] wm A pointer to an existing washing machine object.
PetscInt TDyWashingMachineCost(TDyWashingMachine* wm) {
  ...
}

In “accessor functions”, where the description explains clearly what is returned, the @returns markup can be omitted.

Formatting

The following formatting rules are non-negotiable for source code in TDycore:

  • Use 2 spaces per indentation level.

  • No tabs are allowed in source files–use only spaces.

The following guidelines are offered for readably-formatted code:

  • If a function declaration doesn’t fit neatly on a line, break the line after an argument and align the following argument with its first. As long as the declaration and definition are clearly readable, it’s fine.

  • Place curly braces that open a new scope at the end of the line for which the scope is declared, not on their own line. Closing curly braces go on a line by themselves, at the level of indentation outside of their scope.

  • If a line is excessively long (in other words, if it doesn’t fit on a single screen on a luxuriously large monitor), consider breaking it up.

  • C preprocessor directives are not indented at all.

  • For functions with several parameters, consider linebreaks after each parameter, and consider aligning the parameters to improve readability.