C++ practices #
Most rules presented here cover the same things as the very good C++ Core Guidelines. Whenever something is not covered or you are in doubt - don’t hesitate to consult it - it is a very good source of information.
Avoid anonymous namespaces #
Anonymous namespaces are very convenient, but makes code unreachable by tests and other code. Some code might only make sense in a given context, but if the functionality is generic, it could be put in a library or suchlike.
Use a named namespace to house free functions rather than relying on a class or struct filled with static functions (or functions that should be static). This approach ensures better organization and clarity.
In addition, private static functions do not need to be declared in the header file when moved out of a class. This reduces unnecessary exposure of implementation details.
Algorithms and data structures #
Algorithms + Data Structures = Programs — Niklaus Wirth, 1976
STL algorithms say what they do, as opposed to hand-made for loops that just show how they are implemented. By doing this, STL algorithms are a way to rise the level of abstraction of the code, to match the one of your calling site. — Jonathan Boccara, 2016
Debugging code is twice as hard as writing the code in the first place. Therefore, if you write code as cleverly as possible, you are, by definition not smart enough to debug it. — Brian W. Kernighan
Data is information, facts etc. An algorithm is code that operates on data.
Programming languages, or their libraries, include thoroughly tested algorithms to handle common data structures.
By properly considering algorithms and data structure, and keeping data separate from code, both code and data become simpler, more reliable, more flexible, and easier to maintain for the next person.
Raw loops are those starting with for, while etc. While there are many options on how to write a loop, readability and maintainability should be the priority.
// Pre-C++11:
for (std::map<KeyType, ValueType>::iterator it = itemsMap.begin(); it != itemsMap.end(); ++it) {
//...
doSomething(*it);
//...
}
for (const auto& item : itemsMap) {
//...
doSomething(item);
//...
}
for (const auto& [name, value] : itemsMap) {
//...
doSomething(name, value);
//...
}
Another way, which can be even better, is to use STL <algorithm> library, which offers a wealth of proven, declarative solutions, e.g.:
// C++17
std::for_each(stuff.begin(), stuff.end(), do_something);
auto result = std::find_if(stuff.begin(), stuff.end(), do_something);
// C++20
std::ranges::for_each(stuff, do_something);
auto result = std::ranges::find_if(stuff, do_something);
Note, STL <algorithm> library can also use lambdas.
Code comments #
Don’t comment bad code—rewrite it. — Brian W. Kernighan and P. J. Plaugher, 1974
Comments are a piece of the program that the computer doesn’t execute. While the intension is to aid comprehension, they have a tendency to get out-of-sync with the actual code it is commenting.
It is preferred that code is self documenting when possible. This can be achieved using good naming and structure.
In some cases, comments can be necessary, to convey information that cannot be described in code. This can be links to bugs that a workaround describe or why edge cases are needed.
Conditionals #
Every branch in code doubles the number of paths and makes the code difficult to debug and maintain.
Simple if else may be better expressed as a ternary _ : _ ? _. There are often ways to avoid else, when possible the resulting code will be better without. Ternary is ok though.
Even modestly complex code in an if or else branch should be
extracted to a function or lambda.
A sequence of conditionals stepping through related variables may indicate code and data conflation.
Can also apply to switch statements.
Complicated if else code might benefit from converting to a state machine.
Ternary operator #
You can reduce six lines:
int r; // can’t be const
if (x == 2) {
r = 2;
} else {
r = 3;
}
to one, with a single assignment, no curly braces, no repetition, and const option:
const int r = x == 2 ? 2 : 3;
This works especially great for simplifying return statements.
If you have more than one ternary it might be worth to extract function that encapsulates the conditional logic:
auto someConditionalLogic = []() {
if (condition1) {
return 1;
} else if (condition2) {
return 2;
} else {
return 3;
}
}
const int r = someConditionalLogic();
Const correctness #
const all of the things. — Jason Turner, 2021
const is a statement of intent that something is to be immutable
(not able to change). Immutability aids reliability and is done using constexpr
This provides compile-time evaluation. Can increase compile time, but speedup runtime. More errors are caught at compile-time.
constexpr is preferable for everything that can be so represented.
Reducing dependencies #
Any source code dependency, no matter where it is, can be inverted — Robert C. (Uncle Bob) Martin
Hard dependencies makes the codebase more entangled, makes changes more difficult, and makes unit testing really difficult.
Three examples of dependencies creeping in:
Application::getSomething(); // or any other singleton
SomeDistantClass thing;
void method(AnotherDistantClass values){}
This does not stand in contrast to code reuse, but it does require care when designing how code is accessing data.
A function which has hard dependencies cannot function, be understood, edited or tested, without the context of its dependencies. Avoiding these types of dependencies without code duplication is worth striving for.
Code and its dependencies are said to be coupled. When different pieces of code have the same dependency, they in turn are coupled to each other.
Required information can be injected via constructor or method parameters.
If it is necessary to introduce external code (e.g. a service object), do so by passing an interface, helper function or similar to avoid coupling.
Even in complex cases where singletons are used we can avoid hard coupling and make unit test a breeze. Example:
// For this example the implementation is included in the class definition
// to simplify the example.
class Example {
public:
using DependencyHelper std::function<std::unique_ptr<Dependency>()>;
Example(
//...
DependencyHelper provide_helper_ = {}
) : provide_helper_(std::move(provide_helper))
{
if (!provide_helper_) {
provide_helper_ = my_namespace::defaultDependencyHelper();
}
};
private:
DependencyHelper provide_helper_;
}
Ideally, all dependencies can be avoided, what dependencies to keep depends on the situation.
If your code needs external data that was not available when calling this code, then there’s likely a better overall design that can be used.
Code that has no hard dependencies is single-purpose, reusable, changeable, and testable. Everything is simpler!
Code design #
Any fool can write code that a computer can understand. Good programmers write code that humans can understand — Martin Fowler
Something well-designed is instantly understandable, and a pleasure to work with. Programming principles developed over the last 50 years ensure well-designed code not only runs well, but is also understandable by humans.
Well-designed code is adaptable, flexible, easily changed to suite new circumstances.
Well-designed code is completely free from hard-coded data.
Understandable code can be more easily evaluated for correctness.
For a novice programmer many of these concepts are probably quite foreign, but with a little study and help from the community and code reviews, better code will ensue.
Enums #
Used correctly, enums are invaluable.
Using enums:
- …specifically
enum classallows strongly typed and scoped enumeration to be created. - …instead of booleans for function arguments makes it easy to understand what the argument means without consulting the documentation or at least the method signature.
- …instead of integers when expressing anything other than numbers.
Using enums to codify data values is strongly discouraged, as enums are best suited for representing fixed, intrinsic states rather than variable data. Instead, use std::unordered_map<> or other data structures that better represent dynamic or data-driven values, offering flexibility and improving maintainability.
Minimize getters and setters #
In object-oriented design, a class should encapsulate behavior, not just data. Frequent use of getters and setters can limit a class’s ability to fully encapsulate its responsibilities and may suggest that the data could be handled differently.
Consider:
- Using a struct for simple data containers with public fields.
- Focusing on methods that represent meaningful actions rather than exposing raw data.
A well-designed class manages its own state and provides behavior, not just access.
Appropriate typing #
Using strings for everything can make code harder to understand and maintain. Use appropriate types to add clarity and structure.
The use of auto #
The use of auto has not yet been discussed in the C++ Core Guidelines (see
the to-do
list).
Below follow general guidelines with examples:
The use of auto is ok if the right-hand side makes clear which type it is #
auto* xyz = new Xyz();
auto xyz = <whatever>_cast<Xyz>(...);
auto xyz = getAnything<Xyz>(...);
Counter examples:
auto value = randomThing.weirdProperty->getValue(); // non-obvious type
auto doStuff() { ... } // auto as return type
auto xyz = 1; // unclear what type of integer
The use of auto is ok if the type is long or verbose #
auto it = foo.begin(); // iterator
auto lambda = [](){...};
The use of auto is ok if redundancy is avoided #
std::unordered_map<std::string, int> map;
for (const auto& [key, value] : map) { ... }
Main code path and indentation #
If you are past three indents you are basically screwed. Time to rewrite. — Linus Torvalds, 1995
Indented code can be difficult to reason about, and fragile.
Main execution path should be the least indented one, i.e. conditions should cover specific cases. Early-Exit should be preferred to prune unwanted execution branches fast.
Example:
if (something) {
doSomething();
if (somethingElse) {
doSomethingElse()
if (somethingElseAgain) {
doThing();
} else {
doDifferent();
}
} else {
doTheOther();
}
} else {
doNothing();
}
Can be changed into:
if (!something) {
doNothing();
return;
}
doSomething();
if (!somethingElse) {
doTheOther();
return;
}
doSomethingElse();
if (!somethingElseAgain) {
doDifferent();
return;
}
doThing();
Initialization #
Initialize all objects, and, if possible, use const or better still, constexpr.
Avoid default constructors. If there is not yet a value for an object, then there’s no need to create it. Declare variables close to where they are used (there’s no need to start the declare variables at the start of the block like in ansi-c). Joining declaration and initialization allows const:
AType thing; // mutable. Does it have a default constructor?
const AType thing3 { calcVal() }; // immutable
Using uninitialized POD type variables is undefined behavior.
It is OK to declare variables inside a loop.
Prefer uniform initialization { } (also called brace initialization), it prevents narrowing.
Clarifies not assignment (=) or function call (()).
Initialize class member variables at declaration, not in constructors:
- Simplifies constructors
- Avoids repetition
- Establishes default state
Static members can be defined in header file, no need to split to source file:
struct Something {
static const int _value_ = 2; // since C++98 for int
static inline std::string _something_ {"str"}; // since C++17
}
Lambdas can create and initialize variables in the capture, removing the need to have the parameter in the surrounding scope. But don’t forget mutable if you want to update its value. The variable stays for the lifetime of the lambda (think static, but better).
Lambdas #
One of the most popular features of Modern C++. — Jonathan Boccara, 2021
A lambda (since C++11) is like a function that can be named, passed into and out of functions. They accept parameters, can be generic (since C++ 14), do a really good job of type deduction (auto), and can be constexpr (since C++17).
Lambdas can capture data from enclosing scopes and enforce encapsulation, simplifying surrounding scopes.
Lambdas are indispensable when breaking up complex code into individual responsibilities, perhaps as a precursor to moving to functions. Ditto when removing repetition.
Whilst lambdas are quite happy with simple auto parameters, best not to omit const or & as appropriate. Types may be required to help the IDE.
Consider the following code:
doSomething()
if (somethingWentWrong()) {
// Clean up this
// ...
// Clean up that
return;
}
doSomethingElse();
if (somethingElseWentWrong()) {
// Clean up this
// ...
// Clean up that
return;
}
doSomeOtherThing();
// Clean up this
// ...
// Clean up that
Using lambdas we can remove code duplication to create the following:
auto cleanup = [](){
// Clean up this
// ...
// Clean up that
};
doSomething()
if (somethingWentWrong()) {
cleanup();
return;
}
doSomethingElse();
if (somethingElseWentWrong()) {
cleanup();
return;
}
doSomeOtherThing();
cleanup();
Avoid macros #
While macros was needed back in the days. Today, with modern c++, that’s usually not the case anymore.
Macros are global, can be lead to unpredicted side effects and are difficult to debug. Consider replacing with a function. For conditional compilation in templates consider constexpr if.
Avoid magic literals #
“Magic” literals placed directly in code offer no clue as to what exactly is being specified or its origin, and no clue if the same data is used elsewhere. Comprehension and maintenance burden.
To document what the magic literal is, use a suitable named constant.
Instead of this:
displayLines(80);
Do the following instead:
constexpr unsigned standardScreenLength {80};
displayLines(standardScreenLength);
Good naming #
Clear, concise, naming makes code understandable.
For an object whose purpose is to do something (service object), prefer a verb. E.g. renderer.
For an object that is something (value object), prefer a noun. E.g. drawing.
Something difficult to name concisely likely does not have a single purpose and needs refactoring.
Use names that are specific. E.g. SaveLogToDisk, not ProcessLog. Process could be anything.
A variable named after its data value defeats the whole point of a variable:
struct Dog {
std::string color {};
};
Dog redDog {"blue"}; // BAD
// 200 lines later, -_obviously_\- redDog is red! He’s blue? WTF?
Dog dog {"pink"}; // OK
constexpr auto redDogColor {"red"}; // OK
See also variable sets.
Out parameters #
Out parameters are non-const, by-reference, or by-pointer, function parameters. These are known to cause hard to find bugs.
Whether values are updated by the function is not obvious.
Where possible make function parameters const or const& and return a value, or return a container to return multiple values.
Return value optimizations (RVO) simplifies return and usually elides copies.
auto func = [](const auto& str, const auto num) {
return { str, num };
};
Structured binding (since C++17) simplifies reading back the result at the calling side:
auto [name, value] = func("qty", 2);
For methods that return multiple values, depending on context, it may be better to provide dedicated struct for result.
Code repetition #
One of the things I’ve been trying to do is look for simpler or rules underpinning good or bad design. I think one of the most valuable rules is to avoid duplication — Martin Fowler
Code duplication is by far one of the worst anti-patterns in software engineering, eventually leading to buggy and unmaintainable systems. — Magnus Stuhr, 2020
Reducing code duplication and similar code also reduces the different paths a program might take during runtime. This in turn makes it easier to debug and test the code base.
Change requires finding every usage (difficult) and replicating the change (error-prone). Failure to catch just one instance creates a nasty bug that might remain undiscovered for a long time. Comprehension requires studying every item. Small differences are notoriously difficult to spot.
Repetition is entirely avoidable.
The variant part (the bit that is different between usages) of repeating code is often just one or two items in a long chain of statement. The variant parts can be extracted and passed as parameters to a function or lambda executing the common body. A sequence of repeated code likely indicates the underlying data is actually a set and hence should be defined in a container and dealt with accordingly.
Static #
Often best avoided. For const variables consider constexpr, or
initialization in lambda capture.
static functions may be better moved out of class into a named namespace or some utility library/file.
Automated testing #
Nothing makes a system more flexible than a suite of tests. Nothing. Good architecture and design are important, but the effect of a robust suite of tests is an order of magnitude greater. It’s so much greater because those tests enable you to improve the design. — Uncle Bob
There are multiple types of automated testing. Regression tests, integration tests. unit tests etc. While there are similarities the idea behind them differs. These are different tools in the toolbox we can use to ensure good quality code.
- Regression tests when bugs are found, tests are created to ensure that the found bug is 1. fixed, and 2. does not happen again.
- Integration tests ensures that multiple components can work together to complete a task.
- Unit tests targets a single behavior of a single unit of code, with a single assertion.
Writing tests on a coupled design is difficult, therefore removing hard dependencies is not just an academic exercise, but it is a great tool to make code testable.
Test driven development, where tests are written before, and during code creation, is a good practice to follow to ensure good quality code. Writing tests afterwards can be difficult as it won’t help you with code design as much when code is produced.
Gui code is often classified as untestable because there are good tools to verify behavior when buttons and sliders are manipulated. And while gui code can’t easily be tested, the logic it is connected to, can. Splitting ui from from “business logic” is a good choice and highly recommended.
Just like good naming is important for variables and functions, naming tests are too. A good test name describes what has been tested. This way, when it fails for someone else, they can easily see what failed.
If code is difficult to unit test consider if it can be extracted from its surrounds — maybe into some library-like class — where it can be tested in isolation. Is it possible to refactor to remove hard-wired dependencies? Is it trying to do too much? Is it too tightly coupled? See also dependencies.
Avoid variable sets #
Representing data with code where related variables have names closely coupled to their initial value should be avoided to reduce repetition and make the code easier to understand and maintain.
For example, instead of this:
Item fred { "Fred", 20 };
Item martha { "Martha", 30 };
Item george { "George", 40 };
Move the data into a container e.g. constexpr std::initializer_list, not a vector or map.
Container elements are typically value, array, pair, tuple, or a defined struct. Pair and tuple should only be used for very short lived data, such as this case:
using Pair = std::pair<std::string_view, size_t>;
constexpr std::initializer_list<Pair> items {
{ "Fred", 20 },
{ "Martha", 30 },
{ "George", 40 }
};
A struct can also be used, which has the advantage of named elements, but is slightly more overhead.
struct Button {
std::string_view name;
size_t height;
size_t width;
};
constexpr std::initializer_list<Button> buttonDefs {
{ "Go", 25, 25 },
{ "Get set", 20, 20 },
{ "On your marks", 15, 15 }
};
When in doubt, use a struct - it is better to have good names than not.
API Documentation #
API Documentation in FreeCAD consists of two main types of documentation. Topics explain the concepts behind the code and API documentation strings explain the code itself. Both are important but topics are especially valued.
Although it is not strictly required, developers are highly encouraged to write API documentation when applicable. Note that it is required to make current API documentation up-to-date with changes that are made to the code. This section presents a set of guidelines for that documentation.
General guidelines #
- Be as complete as possible with each item (classes, enums, public and protected members) in a file having a documentation string.
- Minimize the vertical space. So, when possible, use a single line with
///or///<for enum items (see below). - In general, do not document C++ concepts such as destructors, friends, or constructors, except when there are arguments or if something special happens.
Syntax #
Doxygen supports two different types of syntax for commands, those with a backslash (e.g. \brief) and those with an @ symbol (@brief). The @-prefixed version is more prevalent in the FreeCAD source code and is the preferred style for new documentation.
For a single line API documentation comment, for example commenting a function, use /// <your comment. without using @brief and ending with a full stop. Example:
/// Get the transaction ID of this transaction.
int getID() const;
For multiple lines, use /** to start the docstring. The documentation starts on the next line with a @brief command that ends with a full stop and then a line break. In the case below, there are details after which the @param[in] and @return commands document the parameters and return statement, respectively.
/**
* @brief Check whether the passed name is valid.
*
* If a name is null or an empty string it is considered invalid, and valid
* otherwise.
*
* @param[in] name The name to check.
* @return True if the name is valid, false otherwise.
*/
static bool isValidName(const char* name);
Keep the number of added lines to a minimum. For example, use ///< to keep the comments on the same line:
/// The status of the transaction object.
enum Status
{
New, ///< A new object is added to the document.
Del, ///< An object is deleted from the document.
Chn ///< An object is changed in the document.
} status {New};
Use a single line to separate the documentation of different functions to make clearer to which function the documentation string belongs:
/// Generate a new unique transaction ID.
static int getNewID();
/// Get the last transaction ID.
static int getLastID();
To prevent Doxygen from automatically creating an irrelevant or undesired link use a % prefix. For example, since there is a namespace Base in FreeCAD, use %Base as shown in the example below to prevent Doxygen to create a link here to the Base package:
/**
* @brief %Base class of all properties.
* @ingroup PropertyFramework
*
* This is the base class of all properties. Properties are objects that are
* ...
*/
It is possible to use Markdown syntax in doc comments. For example, a Markdown list:
/**
* @brief A multi index container for holding the property spec.
*
* The multi index has the following index:
* - a sequence, to preserve creation order
* - hash index on property name
* - hash index on property pointer offset
*/
mutable bmi::multi_index_container<
PropertySpec,
bmi::indexed_by<
...
> propertyData;
Placing API documentation #
The main page of the API documentation is in src/Doc/mainpage.dox.in. The most important function of this page is the “Organization of the API Documentation” section allowing readers to quickly browse important topics.
Topics should be defined in dedicated .dox files in the main directory of a package. For example, src/App/core-app.dox defines a group APP that is in group CORE (defined in the main page) and then it defines several topics in APP, such as DocumentGroup (for the concept of a Document in FreeCAD), DocumentObjectGroup (for the concept of a document object), etc. Below are two excerpts from that file to give an indication:
/**
* @defgroup APP App
* @ingroup CORE
* @brief The part of FreeCAD that works without GUI (console or server mode).
*
* It contains the App namespace and defines core concepts such as
* @ref DocumentGroup "Document", @ref DocumentObjectGroup "Document Object",
* @ref PropertyFramework "Property Framework", @ref ExpressionFramework
* "Expression Framework", and the @ref ExtensionFramework "Extension
* Framework".
*
* The largest difference between the functionality in @ref BASE "Base"
* compared to %App is that %App introduces the notion of properties, both used
* ...
*/
/**
* @defgroup DocumentGroup Document
* @ingroup APP
* @brief The class that represents a FreeCAD document
*
* This (besides the App::Application class) is the most important class in FreeCAD.
* It contains all the data of the opened, saved, or newly created FreeCAD Document.
* The App::Document manages the Undo and Redo mechanism and the linking of documents.
* ...
*/
Given such a topic, you can include the most important classes into this topic, for example in App/Document.h class App::Document is included in DocumentGroup.
/**
* @brief A class that represents a FreeCAD document.
*
* A document is a container for all objects that are part of a FreeCAD
* project. Besides managing the objects, it also maintains properties itself.
* As such, it is also a PropertyContainer.
*
* @ingroup DocumentGroup
* For a more high-level discussion see the topic @ref DocumentGroup "Document".
*/
class AppExport Document: public PropertyContainer
{
...
Documentation strings can be added to classes, enums, and public and protected members of a class. This is especially important if a class is at the top of a hierarchy and its methods are reused in many other subclasses. The documentation should be added in the header file and not in the C++ file.
Specific guidelines #
This section contains guidelines for specific types of documentation.
Namespaces #
Since namespace statements are typically in many files, it is difficult to determine where to put documentation strings. In most cases a best practice is to add them to the .dox file of the respective module. For example, in src/App/core-app.dox:
/**
* @namespace App
* @ingroup APP
* @brief The namespace for the part of FreeCAD that works without GUI.
*
* This namespace includes %Application services of FreeCAD that such as:
* - The Application class
* - The Document class
* - The DocumentObject classes
* - The Expression classes
* - The Property classes
*
* For a more high-level discussion see the topic @ref APP "App".
*/
Classes #
For classes use a brief statement and more detailed comments:
/**
* @brief A class that represents a FreeCAD document.
*
* A document is a container for all objects that are part of a FreeCAD
* project. Besides managing the objects, it also maintains properties itself.
* As such, it is also a PropertyContainer.
*
* ...
*/
class AppExport Document: public PropertyContainer
{
...
Functions #
For functions, use a brief statement, optionally more detailed comments, and if applicable and the parameters are not trivial, @param statements marking the direction [in], [out], or [in,out], a @return command and @throws command, if applicable. An example:
/**
* @brief Get the value of the property identified by the path.
*
* This function gets the value of the property identified by the path. It
* is meant to be overridden for subclasses in which the `path` is
* typically ignored. The default implementation makes use of the `path`
* ObjectIdentifier to get the value of the property.
*
* @param[in] path The path to the property.
* @return The value of the property.
*/
virtual const boost::any getPathValue(const App::ObjectIdentifier& path) const;
Enums #
For enumerations, it often suffices to have a brief statement and a short comment for each item:
/// The status of the transaction object.
enum Status
{
New, ///< A new object is added to the document.
Del, ///< An object is deleted from the document.
Chn ///< An object is changed in the document.
} status {New};
Grouping #
Special documentation comments can be used to indicate that some group of functions, variables, etc. are all part of some single conceptual grouping. A best practice is to start a group with a documentation block /** with an @name command and an @{ grouping command on the next line. The group is ended with /// @}.
Often, FreeCAD source files contain grouping commands with normal comments instead of documentation comments, such as // @{ and // @}. Since these comments are not documentation comments, this usage is incorrect and can lead to issues in rendering the documentation. This usage should be avoided in new code, and corrected when encountered.
/**
* @name Properties
* @{
*/
/// The long name of the document (utf-8 coded).
PropertyString Label;
/// The fully qualified (with path) file name (utf-8 coded).
PropertyString FileName;
...
/// Whether to use hasher on topological naming.
PropertyBool UseHasher;
/// @}
Templates #
Templates can be document with a @tparam command:
/**
* @brief Get all objects of a given type.
*
* @tparam T The type to search for.
* @return A vector of objects of the given type.
*/
template<typename T>
inline std::vector<T*> getObjectsOfType() const;
Documentation with much repetition #
The preferred way to handle code with significant repetition is to use @copydoc or @copydetails.
/**
* @brief Check if this mapped name starts with the search target.
*
* If there is a postfix, only the postfix is considered. If not, then only
* the data is considered. A search string that overlaps the two will not
* be found.
*
* @param[in] searchTarget The search target to match.
* @param[in] offset An offset to perform the match at.
*
* @return True if this MappedName begins with the target bytes.
*/
bool startsWith(const QByteArray& searchTarget, int offset = 0) const;
/// @copydoc startsWith(const QByteArray&,int) const
bool startsWith(const char* searchTarget, int offset = 0) const;
/// @copydoc startsWith(const QByteArray&,int) const
bool startsWith(const std::string& searchTarget, int offset = 0) const;
Another example that adds a parameter:
/**
* @brief Convert the expression to a string.
*
* @param[in] persistent If true, the string representation is persistent
* and can be saved to a file.
* @param[in] checkPriority If true, check whether the expression requires
* parentheses based on operator priority.
* @param[in] indent The indentation level for pretty-printing.
*
* @return The string representation of the expression.
*/
std::string toString(bool persistent = false, bool checkPriority = false, int indent = 0) const;
/**
* @brief Write a string representation of the expression to a stream.
*
* @param[in,out] os The output stream to write to.
* @copydoc Expression::toString(bool, bool, int) const
*/
void toString(std::ostream &os, bool persistent=false, bool checkPriority=false, int indent=0) const;
An example with @copydetails:
/**
* @brief Register an import filetype given a filter.
*
* An example of a filter is: @c "STEP with colors (*.step *.STEP *.stp *.STP)".
* An example of a module is: @c "Import" or @c "Mesh".
*
* @param[in] filter The filter that describes the file type and extensions.
* @param[in] moduleName The module name that can handle this file type.
*/
void addImportType(const char* filter, const char* moduleName);
/**
* @brief Register an export filetype given a filter.
*
* @copydetails addImportType
*/
void addExportType(const char* filter, const char* moduleName);
Another way to handle documentation with a high degree of repetition is by grouping, although in most cases it is a best practice to use @copydoc or @copydetails instead.
/**
* @name PropertyData accessors
*
* @details These methods are used to access the property data based on:
* - the offset base,
* - either:
* - a property pointer, or
* - a property name.
* @{
*/
const char* getName (OffsetBase offsetBase,const Property* prop) const;
short getType (OffsetBase offsetBase,const Property* prop) const;
short getType (OffsetBase offsetBase,const char* name) const;
const char* getGroup (OffsetBase offsetBase,const char* name) const;
const char* getGroup (OffsetBase offsetBase,const Property* prop) const;
const char* getDocumentation(OffsetBase offsetBase,const char* name) const;
const char* getDocumentation(OffsetBase offsetBase,const Property* prop) const;
/// @}