As a baseline, Donner SVG aligns with the Google C++ coding style, with modifications to more closely align with naming conventions in the SVG standard. Additionally, since Donner SVG is designed as an experiment in C++20, coding standards that exist to support backward compatibility with older standards may be replaced.
Files
Naming
- Folders: Names are lowercase, and one word is preferred. For multiple words, use lower_snake_case.
- Files: UpperCamelCase, matching the C++ class in the file contents, e.g., PathSpline.cc or SVGPathElement.h.
- The filename should match the principal class or struct in the file contents.
- Use the .cc extension for source files, and .h for header files.
- Use the _tests.cc suffix for test files, and _fuzzer.cc for fuzzing files.
- Note: Main files may not follow these patterns, e.g., debugger_main.h.
Examples:
Header Files
- Use #pragma once for header guards.
- Use /// @file at the top of the file, and optionally provide a brief description of the file contents.
- All code is under the donner namespace, with sub-namespaces for different components, e.g., donner::svg.
}
Donner SVG library, which can load, manipulate and render SVG files.
Documentation
Struct and Class Documentation:
- Use /** style multi-line comments for detailed descriptions.
- Provide comprehensive explanations, offering new details that are not immediately obvious from the code. Include usage context and examples where relevant.
Member Documentation:
- Use //!< for inline member documentation, or /** for long descriptions.
- All public members and enum values are required, private/protected members are recommended.
- For struct members, either provide brief single-line descriptions or detailed explanations if relevant.
struct Command {
CommandType type;
size_t pointIndex;
};
enum class ClipPathUnits {
ObjectBoundingBox,
};
Method Documentation:
- Use /** style multi-line comments for methods.
- Omit the @brief prefix; the first line will be the brief by default.
- Describe the purpose and behavior of the method.
- Mention return values when relevant, but don't always use explicit @return tags.
Vector2d pointAt(size_t index, double t) const;
Documentation Content:
- Include tables for complex explanations (e.g., attribute descriptions).
- Provide notes about limitations or unsupported features.
- Cross-reference other parts of the code using \ref.
Includes
Include paths for Donner files are relative to the Donner SVG directory and use double quotes:
STL and third-party dependencies do not use this, and use angle brackets:
#include <vector>
#include <gmock/gmock.h>
Symbol Naming
- Classes: UpperCamelCase, matching the filename. This matches the SVG standard for DOM object naming.
- Class Methods: lowerCamelCase, aligning with the SVG standard.
- Constructors and constructor-like static methods continue to use UpperCamelCase.
- Free Functions and Static Methods: UpperCamelCase.
- Member Variables: lowerCamelCaseWithTrailingUnderscore_.
- Parameters and Local Variables: lowerCamelCase.
- Constants: Use the k prefix, and then UpperCamelCase: kExampleConstant.
- Enum Class Values: UpperCamelCase, no k prefix (e.g. MyEnum::Value).
For variables that hold values with units, such as milliseconds, either use a strongly-typed container or include the unit in the variable name:
const int kTimeoutMs = 100;
const Duration kTimeout = Duration::Milliseconds(2.0);
Example in context:
#pragma once
#include <string>
class ExampleClass {
public:
static ExampleClass Create(int inputVariable) {
const int kConstant = 0;
return ExampleClass();
}
ExampleClass() = default;
int getMember() const {
return someMember_;
}
private:
int someMember_ = 1;
};
#define UTILS_RELEASE_ASSERT(x)
An assert that evaluates on both release and debug builds.
Definition Utils.h:101
Column Limit
The column limit is set at 100 characters.
Formatting
The .clang-format file in the repository root is the source of truth for formatting.
Tests
Tests are placed in a tests/ directory near the file they are testing. Test files should have the suffix of _tests.cc.
Code Conventions
Transform Notation and Coordinate Systems
"DestinationFromSource" Notation
When working with transformation matrices in the context of SVG and 2D graphics, it's crucial to maintain clarity in how coordinate systems are transformed. Donner SVG adopts the "destinationFromSource" notation for transformation matrices. This notation explicitly indicates the source and destination coordinate systems, improving readability and reducing errors in matrix operations.
- Notation: destinationFromSource
- Interpretation: Transforms coordinates from the source coordinate system to the destination coordinate system.
Example in SVG Rendering:
In SVG, common coordinate systems include:
- Local Coordinate System: The coordinate system local to an SVG element.
- User Space: The coordinate system established by the SVG viewport.
- Viewport Coordinate System: The coordinate system of the rendering surface (e.g., screen or canvas).
Suppose we have an SVG element that needs to be transformed from its local coordinate system to user space, and then from user space to the viewport.
Transformd localFromUserSpace = ...;
Transformd userSpaceFromViewport = ...;
Transformd viewportFromLocal = userSpaceFromViewport * localFromUserSpace;
Vector2d pointInLocal = ...;
Vector2d pointInViewport = viewportFromLocal * pointInLocal;
Matrix Multiplication Order:
In this notation, transformations are applied from right to left, corresponding to the order of multiplication:
Importance of Consistent Notation
- Clarity: By explicitly stating the source and destination coordinate systems, the code becomes self-documenting.
- Correctness: Ensures that transformations are applied in the correct order, reducing bugs related to matrix operations.
- Maintainability: Makes it easier for other developers to understand and modify the code.
Applying the Notation in Code
When defining transformations:
- Name variables according to the coordinate systems they transform between.
- Multiply matrices in an order that reflects the coordinate system transitions.
Example in instantiateMask:
Transformd userSpaceFromMaskContent = Transformd::Translate(contentUnitsBounds.topLeft)
* Transformd::Scale(contentUnitsBounds.size());
layerBaseTransform_ = userSpaceFromMaskContent * layerBaseTransform_;
Explanation:
- userSpaceFromMaskContent transforms coordinates from the mask content coordinate system to user space.
- The layerBaseTransform_ is updated by pre-multiplying it with userSpaceFromMaskContent, ensuring that subsequent rendering operations are in the correct coordinate system.
Another Example in Gradient Instantiation:
Transformd gradientUnitsFromObjectBoundingBox = Transformd::Translate(pathBounds.topLeft)
* Transformd::Scale(pathBounds.size());
Transformd gradientLocalFromGradientUnits = gradientLocalFromGradientTransform
* gradientUnitsFromObjectBoundingBox;
- Here, gradientUnitsFromObjectBoundingBox transforms from the object bounding box coordinate system to the gradient units coordinate system.
- gradientLocalFromGradientUnits then applies any additional transformations specified by gradientTransform.
Other Patterns and Practices
Const Correctness
Apply const whenever possible, for any methods that don't modify the object's state.
Vector2d pointAt(size_t index, double t) const;
For variables, use const whenever possible. Only variables that are modified after initialization should be non-const.
const int result = computeResult();
int timeoutRemainingMs = 100;
while (timeoutRemainingMs > 0) {
timeoutRemainingMs -= elapsedTime();
}
struct and class
- Use struct for data classes and class for classes with logic.
- Implement comparison operators using = default when possible.
- Single-argument constructors should be marked explicit by default.
- Use /* implicit */ to indicate intentional implicit constructors.
- Provide operator<< for data classes to streamline debugging.
Example:
struct Vector2 {
float x = 0.0f;
float y = 0.0f;
bool operator==(const Vector2& rhs) = default;
friend std::ostream& operator<<(std::ostream& os, const Vector2& point);
float magnitude() const;
};
class Transform {
public:
Transform(const Matrix3x3& matrix);
bool operator==(const Transform& rhs) = default;
friend std::ostream& operator<<(std::ostream& os, const Transform& transform);
Vector2 apply(const Vector2& point) const;
private:
Matrix3x3 matrix_;
};
Properties (Getters and Setters)
- Use thing() and setThing(...) naming.
RcString id() const;
void setId(std::string_view id);
- Use std::optional for potentially absent values, and to handle unsetting a value.
std::optional<Lengthd> x1() const;
void setX1(std::optional<Lengthd> x1);
enum class
- Use enum class for all enums, and provide an operator<< for debugging.
enum class MaskUnits {
UserSpaceOnUse,
ObjectBoundingBox,
Default = UserSpaceOnUse,
};
inline std::ostream& operator<<(std::ostream& os, MaskUnits units) {
switch (units) {
case MaskUnits::UserSpaceOnUse: return os << "MaskUnits::UserSpaceOnUse";
case MaskUnits::ObjectBoundingBox: return os << "MaskUnits::ObjectBoundingBox";
}
}
#define UTILS_UNREACHABLE()
A hint to the compiler that the code following this macro is unreachable.
Definition Utils.h:56
Asserts
Utility Macros and Functions
Include "donner/base/Utils.h" for utility macros:
Strings
- Use std::string_view for non-owning references.
- Use RcString for owning references.
- Use RcStringOrRef for passing either a non-owning std::string_view or an owning RcString which can be add-ref'd.
For passing strings as parameters:
- void fn(std::string_view str) for non-owning references.
- void fn(const RcString& str) for owning references.
- void fn(const RcStringOrRef& str) for API surfaces which may extend the lifetime of the RcString. This is typically seen at public API surfaces.
For common string helpers, see "donner/base/StringUtils.h":
- StringUtils::EqualsLowercase("ExAmPle", "example") - to compare a string against a lowercase string.
- StringUtils::StartsWith("Example", "Ex") - to check if a string starts with a prefix.
- StringUtils::EndsWith("Example", "ple") - to check if a string ends with a suffix.
Case-insensitive matching is configurable:
- StringUtils::StartsWith<StringComparison::IgnoreCase>("Hello", "he")
Example:
for (std::string_view part : StringUtils::Split("a,b,c", ',')) {
}
Limit Use of auto
auto should only be used when the type is visible on the same source line, or if the type is well-understood, such as for iterators (auto it = ...).
Exceptions:
For std::optional types from function calls, assuming the code remains readable.
if (auto maybeUnit = parseUnit(str, &charsConsumed)) {
if (charsConsumed == str.size()) {
return maybeUnit;
}
}
For ParseResult<Type> types, assuming the type is visible nearby, and the ParseResult pattern is ubiquitous in the codebase.
auto maybeResult = NumberParser::Parse(remaining_);
if (maybeResult.hasError()) {
return err;
}
const NumberParser::Result& result = maybeResult.result();
Error context for a failed parse, such as the error reason, line, and character offset.
Definition ParseError.h:14
FileOffset location
Location of the error, containing a character offset and optional line number.
Definition ParseError.h:19
Operator Overloading and Comparisons
Use of operator<=> When Possible
- Use the C++20 spaceship operator (operator<=>) when possible for automatic generation of comparison operators.
- Be aware that operator<=> may not work as intended with pointers or when custom comparison logic is needed.
- Note: Due to a bug in gtest, operator== must also be supplied when using operator<=>.
Writing Custom Comparison Operators
- When the default behavior of operator<=> is not suitable (e.g., when comparing pointers or references), explicitly define comparison operators.
- Ensure that the comparison logic accurately reflects the semantics of the class.
Example:
constexpr friend bool operator==(const OptionalRef& lhs, const OptionalRef& rhs) {
return lhs.hasValue() == rhs.hasValue() &&
(!lhs.hasValue() || lhs.value() == rhs.value());
}
Providing operator<< for Debugging
- Implement operator<< for classes to streamline debugging and logging.
- The output should be clear and provide meaningful information about the object's state.
Example:
friend std::ostream& operator<<(std::ostream& os, const OptionalRef& opt) {
if (opt) {
return os << *opt;
} else {
return os << "nullopt";
}
}
Use of constexpr
- Use constexpr for functions and methods that can be evaluated at compile-time.
- This can improve performance and enable compile-time checks.
- Apply constexpr to constructors and operators where appropriate.
Example:
constexpr OptionalRef() noexcept = default;
constexpr OptionalRef(const OptionalRef& other) noexcept = default;
Asserting Preconditions
- Use assert statements to enforce preconditions in debug builds.
- Ensure that assert messages are informative.
Example:
constexpr const T& value() const {
assert(ptr_ && "OptionalRef::value() called on empty OptionalRef");
return *ptr_;
}
To assert in release builds, use UTILS_RELEASE_ASSERT or UTILS_RELEASE_ASSERT_MSG.
#define UTILS_RELEASE_ASSERT_MSG(x, msg)
An assert that evaluates on both release and debug builds and errors with the provided msg.
Definition Utils.h:102
Method Naming Conventions
- Class Methods: Use lowerCamelCase, aligning with the SVG standard.
- Examples: hasValue(), reset(), value()
Implicit Constructors
- Use /* implicit */ comment to indicate intentional implicit constructors, especially when they take a single argument.
- This clarifies the intent and aids in code reviews.
Example:
OptionalRef(std::nullopt_t nullopt) noexcept {}
General Coding Practices
- Be cautious when dealing with raw pointers and references.
- Ensure that lifetime and ownership semantics are clear and properly documented.
- When overloading operators, make sure they are consistent with the class's semantics and do not introduce unexpected behavior.
- Maintain consistency in naming, formatting, and code structure throughout the codebase.