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:
Do not include
Windows.h
in header files as it is monolithic and pollutes the global environment for Windows. Instead, a much slimmerCarbWindows.h
exists to declare only what is needed by Carbonite. If additional Windows constructs are desired, add them toCarbWindows.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
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"
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 (underinclude/carb/
):
#include <carb/graphics/Graphics.h> // via packman package
#include <doctest/doctest.h>
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:
Matching header include for cpp file is first, if it exists - in a separate group of one file. This is to ensure self-sufficiency.
carb/Defines.h is it’s own group of one file to ensure that it is included before other includes.
Other local includes are in the third group, alphabetically sorted.
Search path includes to Carbonite are in the fourth group (
#include <carb/*>
), alphabetically sorted.Other 3rd party includes are in the fifth group (
#include <*/*>
), alphabetically sorted.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
ormax
. This is an effort to coexist with#include <Windows.h>
whereNOMINMAX
has not been specified:Instead, include/carb/Defines.h has global symbols
::carb_min()
and::carb_max()
that may be used in similar fashion tostd::min
andstd::max
.For rare locations where it is necessary to use
min
andmax
(i.e. to usestd::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, addingthis->
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, addingthis->
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 bestatic
. 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 preferconstexpr
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
#define
s should be set to 0, 1 or some other value. Accessing an#undef
ined 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
#undef
ed 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, “ifdo 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 usestatic_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 thevolatile
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 asauto a = std::unique_ptr(new (std::nothrow) q)
orauto a = [](Spline *s) {return s->reticulate();}
auto
may optionally be used for trailing return types, such asauto 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 usingauto
. 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 otherauto
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 asauto
.Be careful about accidental copy-by-value when you meant copy-by-reference.
Understand the difference between
auto
andauto&
.
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
template
d functions functions, and for member functions defined inside a class declaration. You can additionally declare theminline
to give the compiler a hint for inlining.neither internal-linkage nor
inline
is implied for fully specializedtemplate
d functions, and thus those follow the rules of non-template
d functions (see below)
static¶
Declare non-interface non-member functions as
static
in.cpp
files (or even better, include them in anonymous namespaces).template
d free functions (specialized or not) in.cpp
files also follow this rule.Declare non-interface non-member functions as
static inline
in.cpp
files (orinline
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-specializedtemplete
d free functions also need to be specifiedinline
(as neitherinline
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 onenum
values when thatenum
changes).static_assert
can also be used to verify that alignment andsizeof(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 class
es/struct
s 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 toCARB_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 toCARB_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 toCARB_CHECK
andCARB_ASSERT
, but callsstd::terminate()
after notifying the assertion handler. A string reason is required forCARB_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 markednoexcept
,std::terminate
will be called, which will prevent the exception from escaping the ABI boundary. It is also helpful to mark internal functions asnoexcept
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 asnoexcept
in C++14, so this cannot be enforced by the compiler.Other entry points into a carbonite plugin, such as
carbOnPluginStartup()
must be marked asnoexcept
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 withwrapPythonCallback()
(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 anoexcept
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
andset -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 asrm -rf "$DIRECTORY"/*
; ifDIRECTORY
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 withoutset -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 thisrm -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
.