Coding Style Guidelines

This document covers style guidelines for the various programming languages used in the Carbonite codebase.

C/C++ Coding Conventions

This covers the basic coding conventions and guidelines for all C/C++ code that is submitted to this repository.

  • It’s expected that you will not love every convention that we’ve adopted.

  • These conventions establish a modern and hybrid C/C++14 style.

  • Please keep in mind that it’s impossible to make everybody happy all the time.

  • Instead appreciate the consistency that these guidelines will bring to our code and thus improve the readability for others.

  • Coding guidelines that can be enforced by clang-format will be applied to the code.

This project heavily embraces a plugin architecture. Please consult Architectural Overview for more information.


The project should maintain a well structured layout where source code, tools, samples and any other folders needed are separated, well organized and maintained.

The convention has been adopted to group all generated files into top-level folders that are prefixed with an underscore, this makes them stand out from the source controlled folders and files while also allowing them to be easily cleaned out from local storage (Ex. rm -r _*).

This is the layout of the Carbonite project repository:




Visual Studio Code configuration files.


Build target outputs (generated).


Compiler scripts, IDE projects (generated).


External dependency configuration files.


Carbonite documentation.


Public includes for consumers of Carbonite SDK.


Small tools or boot-strappers for the project.


All source code for project.


Script bindings for Carbonite SDK.


Examples of using Carbonite.


Carbonite framework implementation.


Source code for tests of Carbonite.


Source code for tools built with Carbonite.


Carbonite plugin implementations.


The carb.assets.plugin implementation


The implementation of interface for Direct3D12


The implementation of interface for Vulkan


Configuration for running clang format on the source code.


Maintains editor and IDE style conformance Ex. Tabs/Spaces.


Configuration for additional coding style conformance.


Governs repository attributes for git repository.


Governs which files to ignore in the git repository.


Build script to build debug and release targets on Windows.

Build script to build debug and release targets on Linux.

These coding guidelines.


Run this to format code on Windows before submitting to repository.

Run this to format code on Linux before submitting to repository.


Run this to generate visual studio solution files on Windows into _compiler folder.


Script for configuration of all build output targets using premake.

Setup run once installation script of Linux platform dependencies.

The summary of any project information you should read first.

One important rule captured in the above folder structure is that public headers are stored under include/carb folder but implementation files and private headers are stored under source folders.


There are four rules to be followed when writing include statements correctly for Carbonite:

  1. Do not include Windows.h in header files as it is monolithic and pollutes the global environment for Windows. Instead, a much slimmer CarbWindows.h exists to declare only what is needed by Carbonite. If additional Windows constructs are desired, add them to CarbWindows.h. There are instructions in that file for how to handle typedefs, enums, structs and functions. Windows.h should still be included in compilation units (cpp and c files); CarbWindows.h exists solely to provide a minimal list of Windows declarations for header files.

Example from a file in include/carb/extras:

#include "../Defines.h"
#    include "../CarbWindows.h"
  1. Public headers (located under include/carb) referencing each other always use path-relative include format:

#include "../Defines.h"
#include "../container/LocklessQueue.h"
#include "IAudioGroup.h"
  1. Includes of files that are not local to Carbonite (or are pulled in via package) use the search path format. Carbonite source files (under source/) may also use search-path format for Carbonite public headers (under include/carb/):

#include <carb/graphics/Graphics.h> // via packman package

#include <doctest/doctest.h>
  1. All other includes local to Carbonite use the path-relative include format:

#include "MyHeader.h"
#include "../ParentHeader.h"

In the example above MyHeader.h is next to the source file and ParentHeader.h is one level above. It is important to note that these relative includes are not allowed to cross package boundaries. If parts are shipped as separate packages the includes must use the angle bracket search path format in item 1 when referring to headers from other packages.

