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.

Repository

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:

Item Description
.vscode Visual Studio Code confiuguration files.
_build Build target outputs (generated).
_compiler Compiler scripts, IDE projects (generated).
deps External dependency configuration files.
docs Carbonite documation.
include/carb Public includes for consumers of Carbonite SDK.
tools Small tools or bootstrappers for the project.
source All source code for project.
source/bindings Script bindings for Carbonite SDK.
source/examples Examples of using Carbonite.
source/framework Carbonite framework implementation.
source/tests Source code for tests of Carbonite.
source/tools Source code for tools built with Carbonite.
source/plugins Carbonite plugin implementations.
source/plugins/carb.assets The carb.assets.plugin implementation
source/plugins/carb.graphics-direct3d The implementation of carb.graphics interface for Direct3D12
source/plugins/carb.graphics-vulkan The implementation of carb.graphics interface for Vulkan
.clang-format Configuration for running clang format on the source code.
.editorconfig Maintains editor and IDE style conformance Ex. Tabs/Spaces.
.flake8 Configuration for additional coding style conformance.
.gitattributes Governs repository attributes for git repository.
.gitignore Governs which files to ignore in the git repository.
build.bat Build script to build debug and release targets on Windows.
build.sh Build script to build debug and release targets on Linux.
CODING.md These coding guidelines.
CONTRIBUTING.md Guidelines for contributing/submitting files to repository.
format_code.bat Run this to format code on Windows before submitting to repository.
format_code.sh Run this to format code on Linux before submitting to repository.
prebuild.bat Run this to generate visual studio solution files on Windows into _compiler folder.
premake5.lua Script for configuration of all build output targets using premake.
setup.sh Setup run once installation script of Linux platform dependencies.
README.md 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.

Include

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"
#if CARB_PLATFORM_WINDOWS
#    include "../CarbWindows.h"
#endif
  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>

#if CARB_PLATFORM_LINUX
#    include <signal.h>
#endif

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.

Files

  • 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.

Type Path
carb::assets::IAssets ./include/carb/assets/IAssets.h
carb::audio::IAudioPlayback ./include/carb/audio/IAudioPlayback.h
carb::settings::ISettings ./include/carb/settings/ISettings.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"
      

Namespaces

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:

Construct Prefixing / Casing
class, struct, enum class and typedef PascalCase
constants kCamelCase
enum class values eCamelCase
functions camelCase
private/protected functions _camelCase
exported C plugin functions carbCamelCase
public member variables camelCase
private/protected member variables m_camelCase
private/protected static member variables s_camelCase
global - static variable at file or project scope g_camelCase
local variables camelCase

When a name includes an abbreviation 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.

myVector.getLength();
myObject.applyForce(x, y, z);
myObject.isDynamic();
texture.getFormat();
  • 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.getName();
employee.setName("Jensen Huang");
light.isEnabled();
light.setEnabled(true);
  • 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

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 slangc.py 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

  • 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 them is unneccessary and frowned upon.

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

  • 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
{
public:

    Widget();

    ~Widget();

    const char* getName() const;

    void setName(const char* name);

    bool isEnabled() const;

    bool setEnabled(bool enabled);

private:
    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
{
public:

    enum class Projection
    {
        ePerspective,
        eOrthographic
    };

    Camera();

    ~Camera();
  • 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);
}
}

NOTE The use of static constexpr within a class or struct may cause link problems if you don’t (re)define the field outside the class. Actual errors could not show up immediately, but reveal much later on some kind of usages (that take a reference to that variable, and where that operation is not optimized away). This is a “bug” in the standard that has been fixed with the introduction of inline variables in C++17 (in C++17 static constexpr implicitly defines an inline variables, and always works as expected).

Rules for Preprocessors 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 CARB_EXPORTS
    #ifdef __cplusplus
        #define CARB_EXPORT extern "C"
    #else
        #define CARB_EXPORT
    #endif
#endif
  • 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.

  • When adding #if pre-processor blocks to support multiple platforms, the block must end with an #else clause containing the specific exact text #error Unsupported platform. This also means that the only use for the #else clause is for the #error statement; if code is conditionally enabled for a platform it must be within a specific #if or #elif clause for the supported platform(s). In other words, “if do X, else (implied all other platforms) do Y” is not allowed; all platforms that conditionally enable code must be specifically stated.

