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 configuration files. |
_build |
Build target outputs (generated). |
_compiler |
Compiler scripts, IDE projects (generated). |
deps |
External dependency configuration files. |
docs |
Carbonite documentation. |
include/carb |
Public includes for consumers of Carbonite SDK. |
tools |
Small tools or boot-strappers 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. |
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 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
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()
andcarb_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 teamThis 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 or acronym that is commonly written entirely in uppercase, you must still follow the casing rules laid out above. For instance:
void* gpuBuffer; // not GPUBuffer
struct HtmlPage; // not HTMLPage
struct UiElement; // not UIElement
using namespace carb::io; // namespaces are always lowercase
Naming - Guidelines
All names must be written in US English.
std::string fileName; // NOT: dateiName
uint32_t color; // NOT: colour
The following names cannot be used according to the C++ standard:
names that are already keywords;
names with a double underscore anywhere are reserved;
names that begin with an underscore followed by an uppercase letter are reserved;
names that begin with an underscore are reserved in the global namespace.
Method names must always begin with a verb.
This avoids confusion about what a method actually does.
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
Internal code
For public header files, a detail
namespace should be used to declare implementation as private and subject to change,
as well as signal to external users that the functions, types, etc. in the detail
namespace should not be called.
Within a translation unit (.cpp file), use an anonymous namespace to prevent external linkage or naming conflicts within a module:
namespace // anonymous
{
struct OnlyForMe
{
};
}
In general, prefer anonymous namespaces over static
.
Deprecation and Retirement
As part of the goal to minimize major version changes, interface functions may be deprecated and retired through the Deprecation and Retirement Guidelines section of the Architectural Overview.
Shader naming
HLSL shaders must have the following naming patterns to properly work with our compiler and 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
Classes that should not be inherited from should be declared as
final
.Each access modifier appears no more than once in a class, in the order:
public
,protected
,private
.All
public
member variables live at the start of the class. They have no prefix. If they are accessed in a member function that access must be prefixed withthis->
for improved readability and reduced head-scratching.All
private
member variables live at the end of the class. They are prefixed withm_
. They should be accessed directly in member functions, addingthis->
to access members in the same class is unnecessary.Use
protected
member variables judiciously. They are prefixed withm_
. They should be accessed directly in member functions, addingthis->
to access them is unnecessary.Constructors and destructor are first methods in a class after
public
member variables unless private scoped in which case they are firstprivate
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
, orfinal
.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
overenum
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);
}
}
Rules for Pre-processors and Macros
It’s recommended to place preprocessor definitions in the source files instead of makefiles/compiler/project files.
Try to reduce the use of
#define
(e.g. for constants and small macro functions), and 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 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
#undef
ed at the end of the header file.CARB_POSIX
is set to_POSIX_VERSION
on platforms that are mostly POSIX conformant, such as Linux and MacOS.CARB_POSIX
is set to0
on other platforms. Functions used in these blocks should be verified to actually follow the POSIX standard, rather than being common but non-standard (e.g.ptrace
). Non-standard calls insideCARB_POSIX
blocks should be wrapped in a nested platform check, such asCARB_PLATFORM_LINUX
.When adding
#if
pre-processor blocks to support multiple platforms, the block must end with an#else
clause containing theCARB_UNSUPPORTED_PLATFORM()
macro. An exception to this is when the#else
block uses entirely C++ standard code; this sometimes happens in the case of platform-specific optimizations. This logic also applied to#if
directives nested in an#if
block for a standard, such asCARB_POSIX
where the#else
block follows that platform. In other words, you may not make assumptions about what features future platforms may have, aside from what’s in the C++ standard; all platform-specific code must have the associated platform specifically stated.Macros that do not have universal appeal (i.e. are only intended to be used within a single header file) shall be prefixed with
CARBLOCAL_
and#undef
’d at the end of the file.
#if CARB_PLATFORM_WINDOWS
// code
#elif CARB_PLATFORM_LINUX
// code
#elif CARB_PLATFORM_MACOS
// code
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
#if CARB_PLATFORM_WINDOWS
// Windows-specific code
#else
// C++ standard code
#endif
Porting to new platforms
This is the process to port Carbonite to new platforms with minimal disruption to development work. This is the process being used to port Carbonite to Mac OS.
The initial commit to master is the minimal code to get the new platform to build. Code paths that cannot be shared with another platform will have a crash macro to mark where they are (The Mac OS port has
CARB_MACOS_UNIMPLEMENTED()
for this). Each crash macro should have a comment with an associated ticket for fixing it. After this point, CI builds should be enabled.Code added after the initial commit should still build for the new platform, but new code can use the crash macro if needed.
CI testing is enabled on a subset of the tests once the framework is able to run on the new platform.
Once there are no remaining crash macros and all tests are enabled on CI, the new platform will be considered fully supported.
Commenting - Header Files
Avoid spelling and grammatical errors.
Assume customers will read comments. Err on the side of caution
Cautionary tale: to ‘nuke’ poor implementation code is a fairly idiomatic usage for US coders. It can be highly offensive elsewhere.
Each source file should start with a comment banner for license
This should be strictly the first thing in the file.
Header comments use doxygen format. We are not too sticky on doxygen formatting policy.
All public functions and variables must be documented.
The level of detail for the comment is based on the complexity for the API.
Most important is that comments are simple and have clarity on how to use the API.
@brief
can be dropped and automatic assumed on first line of code. Easier to read too.@details
is dropped and automatic assumed proceeding the brief line.@param
and@return
are followed with a space after summary brief or details.
/**
* Tests whether this bounding box intersects the specified bounding box.
*
* You would add any specific details that may be needed here. This is
* only necessary if there is complexity to the user of the function.
*
* @param box The bounding box to test intersection with.
* @return true if the specified bounding box intersects this bounding box;
* false otherwise.
*/
bool intersects(const BoundingBox& box) const;
Overridden functions can simply refer to the base class comments.
class Bar: public Foo
{
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.
We use a
.clang-format
file with clang-format (viarepo_format
) to keep our code auto-formatted.In some rare cases where code is manually formatted in a pleasing fashion, auto-formatting can be suspended with a comment block:
// clang-format off ... Manually formatted code // clang-format on
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 and Beyond Recommendations
Carbonite supports a minimum of C++14, therefore public include files should not use any features in later standards.
Carbonite includes some implementations of C++17 and later features that will build on C++14. These are located in directories that name the standard, such as include/carb/cpp17.
Pointers and Smart Pointers
Use raw C/C++ pointers in the public interface (Plugin ABI).
In other cases prefer to use std::unique_ptr or std::shared_ptr to signal ownership, rather than using raw pointers.
Use std::shared_ptr only when sharing is required.
Any
delete
ordelete[]
call appearing in the code is a red flag and needs a good reason.
RAII
Consider using std::unique_ptr to apply RAII principles to malloc()
/free()
style APIs, though keep in mind that
the deleter will only be called if the pointer is non-nullptr
.
For example, the following could be used to apply RAII to dlopen()
/dlclose()
:
struct FreePosixLib
{
void operator()(void* p) noexcept
{
dlclose(p);
}
};
using UniquePosixLib = std::unique_ptr<void, FreePosixLib>;
void sample()
{
auto scoped = UniquePosixLib{ dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD) };
void* lib = scoped.get();
}
For a one-line variant of this, you can use the following, but the sizeof
this unique_ptr
is 2 pointers in this case,
so the above should be preferred when it’s reasonable.
std::unique_ptr<void, int(*)(void*)> scoped(dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD), dlclose)
Another alternative is CARB_SCOPE_EXIT()
. When exiting from a scope, this executes a lambda with local variables
captured by reference. The above example could be written as:
void* lib = dlopen(nullptr, RTLD_LAZY | RTLD_NOLOAD);
CARB_SCOPE_EXIT
{
if (lib)
{
dlclose(lib);
}
};
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.
Using dynamic_cast requires RTTI (Run-Time Type Information), is slow, and happens at runtime. Avoid using
dynamic_cast
.Use the narrowest cast possible. Only use reinterpret_cast if it is unavoidable. Note that static_cast can be used for
void*
:void* userData = ...; MyClass* c = static_cast<MyClass*>(userData);
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 and ABI Compatibility for more details.
Characters and Strings
All strings internally and in interfaces are of the same type: 8-bit char. This type should always be expected to hold a UTF-8 encoded string. This means that the first 7-bits map directly to ASCII and above that we have escaped multi-byte sequences. Please read Unicode to learn how to interact with OS and third-part APIs that cannot consume UTF8 directly. If you need to enter a text string in code that contains characters outside 7-bit ASCII then you must also read Unicode.
For ABI-safe strings, you can use
omni::string
; a string class similar tostd::string
.You are free to use
std::string
inside modules but we cannot expose STL string types in public interfaces (violation of Plugin ABI). Instead use (const) char pointers. This does require some thought on lifetime management. Usually the character array can be associated with an object and the lifetime is directly tied to that object.Even though you can use STL strings and functionality inside your implementation please consider first if what you want to do is easily achievable with the C runtime character functions. These are considerably faster and often lead to fewer lines of code. Additionally the STL string functions can raise exceptions on non-terminal errors and Carbonite plugins are built without exception support so it will most likely just crash.
Auto
Avoid the use of
auto
where it will make code more difficult to read for developers who do not have an in-depth knowledge of the codebase. Reading someone else’s code is harder than writing your own code, so code should be optimized for readability.auto
should be used for generic code, such as templates and macros, where the type will differ based on invocation.auto
may optionally be used for overly verbose types that have a standard usage, such as iterators.auto
may be optionally used for types where the definition makes the type obvious, such asauto a = std::unique_ptr(new (std::nothrow) Q)
orauto&& lambda = [](Spline *s) -> Spline* { 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.For large lambdas, avoid using capture-all
[=]
and[&]
, and prefer explicit capture (by reference or by value, as needed)For asynchronous lambdas, such as passed to
ITasking
functions, orstd::thread
orstd::async
make sure to capture local variables by value instead of by reference or pointer as they will have gone out of scope.
Range-based loops
They’re great, use them.
They don’t necessarily have to be combined with
auto
.They are often more readable.
For complex objects, make sure to use a reference (
&
) or forwarding reference (&&
) if possible to avoid copying (especially when combined withauto
).Use accurate variable naming, as this is more important than choosing between using
auto
or the type name:// BAD: Suggests `dev` might be of type Device even though it's a device ID. `auto` without reference means that a // a large object could be copied. for (auto dev : devices) // GOOD: More obvious that the iterator is a device index. for (int dev : devices) // BETTER: Even more obvious that the iterator is a device index. for (int devId : devices) // BEST: Blatantly obvious that the iterator and container are device indexes. for (int devId : deviceIndexes) // or for (auto&& devId : deviceIndexes)
Integer types
We prefer to use the standard integer types as defined in the C++ standard.
#include <cstdint>
nullptr
Use
nullptr
for any pointer types instead of0
orNULL
.
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 members of which an address is required within a class before C++17, otherwise
// link errors will occur.
}
NOTE Prior to C++17, the use of
static constexpr
as a member within astruct
/class
may cause link problems if the address is taken of the member, unless the definition of the member is contained within a translation unit. This is not possible for header-only classes. Therefore, avoid usingstatic constexpr
members when the address is required of the member (i.e. passed via pointer or reference). Avoid usingstatic constexpr
string or character array members. With C++17, it is possible to declare static members asinline
, andinline
is implied forstatic constexpr
members. However, Carbonite supports a minimum of C++14.
Higher-level Concepts
Internal and Experimental Code
Functions, types, etc. that are inside of a detail
/details
namespace, or contain detail[s]
or internal
(in any
capitalization) as part of their name should not be called by code that is not packaged with and built at the same time.
These functions, types, etc. should be considered private and may change at any time, therefore their existence should not be relied upon.
Similarly, any functions, types, etc. marked as experimental should be considered as such: the ABI may not be entirely stable and subject to change in the future.
Carbonite uses detail
namespace (not details
) to contain private/internal code. The internal
name may also be used
for function and type names.
Thread-safety
Writing thread-safe code is very difficult. Introducing asynchronicity and threads means that code will execute non-deterministically and have potentially exponential variations.
Use std::atomic types sparingly and with caution! These atomic types can ensure low-level synchronization, but also lead to a false sense of thread safety. Race conditions are still quite possible with
std::atomic
. Consider higher- level synchronization primitives instead.Avoid explicitly specifying std::memory_order on uses of std::atomic functions except as described below. The default memory order is also the safest:
std::memory_order_seq_cst
(sequentially consistent). Specifying an incorrect memory order especially on weakly-ordered machines such as ARM can lead to unexpected and extremely-difficult-to-track-down errors.The
std::memory_order_seq_cst
memory order always can be specified explicitly to indicate where sequential consistency is required.For performance intensive code, at least one but ideally two Senior or Principal engineers may sign off on more weakly-ordered uses of specified memory orders.
All explicit uses of memory order should be commented.
volatile
is not a synchronization primitive! It makes no guarantees about atomicity, visibility or ordering. Do not use volatile as a synchronization primitive! Much more control and all of the necessary guarantees are given bystd::atomic
.Avoid global and function-local
static
variables which may be modified by several threads.Avoid busy-waiting–spinning while waiting for a condition. This includes loops that call
std::this_thread::yield()
or sleeping for a brief period of time. Properly architected synchronization code will block in the operating system while waiting for a condition to be met.Many containers and library functions are not thread-safe. Be sure to check the documentation and assume that everything is not thread-safe unless explicitly stated.
Carbonite includes two containers that are not only thread-safe but are generally high performance and wait-free:
carb::container::LocklessQueue
andcarb::container::LocklessStack
.
Use the right synchronization primitive for the job:
std::call_once executes a callable exactly once, even if called concurrently from several threads. All threads wait until the callable completes execution.
So-called “magic”
static
variables (function-local static initialization) is guaranteed to be thread-safe with C++11. That is, they execute in the same manner as std::call_once ensuring that construction happens exactly once and all threads wait until the construction is finished.However, keep in mind that only the static initialization is guaranteed to be thread-safe, but remember that initialization can be by a function return value, in which case the function is called in a thread-safe manner.
A Mutex is one of the most common synchronization primitives for mutual-exclusion and can be used to protect memory reads and writes in critical sections of code. See
carb::thread::mutex
andcarb::thread::recursive_mutex
. Since only one thread may have a mutex locked, all other threads must stop and wait in order to gain exclusive access.A Shared Mutex (sometimes called a read/write mutex) is similar to a Mutex but can be accessed in either of two modes: shared (read) mode, or exclusive (write) mode. A thread which has exclusive access causes all other threads to wait. A thread which has shared access allows other threads to also obtain shared access but causes any threads seeking exclusive access to wait. See
carb::thread::shared_mutex
andcarb::thread::recursive_shared_mutex
.A Condition Variable can be used to signal threads: threads wait until a condition is true, at which point they are signaled. Condition Variables work in concert with a Mutex. See std::condition_variable or std::condition_variable_any.
A Semaphore is a thread-safe counter that controls access to limited resources. These come in two flavors: a binary semaphore that only has two states (signaled or unsignaled), or a counting semaphore that is signaled when the count is non-zero, and unsignaled when the count is zero. A thread that acquires a signaled semaphore decreases the count by one and continues on; a thread that acquires an unsignaled semaphore is forced to wait until it becomes signaled by another thread releasing it. See
carb::cpp::binary_semaphore
andcarb::cpp::counting_semaphore
.A Latch is a one-shot gate that opens once a certain number of threads wait at the gate. See
carb::cpp::latch
.A Barrier is similar to a Latch, but operates in phases as opposed to being one-shot. See
carb::cpp::barrier
.A Future and a Promise create a thread-safe one-way synchronization channel for passing results of asynchronous operations. See std::future and std::promise. Note that carb.tasking has its own versions that are fiber-aware:
carb::tasking::Promise
andcarb::tasking::Future
.A Spin Lock is a primitive similar to a Mutex that waits by busy-waiting, refusing to give up the thread’s assigned CPU under the guise of resuming as quickly as possible. Spin Locks are not recommended for use in user- level code (kernel or driver code only) and are generally less performant than Mutex due to increased contention.
When using carb.tasking’s
carb::tasking::ITasking
interface to launch tasks, the tasks should use synchronization primitives provided by theITasking
interface (i.e.carb::tasking::Mutex
,carb::tasking::Semaphore
, etc.).Avoid conditions that may cause a deadlock. A deadlock occurs when one thread has locked Mutex A followed by Mutex B, while another thread has locked Mutex B and wants to lock Mutex A. Each thread owns a resource desired by the other thread and neither will give it up. Therefore, both threads are stuck. A simple way to solve this problem is to always lock Mutexes in the same order (always A followed by B), but this problem can be much more complicated. std::lock (C++11) and std::scoped_lock (C++17) provide the ability to lock multiple locks with deadlock avoidance heuristics.
Make use of tools that can help visualize and diagnose threading issues:
Visual Studio has several tools, such as Parallel Stacks and Parallel Watch.
On Linux, Valgrind and Thread Sanitizer may be of use.
carb.tasking.plugin has a robust debug visualizer for Visual Studio.
When writing C++ functions called from Python, release the GIL (Global Interpreter Lock) as soon as possible.
When using Pybind, this can be accomplished through the py::gil_scoped_release RAII class.
Consider using carb.tasking and thinking in terms of tasks or co-routines. See the main article here.
Testing
Main article: Testing
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 calls std::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.
Callbacks
Carbonite often runs in a multi-threaded environment, so clear documentation and conformance of how callbacks operate is required.
Basic Callback Hygiene in Carbonite is as follows:
Callback un-registration may occur from within the callback.
Un-registration of a callback must ensure that the callback will never be called again, and any calls to the callback in other threads must be complete.
Holding locks while calling a callback is strongly discouraged, and generally will require that they be recursive locks as a callback function may re-enter the system.
These principles are codified in the
carb::delegate::Delegate
class.
Exceptions
Exceptions may not cross the ABI boundary of a Carbonite plugin because that would require all users to dynamically link to the same C++ runtime as your plugin to operate safely.
Functions in a Carbonite interface should be marked noexcept as they are not allowed to throw exceptions. If an exception is not handled in a function marked noexcept,
std::terminate
will be called, which will prevent the exception from escaping the ABI boundary. It is also helpful to mark internal functions as noexcept when they’re known not to throw exceptions (especially if you are building with exceptions enabled).Callback functions passed into Carbonite interfaces should be marked as noexcept. Callback types cannot be marked as noexcept until C++17, so this cannot be enforced by the compiler.
Other entry points into a carbonite plugin, such as
carbOnPluginStartup()
must be marked as noexcept as well.The behavior of noexcept described above will only occur in code built with exceptions enabled. Code must be built with exceptions unless exceptions will not occur under any circumstances.
When using libraries that can throw exceptions (for example, the STL throws exceptions on GCC even when building with
-fno-exceptions
), ensure that your exceptions either are handled gracefully or cause the process to exit before the exception can cross the ABI boundary. See the section on error handling for guidelines on how to choose between these two options.Python bindings all need to be built against the same shared C++ runtime because pybind11 C++ objects in a manner that is not ABI safe (this is why they are distributed as headers). Python will also catch exceptions, so exceptions aren’t fatal when they’re thrown in a python binding. Because of this, exceptions are acceptable to use in python bindings as a method of error handling.
Because pybind11 can throw exceptions, callbacks into python must call through
callPythonCodeSafe()
or wrap the callback 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 formatting strings when you print the values of expressions or variables. The format string is composed of zero or more directives: ordinary characters (not
%
), which are copied unchanged to the output stream; and conversion specifications, each of which results in fetching zero or more subsequent arguments. Each conversion specification is introduced by the character%
, and ends with a conversion specifier. In between there may be zero or more flag characters, an optional minimum field width, an optional precision, and an optional size modifier.flag characters
description
#
The value should be converted in “alternative form”. For
o
conversions, the first character of the output string is made zero (by prefixing a0
if it was not zero already). Forx
andX
conversions, a nonzero result prefixed by the string0x
(or0X
). Forf
,e
conversions, the result will always contain a decimal point,even if no digits follow it. Forg
conversion, trailing zeros are not removed from the result.0
The value should be zero padded. If the
0
and-
flags both appear, the0
flag is ignored. If a precision is given with a numeric conversiond
,u
,o
,x
,X
, the0
flag is ignored.-
The converted value is to be left adjusted on the field boundary. The converted value is padded on the right with blanks, rather than on the left with blanks or zeros.
(a space) A blank should be left before positive number (or empty string) produced by a signed conversion.
+
A sign (
+
or-
) should always be placed before a number produced by a signed conversion.size modifier
description
hh
A following integer conversion corresponds to
signed char
orunsigned char
argument.h
A following integer conversion corresponds to
short int
orunsigned short int
argument.l
A following integer conversion corresponds to
long int
orunsigned long int
argument.ll
A following integer conversion corresponds to
long long int
orunsigned long long int
argument.j
A following integer conversion corresponds to
intmax_t
oruintmax_t
argument.z
A following integer conversion corresponds to
size_t
orssize_t
argument.t
A following integer conversion corresponds to a
ptrdiff_t
argument.conversion specifiers
description
d
The integer argument 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
orf
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 argument 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 hasint8_t
oruint8_t
type.PRId16
,PRIu16
,PRIo16
,PRIx16
,PRIX16
The integer argument is converted in
d
,u
,o
,x
,X
notation correspondingly and hasint16_t
oruint16_t
type.PRId32
,PRIu32
,PRIo32
,PRIx32
,PRIX32
The integer argument is converted in
d
,u
,o
,x
,X
notation correspondingly and hasint32_t
oruint32_t
type.PRId64
,PRIu64
,PRIo64
,PRIx64
,PRIX64
The integer argument is converted in
d
,u
,o
,x
,X
notation correspondingly and hasint64_t
oruint64_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 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
.