We do also have rules about ordering of includes but all of these are enforced by format_code.{bat|sh} so there is no need to memorize them. They are captured here for completeness:

  1. Matching header include for cpp file is first, if it exists - in a separate group of one file. This is to ensure self-sufficiency.

  2. carb/Defines.h is it’s own group of one file to ensure that it is included before other includes.

  3. Other local includes are in the third group, alphabetically sorted.

  4. Search path includes to Carbonite are in the fourth group (#include <carb/*>), alphabetically sorted.

  5. Other 3rd party includes are in the fifth group (#include <*/*>), alphabetically sorted.

  6. System includes are in the sixth and final group, alphabetically sorted.

Here is an example from AllocationGroup.cpp (doesn’t have the fifth group)

#include "AllocationGroup.h"

#include <carb/Defines.h>

#include "StackEntry.h"

#include <carb/logging/Log.h>

#    include <signal.h>

Two things are worth noting about the automatic grouping and reordering that we do with format_code script. If you need to associate a comment with an include put the comment on the same line as the include statement - otherwise clang-format will not move the chunk of code. Like this:

#include <stdlib.h> // this is needed for size_t on Linux

Secondly, if include order is important for some files just put // clang-format off and // clang-format on around those lines.


  • Header files should have the extension .h, since this is least surprising.

  • Source files should have the extension .cpp, since this is least surprising.

    • .cc is typically used for UNIX only and not recommended.

  • Header files must include the preprocessor directive to only include a header file once.

#pragma once
  • Source files should include the associated header in the first line of code after the commented license banner.

  • All files must end in blank line.

  • Header and source files should be named with PascalCase according to their type names and placed in their appropriate namespaced folder paths, which are in lowercase. A file that doesn’t represent a type name should nevertheless start with uppercase and be written in PascalCase, Ex. carb/Defines.h.









  • This allows for inclusion of headers that match code casing while creating a unique include path:

#include <carb/assets/IAssets.h>
#include <carb/audio/IAudioPlayback.h>
#include <carb/settings/ISettings.h>
  • In an effort to reduce difficulty downstream, all public header files (i.e. those under the include directory) must not use any identifier named min or max. This is an effort to coexist with #include <Windows.h> where NOMINMAX has not been specified:

    • Instead, include/carb/Defines.h has global symbols carb_min() and carb_max() that may be used in similar fashion to std::min and std::max.

    • For rare locations where it is necessary to use min and max (i.e. to use std::numeric_limits<>::max() for instance), please use the following construct:

      #pragma push_macro("max") // or "min"
      #undef max // or min
      /* use the max or min symbol as you normally would */
      #pragma pop_macro("max") // or "min"


Before we dive into usage of namespaces it’s important to establish what namespaces were originally intended for. They were added to prevent name collisions. Instead of each group prefixing all their names with a unique identifier they could now scope their work within a unique namespace. The benefit of this was that implementers could write their implementations within the namespace and did therefore not have to prefix that code with the namespace. However, when adding this feature a few other features were also added and that is where things took a turn for the worse. Outside parties can alias the namespace, i.e. give it a different name when using it. This causes confusion because now a namespace is known by multiple names. Outside parties can hoist the namespace, effectively removing the protection. Hoisting can also be used within a user created namespace to introduce another structure and names for 3rd party namespaces, for an even higher level of confusion. Finally, the namespaces were designed to support nesting of namespaces. It didn’t take long for programmers to run away with this feature for organization.

Nested namespaces stem from a desire to hierarchically organize a library but this is at best a convenience for the implementer and a nuisance for the consumer. Why should our consumers have to learn about our internal organization hierarchy? It should be noted here that the aliasing of namespaces and hoisting of namespaces are often coping mechanisms for consumers trapped in deeply nested namespaces. So, essentially the C++ committee created both the disease and the palliative care. What consumers really need is a namespace to protect their code from clashing with code in external libraries that they have no control over. Notice the word a in there. Consumers don’t need nested levels of namespaces in these libraries - one is quite enough for this purpose. This also means that a namespace should preferably map to a team or project, since such governing bodies can easily resolve naming conflicts within their namespace when they arise.

With the above in mind we have developed the following rules:

  • The C++ namespace should be project and/or team based and easily associated with the project.

    • Ex. The Carbonite project namespace is carb:: and is managed by the Carbonite team

    • This avoids collisions with other external and internal NVIDIA project namespaces.

    • We do not use a top level nvidia:: namespace because there is no central governance for this namespace, additionally this would lead to a level of nesting that benefits no one.

namespace carb
  • Namespaces are all lowercase.

    • This distinguishes them from classes which is important because the usage is sometimes similar.

    • This encourages short namespace names, preferably a single word; reduces chances of users hoisting them.

    • Demands less attention when reading, which is precisely what we want. We want people to use them for protection but not hamper code readability.

  • Exposed namespaces are no more than two levels deep.

    • One level deep is sufficient to avoid collisions since by definition the top level namespace is always managed by a governing body (team or project)

    • A second level is permitted for organization; we accept that in larger systems one level of organization is justifiable (in addition to the top level name-clash preventing namespace). Related plugin interfaces and type headers are often grouped together in a namespace.

    • Other NVIDIA projects can make plugins and manage namespace and naming within. These rules don’t really apply because we don’t have governance for such projects. However, we recommend that these rules be followed. For a single plugin a top level namespace will typically suffice. For a collection of plugins a single top level namespace may still suffice, but breaking it down into two levels is permitted by these guidelines.

  • We don’t add indentation for code inside namespaces.

    • This conserves maximum space for indentation inside code.

namespace carb
namespace audio
struct IAudioPlayback
  • We don’t add comments for documenting closing of structs or definitions, but it’s OK for namespaces because they often span many pages and there is no indentation to help:

}; // end of IAudioPlayback struct <- don't
} // audio namespace <- ok
} // carb namespace <- ok

Name Prefixing and Casing

The following table outlines the naming prefixing and casing used:


Prefixing / Casing

class, struct, enum class and typedef




enum class values




private/protected functions


exported C plugin functions


public member variables


private/protected member variables


private/protected static member variables


global - static variable at file or project scope


local variables


When a name includes an abbreviation or acronym that is commonly written entirely in uppercase, you must still follow the casing rules laid out above. For instance:

void* gpuBuffer; // not GPUBuffer
struct HtmlPage; // not HTMLPage
struct UiElement; // not UIElement
using namespace carb::io; // namespaces are always lowercase

Naming - Guidelines

  • All names must be written in US English.

std::string fileName; // NOT: dateiName
uint32_t color; // NOT: colour
  • The following names cannot be used according to the C++ standard:

    • names that are already keywords;

    • names with a double underscore anywhere are reserved;

    • names that begin with an underscore followed by an uppercase letter are reserved;

    • names that begin with an underscore are reserved in the global namespace.

  • Method names must always begin with a verb.

    • This avoids confusion about what a method actually does.

myObject.applyForce(x, y, z);
  • The terms get/set or is/set (bool) should be used where an attribute is accessed directly.

    • This indicates there is no significant computation overhead and only access.

employee.setName("Jensen Huang");
  • Use stateful names for all boolean variables. (Ex bool enabled, bool m_initialized, bool g_cached) and leave questions for methods (Ex. isXxxx() and hasXxxx())

bool isEnabled() const;
void setEnabled(bool enabled);

void doSomething()
    bool initialized = m_coolSystem.isInitialized();
  • Please consult the antonym list if naming symmetric functions.

  • Avoid redundancy in naming methods and functions.

    • The name of the object is implicit, and must be avoided in method names.

line.getLength(); // NOT: line.getLineLength();
  • Function names must indicate when a method does significant work.

float waveHeight = wave.computeHeight(); // NOT: wave.getHeight();
  • Avoid public method, arguments and member names that are likely to have been defined in the preprocessor.

  • When in doubt, use another name or prefix it.

size_t malloc; // BAD
size_t bufferMalloc; // GOOD
int min, max; // BAD
int boundsMin, boundsMax; // GOOD
  • Avoid conjunctions and sentences in names as much as possible.

  • Use Count at the end of a name for the number of items.

size_t numberOfShaders; // BAD
size_t shaderCount; // GOOD
VkBool32 skipIfDataIsCached; // BAD
VkBool32 skipCachedData; // GOOD

Internal code

For public header files, a detail namespace should be used to declare implementation as private and subject to change, as well as signal to external users that the functions, types, etc. in the detail namespace should not be called.

Within a translation unit (.cpp file), use an anonymous namespace to prevent external linkage or naming conflicts within a module:

namespace // anonymous

struct OnlyForMe



In general, prefer anonymous namespaces over static.

Deprecation and Retirement

As part of the goal to minimize major version changes, interface functions may be deprecated and retired through the Deprecation and Retirement Guidelines section of the Architectural Overview.

Shader naming

HLSL shaders must have the following naming patterns to properly work with our compiler and script:

  • HLSL shader naming:

    • if it contains multiple entry points or stages: [shader name].hlsl

    • if it contains a single entry point and stage: [shader name].[stage].hlsl

  • Compiled shader naming: [shader name].[entry point name].[stage].dxil/dxbc/spv[.h]

  • Do not add extra dots to the names, or they will be ignored. You may use underscore instead.