#if CARB_PLATFORM_WINDOWS
    // code
#elif CARB_PLATFORM_LINUX || CARB_PLATFORM_MACOS
    // code
#else
#    error "unsupported platform"
#endif

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
{
protected:

    /**
     * @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.

License

  • 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.

Indentation

  • 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 (enumName.compare("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
{
    ...

private:
    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 - sphere.center.x) * (point.x - sphere.center.x) +
            (point.y - sphere.center.y) * (point.y - sphere.center.x) +
            (point.z - sphere.center.z) * (point.z - sphere.center.x));
  • Use a line of space within .cpp implementation functions to help organize blocks of code.

// Lookup device surface extensions
 ...
 ...

// Create the platform surface connection
 ...
 ...
 ...

Indentation

  • 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())
{
    return;
}
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 - Recommendations

  • Here are just some recommendation if you plan to use any new C++14 features in your code.

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<Foo> only when sharing is required.

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

Casts

  • 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.

  • Use the narrowest cast possible. Only use reinterpret_cast if it is unavoidable:

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

Containers

  • 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 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 UTF8 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.

  • You are free to use std::string inside implementations 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.

Auto

  • Avoid the use of auto where it will make code more difficult to read for devs 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 a = [](Spline *s) {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

  • 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.

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

Range-based loops

  • They’re great, use them.

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

  • They are often more readable.

  • Do not use auto within for simple types.

// BAD: Suggests dev might be of type Device.
for (const auto& dev : devices)

// GOOD: More obvious that the iterator is a device index.
for (int dev : devices)

Integer types

  • We prefer to use the standard integer types as defined in: http://en.cppreference.com/w/cpp/header/cstdint

#include <cstdint>

nullptr

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

friend

  • 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)

static

  • 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.

inline

  • 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).

static_assert

  • 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 within a class before C++17

}

The “special” case for classes/structs is due to a “bug” in the C++14 standard, that has been fixed in C++17 (after switching to C++17, we’ll be able to use an unified syntax).

C++ - General Recommendations

We follow the GameWorks coding style when it comes to naming in all other regards. The standard is not prescriptive in how to name and use namespaces. We have therefore developed a few conventions in that area that we feel improves readability and usability of the libraries.

Assertions

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.

Exceptions

  • 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 in C++14, 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.

Logging

  • 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 formating 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 description
# 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.
0 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 laft 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 description
hh A following integer conversion corresponds to signed char or unsigned char argument.
h A following integer conversion corresponds to short int or unsigned short int argument.
l A following integer conversion corresponds to long int or unsigned long int argument.
ll A following integer conversion corresponds to long long int or unsigned long long int argument.
j A following integer conversion corresponds to intmax_t or uintmax_t argument.
z A following integer conversion corresponds to size_t or ssize_t argument.
t A following integer conversion corresponds to a ptrdiff_t argument.
conversion specifiers description
d The integer agrument is converted to signed decimal notation.
u The integer argument is converted to unsigned decimal notation.
o The integer argument is converted to unsigned octal notation.
x,X The integer argument is converted to unsigned hexadecimal notation.
e The double argument is converted in the style [-]1.123e[-]34
f The double argument is converted in style [-]123.456
g The double argument is converted in style e or f depending on its value.
c The integer argument is converted to a character.
s The argument is expected to be a pointer to a \0-terminated string.
p 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 agrument types (types like uint32_t..), so the pairs size modifier and conversion specifier are emulated with special platform-specific macros to be portable.

macros description
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.

Example:

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 extremal 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
BYTE uint8_t "%"PRId8"", "%"PRIu8"", "%"PRIo8"", "%"PRIx8"", "%"PRIX8""
WORD uint16_t "%"PRId16"", "%"PRIu16"", "%"PRIo16"", "%"PRIx16"", "%"PRIX16""
DWORD uint32_t "%"PRId32"", "%"PRIu32"", "%"PRIo32"", "%"PRIx32"", "%"PRIX32""
QWORD uint64_t "%"PRId64"", "%"PRIu64"", "%"PRIo64"", "%"PRIx64"", "%"PRIX64""

Example:

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/bad.sh 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.

    FILES=($BUILDDIR/*.o)
    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.