basic_raytracing.hlsl // Input: DXIL library with multiple entry points
basic_raytracing.chs.closesthit.dxil // Output: entry point: chs, stage: closesthit shader

color.pixel.hlsl // Input: a pixel shader
color.main.pixel.dxbc // Output: entrypoint: main, stage: pixel shader

Rules for class

  • Classes that should not be inherited from should be declared as final.

  • Each access modifier appears no more than once in a class, in the order: public, protected, private.

  • All public member variables live at the start of the class. They have no prefix. If they are accessed in a member function that access must be prefixed with this-> for improved readability and reduced head-scratching.

  • All private member variables live at the end of the class. They are prefixed with m_. They should be accessed directly in member functions, adding this-> to access members in the same class is unnecessary.

  • Use protected member variables judiciously. They are prefixed with m_. They should be accessed directly in member functions, adding this-> to access them is unnecessary.

  • Constructors and destructor are first methods in a class after public member variables unless private scoped in which case they are first private methods.

  • The implementations in cpp should appears in the order which they are declared in the class.

  • Avoid inline implementations unless trivial and needed for optimization.

  • Use the override specifier on all overridden virtual methods. Also, every member function should have at most one of these specifiers: virtual, override, or final.

  • Do not override pure-virtual method with another pure-virtual method.

  • Here is a typical class layout

#pragma once

namespace carb
namespace ui

 * Defines a user interface widget.
class Widget



    const char* getName() const;

    void setName(const char* name);

    bool isEnabled() const;

    bool setEnabled(bool enabled);

    char* m_name;
    bool m_enabled;


Rules for struct

  • We make a clear distinction between structs and classes.

  • We do not permit any member functions on structs. Those we make classes.

  • If you must initialize a member of the struct then use C++14 static initializers for this, but don’t do this for basic types like a Float3 struct because default construction/initialization is not free.

  • No additional scoping is needed on struct variables.

  • Not everything needs to be a class object with logic.

    • Sometimes it’s better to separate the data type from the functionality and structs are a great vehicle for this.

    • For instance, vector math types follow this convention.

    • Allows keeping vector math functionality internalized rather than imposing it on users.

  • Here is a typical struct with plain-old-data (pod):

struct Float3
    float x;
    float y;
    float z;

// check this out (structs are awesome):
Float3 pointA = {0};
Float3 pointB = {1, 0, 0};

Rules for function

  • When declaring a function that accepts a pointer to a memory area and a counter or size for the area we should place them in a fixed order: the address first, followed by the counter. Additionally, size_t must be used as the type for the counter.

void readData(const char* buffer, size_t bufferSize);
void setNames(const char* names, size_t nameCount);
void updateTag(const char* tag, size_t tagLength);

Rules for enum class and bit flags

  • We use enum class over enum to support namespaced values that do not collide.

  • Keep their names as simple and short-and-sweet as possible.

  • If you have an enum class as a subclass, then it should be declared inside the class directly before the constructor and destructor.

  • Here is a typical enum class definition:

class Camera

    enum class Projection


  • The values are accessed like this:

    • EnumName::eSomeValue

  • Note that any sequential or non-sequential enumeration is acceptable - the only rule is that the type should never be able to hold the value of more than one enumeration literal at any time. An example of a type that violates this rule is a bit mask. Those should not be represented by an enum. Instead use constant integers (constexpr) and group them by a prefix. Also, in a cpp file you want them to also be static. Below we show an example of a bit mask and bit flags in Carbonite:

namespace carb
namespace graphics
constexpr uint32_t kColorMaskRed    = 0x00000001; // static constexpr in .cpp
constexpr uint32_t kColorMaskGreen  = 0x00000002;
constexpr uint32_t kColorMaskBlue   = 0x00000004;
constexpr uint32_t kColorMaskAlpha  = 0x00000008;

namespace input
 * Type used as an identifier for all subscriptions.
typedef uint32_t SubscriptionId;

 * Defines possible press states.
typedef uint32_t ButtonFlags;
constexpr uint32_t kButtonFlagNone = 0;
constexpr uint32_t kButtonFlagTransitionUp = 1;
constexpr uint32_t kButtonFlagStateUp = (1 << 1);
constexpr uint32_t kButtonFlagTransitionDown = (1 << 2);
constexpr uint32_t kButtonFlagStateDown = (1 << 3);

Rules for Pre-processors and Macros

  • It’s recommended to place preprocessor definitions in the source files instead of makefiles/compiler/project files.

  • Try to reduce the use of #define (e.g. for constants and small macro functions), and prefer constexpr values or functions when possible.

  • Definitions in the public global namespace must be prefixed with the namespace in uppercase:

#define CARB_API
  • Indent macros that are embedded within one another.

    #ifdef __cplusplus
        #define CARB_EXPORT extern "C"
        #define CARB_EXPORT
  • All #defines should be set to 0, 1 or some other value. Accessing an undefined macro in Carbonite is an error.

  • All checks for Carbonite macros should use #if and not #ifdef or #if defined()

  • Macros that are defined for all of Carbonite should be placed in carb/Defines.h

  • Transient macros that are only needed inside of a header file should be #undefed at the end of the header file.

  • CARB_POSIX is set to _POSIX_VERSION on platforms that are mostly POSIX conformant, such as Linux and MacOS. CARB_POSIX is set to 0 on other platforms. Functions used in these blocks should be verified to actually follow the POSIX standard, rather than being common but non-standard (e.g. ptrace). Non-standard calls inside CARB_POSIX blocks should be wrapped in a nested platform check, such as CARB_PLATFORM_LINUX.

  • When adding #if pre-processor blocks to support multiple platforms, the block must end with an #else clause containing the CARB_UNSUPPORTED_PLATFORM() macro. An exception to this is when the #else block uses entirely C++ standard code; this sometimes happens in the case of platform-specific optimizations. This logic also applied to #if directives nested in an #if block for a standard, such as CARB_POSIX where the #else block follows that platform. In other words, you may not make assumptions about what features future platforms may have, aside from what’s in the C++ standard; all platform-specific code must have the associated platform specifically stated.

  • Macros that do not have universal appeal (i.e. are only intended to be used within a single header file) shall be prefixed with CARBLOCAL_ and #undef’d at the end of the file.

    // code
    // code
    // code
    // Windows-specific code
    // C++ standard code

Porting to new platforms

This is the process to port Carbonite to new platforms with minimal disruption to development work. This is the process being used to port Carbonite to Mac OS.

  • The initial commit to master is the minimal code to get the new platform to build. Code paths that cannot be shared with another platform will have a crash macro to mark where they are (The Mac OS port has CARB_MACOS_UNIMPLEMENTED() for this). Each crash macro should have a comment with an associated ticket for fixing it. After this point, CI builds should be enabled.

  • Code added after the initial commit should still build for the new platform, but new code can use the crash macro if needed.

  • CI testing is enabled on a subset of the tests once the framework is able to run on the new platform.

  • Once there are no remaining crash macros and all tests are enabled on CI, the new platform will be considered fully supported.

Commenting - Header Files

  • Avoid spelling and grammatical errors.

  • Assume customers will read comments. Err on the side of caution

    • Cautionary tale: to ‘nuke’ poor implementation code is a fairly idiomatic usage for US coders. It can be highly offensive elsewhere.

  • Each source file should start with a comment banner for license

    • This should be strictly the first thing in the file.

  • Header comments use doxygen format. We are not too sticky on doxygen formatting policy.

  • All public functions and variables must be documented.

  • The level of detail for the comment is based on the complexity for the API.

  • Most important is that comments are simple and have clarity on how to use the API.

  • @brief can be dropped and automatic assumed on first line of code. Easier to read too.

  • @details is dropped and automatic assumed proceeding the brief line.

  • @param and @return are followed with a space after summary brief or details.

 * Tests whether this bounding box intersects the specified bounding box.
 * You would add any specific details that may be needed here. This is
 * only necessary if there is complexity to the user of the function.
 * @param box The bounding box to test intersection with.
 * @return true if the specified bounding box intersects this bounding box;
 *     false otherwise.
bool intersects(const BoundingBox& box) const;
  • Overridden functions can simply refer to the base class comments.

class Bar: public Foo

     * @see Foo::render
    void render(float elapsedTime) override;

Commenting - Source Files

  • Clean simple code is the best form of commenting.

  • Do not add comments above function definitions in .cpp if they are already in header.

  • Comment necessary non-obvious implementation details not the API.

  • Only use // line comments on the line above the code you plan to comment.

  • Avoid /* */ block comments inside implementation code (.cpp). This prevents others from easily doing their own block comments when testing, debugging, etc.

  • Avoid explicitly referring to identifiers in comments, since that’s an easy way to make your comment outdated when an identifier is renamed.


  • The following must be included at the start of every header and source file:

// Copyright (c) 2020 NVIDIA CORPORATION.  All rights reserved.
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.

Formatting Code

  • You should set your Editor/IDE to follow the formatting guidelines.

  • This repository uses .editorconfig - take advantage of it

  • Keep all code less than 120 characters per line.

  • We use a .clang-format file with clang-format (via repo_format) to keep our code auto-formatted.

    • In some rare cases where code is manually formatted in a pleasing fashion, auto-formatting can be suspended with a comment block:

      // clang-format off
      ... Manually formatted code
      // clang-format on


  • Insert 4 spaces for each tab. We’ve gone back and forth on this but ultimately GitLab ruined our affair with tabs since it relies on default browser behavior for displaying tabs. Most browsers, including Chrome, are set to display each tab as 8 spaces. This made the code out of alignment when viewed in GitLab, where we perform our code reviews. That was the straw that broke the camel’s back.

  • The repository includes .editorconfig which automatically configures this setting for VisualStudio and many other popular editors. In most cases you won’t have to do a thing to comply with this rule.

Line Spacing

  • One line of space between function declarations in source and header.

  • One line after each class scope section in header.

  • Function call spacing:

    • No space before bracket.

    • No space just inside brackets.

    • One space after each comma separating parameters.

      serializer->writeFloat("range", range, kLightRange);
  • Conditional statement spacing:

    • One space after conditional keywords.

    • No space just inside the brackets.

    • One space separating commas, colons and condition comparison operators.

      if ("carb::scenerendering::Light::Type") == 0)
          switch (static_cast<Light::Type>(value))
              case Light::Type::eDirectional:
                  return "eDirectional";
  • Don’t align blocks of variables or trailing comments to match spacing causing unnecessary code changes when new variables are introduced:

    class Foo
        bool         m_very;      // Formatting
        float3       m_annoying;  // generates
        ray          m_nooNoo;    // spurious
        uint32_t     m_dirtyBits; // diffs.
  • Align indentation space for parameters when wrapping lines to match the initial bracket:

    Matrix::Matrix(float m11, float m12, float m13, float m14,
                  float m21, float m22, float m23, float m24,
                  float m31, float m32, float m33, float m34,
                  float m41, float m42, float m43, float m44)
    return sqrt((point.x - * (point.x - +
                (point.y - * (point.y - +
                (point.z - * (point.z -;
  • Use a line of space within .cpp implementation functions to help organize blocks of code.

    // Lookup device surface extensions
    // Create the platform surface connection


  • Indent next line after all braces { }.

  • Move code after braces { } to the next line.

  • Always indent the next line of any condition statement line.

    if (box.isEmpty())
    for (size_t i = 0; i < count; ++i)
        if (distance(sphere, points[i]) > sphere.radius)
            return false;
  • Never leave conditional code statements on same line as condition test:

    if (box.isEmpty()) return;

C++14 and Beyond Recommendations

Carbonite supports a minimum of C++14, therefore public include files should not use any features in later standards.

Carbonite includes some implementations of C++17 and later features that will build on C++14. These are located in directories that name the standard, such as include/carb/cpp17.

Pointers and Smart Pointers

  • Use raw C/C++ pointers in the public interface (Plugin ABI).

  • In other cases prefer to use std::unique_ptr or std::shared_ptr to signal ownership, rather than using raw pointers.

  • Use std::shared_ptr only when sharing is required.

  • Any delete or delete[] call appearing in the code is a red flag and needs a good reason.


Consider using std::unique_ptr to apply RAII principles to malloc()/free() style APIs, though keep in mind that the deleter will only be called if the pointer is non-nullptr.

For example, the following could be used to apply RAII to dlopen()/dlclose():

struct FreePosixLib
    void operator()(void* p) noexcept

using UniquePosixLib = std::unique_ptr<void, FreePosixLib>;

void sample()
    auto scoped = UniquePosixLib{ dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD) };
    void* lib = scoped.get();

For a one-line variant of this, you can use the following, but the sizeof this unique_ptr is 2 pointers in this case, so the above should be preferred when it’s reasonable.

std::unique_ptr<void, int(*)(void*)> scoped(dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD), dlclose)

Another alternative is CARB_SCOPE_EXIT(). When exiting from a scope, this executes a lambda with local variables captured by reference. The above example could be written as:

void* lib = dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD);
    if (lib)


  • Casting between numeric types (integer types, float, etc.) or pointer-to/from-numeric (size_t(ptr)) may use C-style or functional-style casts (i.e. ptrdiff_t(val)) for brevity. One may still use static_cast if desired.

  • Except as mentioned above, avoid using C-style casts wherever possible. Note that const_cast can also be used to add/remove the volatile qualifier.

  • For non-numeric types, prefer explicit C++ named casts (static_cast, const_cast, reinterpret_cast) over C-style cast or functional cast. That will allow compiler to catch some errors.

  • Using dynamic_cast requires RTTI (Run-Time Type Information), is slow, and happens at runtime. Avoid using dynamic_cast.

  • Use the narrowest cast possible. Only use reinterpret_cast if it is unavoidable. Note that static_cast can be used for void*:

    void* userData = ...;
    MyClass* c = static_cast<MyClass*>(userData);


  • You are free to use the STL containers but you can never allow them to cross the ABI boundary. That is, you cannot create them inside one plugin and have another plugin take over the object and be responsible for freeing it via the default C++ means. Instead you must hide the STL object within an opaque data structure and expose create/destroy functions. If you violate this rule you are forced to link the C++ runtime dynamically and the ABI breaks down. See Architecture documentation and ABI Compatibility for more details.

Characters and Strings

  • All strings internally and in interfaces are of the same type: 8-bit char. This type should always be expected to hold a UTF-8 encoded string. This means that the first 7-bits map directly to ASCII and above that we have escaped multi-byte sequences. Please read Unicode to learn how to interact with OS and third-part APIs that cannot consume UTF8 directly. If you need to enter a text string in code that contains characters outside 7-bit ASCII then you must also read Unicode.

  • For ABI-safe strings, you can use omni::string; a string class similar to std::string.

  • You are free to use std::string inside modules but we cannot expose STL string types in public interfaces (violation of Plugin ABI). Instead use (const) char pointers. This does require some thought on lifetime management. Usually the character array can be associated with an object and the lifetime is directly tied to that object.

  • Even though you can use STL strings and functionality inside your implementation please consider first if what you want to do is easily achievable with the C runtime character functions. These are considerably faster and often lead to fewer lines of code. Additionally the STL string functions can raise exceptions on non-terminal errors and Carbonite plugins are built without exception support so it will most likely just crash.


  • Avoid the use of auto where it will make code more difficult to read for developers who do not have an in-depth knowledge of the codebase. Reading someone else’s code is harder than writing your own code, so code should be optimized for readability.

  • auto should be used for generic code, such as templates and macros, where the type will differ based on invocation.

  • auto may optionally be used for overly verbose types that have a standard usage, such as iterators.

  • auto may be optionally used for types where the definition makes the type obvious, such as auto a = std::unique_ptr(new (std::nothrow) Q) or auto&& lambda = [](Spline *s) -> Spline* { return s->reticulate(); }

  • auto may optionally be used for trailing return types, such as auto MyClass::MyFunction() -> MyEnum.

  • To avoid typing out types with overly verbose template arguments, it is preferable to define a new type with the using keyword rather than using auto. For types with a very broad scope, it is generally beneficial for readability to give a type a name that reflects its usage.

  • Avoid having auto variables initialized from methods of other auto variables, since this makes the code much harder to follow.

  • If you find yourself using tools to resolve the type of an auto variable, that variable should not be declared as auto.

  • Be careful about accidental copy-by-value when you meant copy-by-reference.

  • Understand the difference between auto and auto&.


  • Lambdas are acceptable where they make sense.

  • Focus use around anonymity.

  • Avoid over use, but especially for std algorithms (Ex. std::sort, etc.) they are fine.

  • For large lambdas, avoid using capture-all [=] and [&], and prefer explicit capture (by reference or by value, as needed)

  • For asynchronous lambdas, such as passed to ITasking functions, or std::thread or std::async make sure to capture local variables by value instead of by reference or pointer as they will have gone out of scope.

Range-based loops

  • They’re great, use them.

  • They don’t necessarily have to be combined with auto.

  • They are often more readable.

  • For complex objects, make sure to use a reference (&) or forwarding reference (&&) if possible to avoid copying (especially when combined with auto).

  • Use accurate variable naming, as this is more important than choosing between using auto or the type name:

    // BAD: Suggests `dev` might be of type Device even though it's a device ID. `auto` without reference means that a
    // a large object could be copied.
    for (auto dev : devices)
    // GOOD: More obvious that the iterator is a device index.
    for (int dev : devices)
    // BETTER: Even more obvious that the iterator is a device index.
    for (int devId : devices)
    // BEST: Blatantly obvious that the iterator and container are device indexes.
    for (int devId : deviceIndexes)
    // or
    for (auto&& devId : deviceIndexes)

Integer types


  • Use nullptr for any pointer types instead of 0 or NULL.


  • Avoid using friend unless absolutely needed to restrict access to inter-class interop only.

  • It easily leads to difficult-to-untangle inter-dependencies that are hard to maintain.

use of anonymous namespaces

  • Prefer anonymous namespaces to static free functions in .cpp files (static should be omitted).

templated functions

  • internal-linkage is implied for non-specialized templated functions functions, and for member functions defined inside a class declaration. You can additionally declare them inline to give the compiler a hint for inlining.

  • neither internal-linkage nor inline is implied for fully specialized templated functions, and thus those follow the rules of non-templated functions (see below)


  • Declare non-interface non-member functions as static in .cpp files (or even better, include them in anonymous namespaces). templated free functions (specialized or not) in .cpp files also follow this rule.

  • Declare non-interface non-member functions as static inline in .cpp files (or inline in anonymous namespaces) if you want to give the compiler a hint for inlining.

  • Avoid static non-member functions in includes, as they will cause code to appear multiple times in different translation units.


  • Declare non-interface non-member functions as inline in include files. Fully-specialized templeted free functions also need to be specified inline (as neither inline nor internal-linkage is implied).

  • Avoid non-static inline non-member functions in .cpp files, as they can hide potential bugs (different function with same signature might get silently merged at link time).


  • Use static_assert liberally as it is a compile-time check and can be used to check assumptions at compile time. Failing the check will cause a compile error. Providing an expression that cannot be evaluated at compile time will also produce a compile error. It can be used within global, namespace and block scopes. It can be used within class declarations and function bodies.

  • static_assert should be used to future-proof code.

  • static_assert can be used to purposefully break code that must be maintained when assumptions change (an example of this would be to break code dependent on enum values when that enum changes).

  • static_assert can also be used to verify that alignment and sizeof(type) matches assumptions.

  • static_assert can be used with C++ traits (i.e. std::is_standard_layout, etc.) to notify future engineers of broken assumptions.

Constant Strings

Suggested way of declaring string constants:

// in a .h file
constexpr char mystring[] = "constant string";

// in a .cpp file
static constexpr char mystring[] = "constant string";

class A {
    // inside a class:
    static const char* const mystring = "constant string";
    // ^^^ do not use static constexpr members of which an address is required within a class before C++17, otherwise
    // link errors will occur.


NOTE Prior to C++17, the use of static constexpr as a member within a struct/class may cause link problems if the address is taken of the member, unless the definition of the member is contained within a translation unit. This is not possible for header-only classes. Therefore, avoid using static constexpr members when the address is required of the member (i.e. passed via pointer or reference). Avoid using static constexpr string or character array members. With C++17, it is possible to declare static members as inline, and inline is implied for static constexpr members. However, Carbonite supports a minimum of C++14.

Higher-level Concepts

Internal and Experimental Code

Functions, types, etc. that are inside of a detail/details namespace, or contain detail[s] or internal (in any capitalization) as part of their name should not be called by code that is not packaged with and built at the same time. These functions, types, etc. should be considered private and may change at any time, therefore their existence should not be relied upon.

Similarly, any functions, types, etc. marked as experimental should be considered as such: the ABI may not be entirely stable and subject to change in the future.

Carbonite uses detail namespace (not details) to contain private/internal code. The internal name may also be used for function and type names.


Writing thread-safe code is very difficult. Introducing asynchronicity and threads means that code will execute non-deterministically and have potentially exponential variations.

  • Use std::atomic types sparingly and with caution! These atomic types can ensure low-level synchronization, but also lead to a false sense of thread safety. Race conditions are still quite possible with std::atomic. Consider higher- level synchronization primitives instead.

  • Avoid explicitly specifying std::memory_order on uses of std::atomic functions except as described below. The default memory order is also the safest: std::memory_order_seq_cst (sequentially consistent). Specifying an incorrect memory order especially on weakly-ordered machines such as ARM can lead to unexpected and extremely-difficult-to-track-down errors.

    • The std::memory_order_seq_cst memory order always can be specified explicitly to indicate where sequential consistency is required.

    • For performance intensive code, at least one but ideally two Senior or Principal engineers may sign off on more weakly-ordered uses of specified memory orders.

    • All explicit uses of memory order should be commented.

  • volatile is not a synchronization primitive! It makes no guarantees about atomicity, visibility or ordering. Do not use volatile as a synchronization primitive! Much more control and all of the necessary guarantees are given by std::atomic.

  • Avoid global and function-local static variables which may be modified by several threads.

  • Avoid busy-waiting–spinning while waiting for a condition. This includes loops that call std::this_thread::yield() or sleeping for a brief period of time. Properly architected synchronization code will block in the operating system while waiting for a condition to be met.

  • Many containers and library functions are not thread-safe. Be sure to check the documentation and assume that everything is not thread-safe unless explicitly stated.

  • Use the right synchronization primitive for the job:

    • std::call_once executes a callable exactly once, even if called concurrently from several threads. All threads wait until the callable completes execution.

    • So-called “magic” static variables (function-local static initialization) is guaranteed to be thread-safe with C++11. That is, they execute in the same manner as std::call_once ensuring that construction happens exactly once and all threads wait until the construction is finished.

      • However, keep in mind that only the static initialization is guaranteed to be thread-safe, but remember that initialization can be by a function return value, in which case the function is called in a thread-safe manner.

    • A Mutex is one of the most common synchronization primitives for mutual-exclusion and can be used to protect memory reads and writes in critical sections of code. See carb::thread::mutex and carb::thread::recursive_mutex. Since only one thread may have a mutex locked, all other threads must stop and wait in order to gain exclusive access.

    • A Shared Mutex (sometimes called a read/write mutex) is similar to a Mutex but can be accessed in either of two modes: shared (read) mode, or exclusive (write) mode. A thread which has exclusive access causes all other threads to wait. A thread which has shared access allows other threads to also obtain shared access but causes any threads seeking exclusive access to wait. See carb::thread::shared_mutex and carb::thread::recursive_shared_mutex.

    • A Condition Variable can be used to signal threads: threads wait until a condition is true, at which point they are signaled. Condition Variables work in concert with a Mutex. See std::condition_variable or std::condition_variable_any.

    • A Semaphore is a thread-safe counter that controls access to limited resources. These come in two flavors: a binary semaphore that only has two states (signaled or unsignaled), or a counting semaphore that is signaled when the count is non-zero, and unsignaled when the count is zero. A thread that acquires a signaled semaphore decreases the count by one and continues on; a thread that acquires an unsignaled semaphore is forced to wait until it becomes signaled by another thread releasing it. See carb::cpp::binary_semaphore and carb::cpp::counting_semaphore.

    • A Latch is a one-shot gate that opens once a certain number of threads wait at the gate. See carb::cpp::latch.

    • A Barrier is similar to a Latch, but operates in phases as opposed to being one-shot. See carb::cpp::barrier.

    • A Future and a Promise create a thread-safe one-way synchronization channel for passing results of asynchronous operations. See std::future and std::promise. Note that carb.tasking has its own versions that are fiber-aware: carb::tasking::Promise and carb::tasking::Future.

    • A Spin Lock is a primitive similar to a Mutex that waits by busy-waiting, refusing to give up the thread’s assigned CPU under the guise of resuming as quickly as possible. Spin Locks are not recommended for use in user- level code (kernel or driver code only) and are generally less performant than Mutex due to increased contention.

  • When using carb.tasking’s carb::tasking::ITasking interface to launch tasks, the tasks should use synchronization primitives provided by the ITasking interface (i.e. carb::tasking::Mutex, carb::tasking::Semaphore, etc.).

  • Avoid conditions that may cause a deadlock. A deadlock occurs when one thread has locked Mutex A followed by Mutex B, while another thread has locked Mutex B and wants to lock Mutex A. Each thread owns a resource desired by the other thread and neither will give it up. Therefore, both threads are stuck. A simple way to solve this problem is to always lock Mutexes in the same order (always A followed by B), but this problem can be much more complicated. std::lock (C++11) and std::scoped_lock (C++17) provide the ability to lock multiple locks with deadlock avoidance heuristics.

  • Make use of tools that can help visualize and diagnose threading issues:

  • When writing C++ functions called from Python, release the GIL (Global Interpreter Lock) as soon as possible.

  • Consider using carb.tasking and thinking in terms of tasks or co-routines. See the main article here.


Main article: Testing


Compile-time assertions (using static_assert) should be preferred. Carbonite offers three kinds of runtime assertions:

  • CARB_ASSERT should be used for non-performance-intensive code and code that is commonly run in debug. It compiles to a no-op in optimized builds. It should be used for code where expectations are set when code is written and not possible to change based on user input (e.g. a destructor in a linked list asserting that the number of destroyed items matches size()). It can also be used in performance-critical code where release builds must be as fast as possible and the additional overhead of a check is undesired. Another example would be to CARB_ASSERT immediately prior to gracefully handling a condition, the idea here being to alert a developer to an error condition that would be gracefully handled in release builds.

  • CARB_CHECK is similar to CARB_ASSERT but also occurs in optimized builds. This should be the default for nearly all checks, and especially important for checking assumptions that other developers make, such as iterator validation or function inputs. This assert may be avoided in performance critical code.

  • CARB_FATAL_UNLESS performs a similar check to CARB_CHECK and CARB_ASSERT, but calls std::terminate() after notifying the assertion handler. A string reason is required for CARB_FATAL_UNLESS. This check should be used if the result of failing means that the program is in a bad state, or memory or data would be corrupted, and the entire situation is unrecoverable.


  • Carbonite often runs in a multi-threaded environment, so clear documentation and conformance of how callbacks operate is required.

  • Basic Callback Hygiene in Carbonite is as follows:

    1. Callback un-registration may occur from within the callback.

    2. Un-registration of a callback must ensure that the callback will never be called again, and any calls to the callback in other threads must be complete.

    3. Holding locks while calling a callback is strongly discouraged, and generally will require that they be recursive locks as a callback function may re-enter the system.

  • These principles are codified in the carb::delegate::Delegate class.


  • Exceptions may not cross the ABI boundary of a Carbonite plugin because that would require all users to dynamically link to the same C++ runtime as your plugin to operate safely.

  • Functions in a Carbonite interface should be marked noexcept as they are not allowed to throw exceptions. If an exception is not handled in a function marked noexcept, std::terminate will be called, which will prevent the exception from escaping the ABI boundary. It is also helpful to mark internal functions as noexcept when they’re known not to throw exceptions (especially if you are building with exceptions enabled).

  • Callback functions passed into Carbonite interfaces should be marked as noexcept. Callback types cannot be marked as noexcept until C++17, so this cannot be enforced by the compiler.

  • Other entry points into a carbonite plugin, such as carbOnPluginStartup() must be marked as noexcept as well.

  • The behavior of noexcept described above will only occur in code built with exceptions enabled. Code must be built with exceptions unless exceptions will not occur under any circumstances.

  • When using libraries that can throw exceptions (for example, the STL throws exceptions on GCC even when building with -fno-exceptions), ensure that your exceptions either are handled gracefully or cause the process to exit before the exception can cross the ABI boundary. See the section on error handling for guidelines on how to choose between these two options.

  • Python bindings all need to be built against the same shared C++ runtime because pybind11 C++ objects in a manner that is not ABI safe (this is why they are distributed as headers). Python will also catch exceptions, so exceptions aren’t fatal when they’re thrown in a python binding. Because of this, exceptions are acceptable to use in python bindings as a method of error handling.

  • Because pybind11 can throw exceptions, callbacks into python must call through callPythonCodeSafe() or wrap the callback with wrapPythonCallback() (this also ensures the python GIL is locked).

Error handling

  • Errors that can realistically happen under normal circumstances should always be handled. For example, in almost all cases, you should check whether a file opened successfully before trying to read from the file.

  • Errors that won’t realistically happen or are difficult to recover from, like failed allocation of a 64 byte struct, don’t need to be handled. You must ensure that the application will terminate in a predictable manner if this occurs, however. A failed CARB_FATAL_UNLESS statement is a good way to terminate the application in a reliable way. Allowing an exception to reach the end of a noexcept function is another way to terminate the application in predictable manner.

  • Performing large allocations or allocations where the size is potentially unbounded (e.g. if the size has been specified by code calling into your plugin), should be considered as cases where a memory allocation failure could potentially occur. This should be handled if it is possible; for example, decoding audio or a texture can easily fail for many reasons, so an allocation failure can be reasonably handled. A more complex case, like allocating a stack for a fiber, may be unrealistic to handle, so crashing is acceptable.


  • Log messages should be descriptive enough that the reader would not need to be looking at the code that printed them to understand them. For example, a log message that prints “7” instead of “calculated volume 7” is not acceptable.

  • Strings that are printed in log messages should be wrapped in some form of delimiter, such as '%s', so that it is obvious in log messages if the string was empty. Delimiters may be omitted if the printed string is a compile time constant string or the printed string is already guaranteed to have its own delimiters. For example, the code segment below could print out very confusing looking messages if the quotes around the %s formats were missing (e.g. “failed to make relative to “).

    CARB_LOG_WARN("failed to make '%s' relative to '%s'", baseName, path);
  • Unexpected errors from system library functions should always be logged, preferably as an error. Some examples of unexpected errors would be: memory allocation failure, failing to read from a file for a reason other than reaching the end of the file or GetModuleHandle(nullptr) failing. It is important to log these because this type of thing failing silently can lead to bugs that are very difficult to track down. If a crash handler is bound, immediately crashing after the failure is an acceptable way to log the crash. CARB_FATAL_UNLESS is also a good way to terminate an application while logging what the error condition was.

  • Please use portable formatting strings when you print the values of expressions or variables. The format string is composed of zero or more directives: ordinary characters (not %), which are copied unchanged to the output stream; and conversion specifications, each of which results in fetching zero or more subsequent arguments. Each conversion specification is introduced by the character %, and ends with a conversion specifier. In between there may be zero or more flag characters, an optional minimum field width, an optional precision, and an optional size modifier.

    flag characters



    The value should be converted in “alternative form”. For o conversions, the first character of the output string is made zero (by prefixing a 0 if it was not zero already). For x and X conversions, a nonzero result prefixed by the string 0x (or 0X). For f, e conversions, the result will always contain a decimal point,even if no digits follow it. For g conversion, trailing zeros are not removed from the result.


    The value should be zero padded. If the 0 and - flags both appear, the 0 flag is ignored. If a precision is given with a numeric conversion d, u, o, x, X, the 0 flag is ignored.


    The converted value is to be left adjusted on the field boundary. The converted value is padded on the right with blanks, rather than on the left with blanks or zeros.

    (a space) A blank should be left before positive number (or empty string) produced by a signed conversion.


    A sign (+ or -) should always be placed before a number produced by a signed conversion.

    size modifier



    A following integer conversion corresponds to signed char or unsigned char argument.


    A following integer conversion corresponds to short int or unsigned short int argument.


    A following integer conversion corresponds to long int or unsigned long int argument.


    A following integer conversion corresponds to long long int or unsigned long long int argument.


    A following integer conversion corresponds to intmax_t or uintmax_t argument.


    A following integer conversion corresponds to size_t or ssize_t argument.


    A following integer conversion corresponds to a ptrdiff_t argument.

    conversion specifiers



    The integer argument is converted to signed decimal notation.


    The integer argument is converted to unsigned decimal notation.


    The integer argument is converted to unsigned octal notation.


    The integer argument is converted to unsigned hexadecimal notation.


    The double argument is converted in the style [-]1.123e[-]34


    The double argument is converted in style [-]123.456


    The double argument is converted in style e or f depending on its value.


    The integer argument is converted to a character.


    The argument is expected to be a pointer to a \0-terminated string.


    The argument is expected to be a pointer.


    A % character is printed, no argument is expected.

    The are no standard size modifiers for fixed-size argument types (types like uint32_t..), so the pairs size modifier and conversion specifier are emulated with special platform-specific macros to be portable.



    PRId8, PRIu8, PRIo8, PRIx8, PRIX8

    The integer argument is converted in d, u, o, x, X notation correspondingly and has int8_t or uint8_t type.

    PRId16, PRIu16, PRIo16, PRIx16, PRIX16

    The integer argument is converted in d, u, o, x, X notation correspondingly and has int16_t or uint16_t type.

    PRId32, PRIu32, PRIo32, PRIx32, PRIX32

    The integer argument is converted in d, u, o, x, X notation correspondingly and has int32_t or uint32_t type.

    PRId64, PRIu64, PRIo64, PRIx64, PRIX64

    The integer argument is converted in d, u, o, x, X notation correspondingly and has int64_t or uint64_t type.


    int x = 2, y = 3;
    unsigned long long z = 25ULL;
    size_t s = sizeof(z);
    ptrdiff_t d = &y - &x;
    uint32_t r = 32;
    CARB_LOG_WARN("x = %d, y = %u, z = %llu", x, y, z);
    CARB_LOG_INFO("sizeof(z) = %zu", s);
    CARB_LOG_DEBUG("&y - &x = %td", d);
    CARB_LOG_INFO("r = %"PRIu32"", r);

    Please note, that Windows-family OSes, contrary to Unix family, uses fixed-size types in their API to provide binary compatibility without providing any OS sources. Please use portable macros to make your code portable across different hardware platforms and compilers.

    Windows type

    compatible fixed-size type

    portable format string



    "%"PRId8"", "%"PRIu8"", "%"PRIo8"", "%"PRIx8"", "%"PRIX8""



    "%"PRId16"", "%"PRIu16"", "%"PRIo16"", "%"PRIx16"", "%"PRIX16""



    "%"PRId32"", "%"PRIu32"", "%"PRIo32"", "%"PRIx32"", "%"PRIX32""



    "%"PRId64"", "%"PRIu64"", "%"PRIo64"", "%"PRIx64"", "%"PRIX64""


    DWORD rc = GetLastError();
    if (rc != ERROR_SUCCESS)
        CARB_LOG_ERROR("Operation failed with error code %#"PRIx32"", rc);
        return rc;

Debugging Functionality

When adding code that is to only run or exist in debug builds, it should be wrapped in an #if CARB_DEBUG block. This symbol is defined for all C++ translation units on all platforms and is set to 1 and 0 for the debug and release configurations correspondingly in the carb/Defines.h file. Thus this header file must be included before checking the value of the CARB_DEBUG.

The CARB_DEBUG macro should be preferred over other macros such as NDEBUG, _DEBUG etc.

The preferred method of enabling or disabling debug code that is purely internal to Carbonite would be to check CARB_DEBUG. Do not check the CARB_DEBUG with #ifdef or #if defined() as it will be defined in both release and debug builds.

Batch Coding Conventions

Please consult David Sullins’s guide when writing Windows batch files.

Bash Coding Conventions

  • Bash scripts should be run through shellcheck and pass with 0 warnings (excluding spurious warnings that occur due to edge cases). shellcheck can save you from a wide variety of common bash bugs and typos. For example:

    In scratch/ line 2:
    rm -rf /usr /share/directory/to/delete
           ^-- SC2114: Warning: deletes a system directory. Use 'rm --' to disable this message.
  • Bash scripts should run with set -e and set -o pipefail to immediately exit when an unhandled command error occurs. You can explicitly ignore a command failure by appending || true.

  • Bash scripts should be run with set -u to avoid unexpected issues when variables are unexpectedly unset. A classic example where this is useful is a command such as rm -rf "$DIRECTORY"/*; if DIRECTORY were unexpectedly undefined, set -u would terminate the script instead of destroying your system. If you still want to expand a potentially undefined variable, you can use a default substitution value ${POSSIBLY_DEFINED-$DEFAULT_VALUE}. If $POSSIBLY_DEFINED is defined, it will expand to that value. If $POSSIBLY_DEFINED is not defined, it will expand to $DEFAULT_VALUE. The default value can be empty (${POSSIBLY_DEFINED-}), which will give you behavior identical to the default variable expansion in bash without set -u. You can also use :- instead of - (e.g. ${POSSIBLY_DEFINED:-}) and empty variables will be treated the same as undefined variables.

  • For a stronger guarantee that a command such as rm -rf "$DIRECTORY"/* will not be dangerous, you can expand the variable like this rm -rf "${DIRECTORY:?}"/*, which will terminate the script if it evaluates to an empty string.

  • Use arrays to avoid word splitting. A classic example is something like:

    rm $BUILDDIR/*.o

This will not work on paths with spaces and shellcheck will warn about this. You can instead use an array so that each file will be passed as a separate argument.

    rm "${FILES[@]}"
  • Set nullglob mode when using wildcards:

FILES=(*.c)       # if there are no .c files, "*.c" will be in the array
shopt -s nullglob # set nullglob mode
FILES=(*.c)       # if there are no .c files, the array will be empty
shopt -u nullglob # unset nullglob mode - things will break if you forget this

You can alternatively use failglob to have the command fail out if the glob doesn’t match anything.

  • Bash scripts should use the following shebang: #!/usr/bin/env bash. This is somewhat more portable than: #!/bin/bash.