Migration Notes
Mr.Docs takes a different shape from other tools. Most of the differences come from one design choice:
Mr.Docs only works with valid C++ code.
It uses the compilation database and Clang to parse the source as the compiler does, so symbols come from a real AST rather than from text. The rest of this page covers what those changes imply for a project migrating to Mr.Docs.
In this page, we
-
Explore the workflow in more detail,
-
Contrast it with the workarounds other tools require for common C++ idioms,
-
Cover how to handle dependencies in the documentation, including system headers.
Mr.Docs workflow
By the time you reach this page, you have already seen a documented project running in Basic Usage Patterns. The diagram below traces the workflow: how the configuration, the source, and the doc comments come together into a corpus that the generators turn into rendered output.
The four steps are:
-
You provide a configuration file (or command-line arguments) that point at the source code and pick the output format.
-
The configuration defines a compilation database Mr.Docs reads to drive Clang. The Usage patterns section walks the three ways to supply that database.
-
Clang extracts every symbol with its doc comments into one corpus.
-
A generator turns the corpus into rendered output.
Two design choices follow from this workflow: Mr.Docs needs to parse the code (not just read it as text), and the result is a symbol corpus rather than documents.
Idioms without workarounds
Several C++ idioms can’t be expressed directly in the language. In tools such as Doxygen, projects work around this with a preprocessor macro that swaps in a documentation-only declaration when the tool runs. The source then carries two versions of each affected symbol. Mr.Docs reads the real source and lets the documentation and configuration options express the intent, which removes the need for that macro layer.
Example 1: implementation-defined symbols
A public function returns an opaque, implementation-detail type that callers should not name. The real type lives in a detail header somewhere else in the project tree.
-
Mr.Docs
-
Other tools
The function is declared with its real return type. The configuration marks the symbol as implementation-defined, so the rendered synopsis shows the placeholder, and the type itself gets no documentation page:
include/logr/detail/scope_token.hppnamespace logr::detail {
struct scope_token { ~scope_token(); };
}
include/logr/log.hppnamespace logr {
/** Attach a key/value pair to every log line in the current scope.
The context stays attached until the returned token is destroyed.
Hold it in `auto`; nested contexts stack and unwind in reverse order.
@par Example
@code
void handle_request(request const& req) {
auto ctx = logr::scoped_context("request_id", req.id);
do_work(req);
}
@endcode
@param key The context name surfaced on each log line.
@param value The context value surfaced on each log line.
@return An opaque RAII token whose lifetime governs how long the pair
stays attached. The type is implementation-defined; store it
in `auto`.
*/
detail::scope_token scoped_context(char const* key, char const* value);
}
docs/mrdocs.ymlimplementation-defined:
- 'logr::detail::**'
logr::scoped_context
Attach a key/value pair to every log line in the current scope.
Synopsis
Declared in <logr/log.hpp>
/* implementation-defined */
scoped_context(
char const* key,
char const* value);
Description
The context stays attached until the returned token is destroyed. Hold it in auto; nested contexts stack and unwind in reverse order.
Example
void handle_request(request const& req) {
auto ctx = logr::scoped_context("request_id", req.id);
do_work(req);
}
Return Value
An opaque RAII token whose lifetime governs how long the pair stays attached. The type is implementation‐defined; store it in auto.
Parameters
Name |
Description |
key |
The context name surfaced on each log line. |
value |
The context value surfaced on each log line. |
The concrete type is wrapped in a preprocessor conditional so the documentation tool doesn’t see it:
include/logr/detail/scope_token.hpp#if !defined(LOGR_DOCUMENTATION_BUILD)
namespace logr::detail {
struct scope_token { ~scope_token(); };
}
#endif
The public function splits its declaration into a documentation branch, with a placeholder for the return type that won’t parse, and a real branch the compiler sees. This relies on the C++ parser not recognizing the placeholder as invalid code:
include/logr/log.hpp#include <logr/detail/scope_token.hpp>
namespace logr {
#if defined(LOGR_DOCUMENTATION_BUILD)
/** Attach a key/value pair to every log line in the current scope.
The context stays attached until the returned token is destroyed.
Hold it in `auto`; nested contexts stack and unwind in reverse order.
@par Example
@code
void handle_request(request const& req) {
auto ctx = logr::scoped_context("request_id", req.id);
do_work(req);
}
@endcode
@param key The context name surfaced on each log line.
@param value The context value surfaced on each log line.
@return An opaque RAII token whose lifetime governs how long the pair
stays attached. The type is implementation-defined; store it
in `auto`.
*/
__implementation_defined__ scoped_context(char const* key, char const* value);
#else
detail::scope_token scoped_context(char const* key, char const* value);
#endif
} // namespace logr
The project’s Doxyfile defines the documentation-mode macro so the documented branch is what the tool reads:
DoxyfilePREDEFINED = LOGR_DOCUMENTATION_BUILD=1
|
The This means the macro workarounds in the "Other tools" tabs would still work in Mr.Docs as long as both branches parse as valid C++ by literally defining an |
Example 2: "see-below" types
The type is part of the public API, and callers may need to name it, but the documentation should hide its members or definition, as we don’t want to list them exhaustively for this symbol. The user can hold an instance, pass it around, or feed it to other declarations. The synopsis page should show "see below" instead of listing the struct body.
-
Mr.Docs
-
Other tools
The type is declared normally and tagged with the @seebelow doc command. The synopsis is rendered as "see below" and the members are omitted, but the type itself remains a documented symbol in the corpus. No configuration entry is needed; the command on the symbol is enough:
include/netz/buffer_view.hppnamespace netz {
/** A non-owning view of a contiguous byte range.
Used as the parameter and return type for the framing helpers in
`netz::stream` and as the buffer surface presented to async read
handlers. Stores a pointer and a length, and does not extend the
lifetime of the storage it refers to.
*/
class buffer_view
{
public:
unsigned char const* data() const noexcept;
unsigned long size() const noexcept;
};
}
include/netz/stream.hppnamespace netz {
/** Locate the first complete packet at the start of the buffer.
Scans `input` for a frame boundary. The returned view aliases the
same memory as `input`, so the storage behind `input` must outlive
the returned view.
@param input Bytes received from the stream, up to the caller's
current fill mark.
@return A view of the bytes belonging to the first packet, or an
empty view if no complete packet is present yet.
*/
buffer_view first_packet(buffer_view input);
}
|
The |
netz::buffer_view
A non‐owning view of a contiguous byte range.
Synopsis
Declared in <netz/buffer_view.hpp>
class buffer_view { /* see-below */ };
Description
Used as the parameter and return type for the framing helpers in netz::stream and as the buffer surface presented to async read handlers. Stores a pointer and a length, and does not extend the lifetime of the storage it refers to.
netz::first_packet
Locate the first complete packet at the start of the buffer.
Synopsis
Declared in <netz/stream.hpp>
buffer_view
first_packet(buffer_view input);
Description
Scans input for a frame boundary. The returned view aliases the same memory as input, so the storage behind input must outlive the returned view.
Return Value
A view of the bytes belonging to the first packet, or an empty view if no complete packet is present yet.
Parameters
Name |
Description |
input |
Bytes received from the stream, up to the caller's current fill mark. |
The type is declared normally so that its page stays in the documentation. The public function’s declaration swaps in a see_below placeholder when the documentation tool is running:
include/netz/buffer_view.hppnamespace netz {
/** A non-owning view of a contiguous byte range.
Used as the parameter and return type for the framing helpers in
`netz::stream` and as the buffer surface presented to async read
handlers. Stores a pointer and a length, and does not extend the
lifetime of the storage it refers to.
*/
class buffer_view
{
public:
unsigned char const* data() const noexcept;
unsigned long size() const noexcept;
};
}
include/netz/stream.hpp#include <netz/buffer_view.hpp>
namespace netz {
#if defined(NETZ_DOCUMENTATION_BUILD)
/** Locate the first complete packet at the start of the buffer.
Scans `input` for a frame boundary. The returned view aliases the
same memory as `input`, so the storage behind `input` must outlive
the returned view.
@param input Bytes received from the stream, up to the caller's
current fill mark.
@return A view of the bytes belonging to the first packet, or an
empty view if no complete packet is present yet.
*/
__see_below__ first_packet(__see_below__ input);
#else
buffer_view first_packet(buffer_view input);
#endif
} // namespace netz
DoxyfilePREDEFINED = NETZ_DOCUMENTATION_BUILD=1
Example 3: algorithm function objects
A library exposes algorithm function objects (informally known as niebloids): a symbol callers use like a function, but that is implemented as a constexpr variable of a functor record type. The variable inhibits Argument-Dependent Lookup (ADL).
-
Mr.Docs
-
Other tools
The algorithm function object is declared as the compiler sees it. Mr.Docs detects the pattern and documents the pattern as a single symbol:
include/numerics/clamp.hppnamespace numerics {
struct clamp_fn
{
/** Constrain a value to the inclusive range `[lo, hi]`.
Returns `lo` when `v < lo`, `hi` when `hi < v`, otherwise `v`.
Behavior is undefined if `hi < lo`.
@tparam T A type comparable with `operator<`.
@param v The value to constrain.
@param lo The inclusive lower bound.
@param hi The inclusive upper bound.
@return A copy of `v`, `lo`, or `hi`, whichever lies within the range.
*/
template <class T>
T operator()(T const& v, T const& lo, T const& hi) const;
};
inline constexpr clamp_fn clamp = {};
}
docs/mrdocs.ymlauto-function-objects: true
|
The |
_fn struct and its constexpr instance merge into a single function entrynumerics::clamp
Constrain a value to the inclusive range [lo, hi].
Synopsis
Declared in <numerics/clamp.hpp>
template<class T>
T
clamp(
T const& v,
T const& lo,
T const& hi);
|
This function is defined as an Algorithm Function Object (AFO). |
Description
Returns lo when v < lo, hi when hi < v, otherwise v. Behavior is undefined if hi < lo.
Return Value
A copy of v, lo, or hi, whichever lies within the range.
Template Parameters
Name |
Description |
T |
A type comparable with |
Parameters
Name |
Description |
v |
The value to constrain. |
lo |
The inclusive lower bound. |
hi |
The inclusive upper bound. |
The documented form is a plain function template the compiler never compiles; the implementation form is the algorithm function object the documentation tool never sees:
include/numerics/clamp.hppnamespace numerics {
#if defined(NUMERICS_DOCUMENTATION_BUILD)
/** Constrain a value to the inclusive range `[lo, hi]`.
Returns `lo` when `v < lo`, `hi` when `hi < v`, otherwise `v`.
Behavior is undefined if `hi < lo`.
@tparam T A type comparable with `operator<`.
@param v The value to constrain.
@param lo The inclusive lower bound.
@param hi The inclusive upper bound.
@return A copy of `v`, `lo`, or `hi`, whichever lies within the range.
*/
template <class T>
T clamp(T const& v, T const& lo, T const& hi);
#else
struct clamp_fn
{
template <class T>
T operator()(T const& v, T const& lo, T const& hi) const;
};
inline constexpr clamp_fn clamp = {};
#endif
} // namespace numerics
DoxyfilePREDEFINED = NUMERICS_DOCUMENTATION_BUILD=1
Example 4: excluded symbols
A library ships an auxiliary namespace (here jzon::extra) with trace, benchmarking, or other secondary utilities that should not appear in the rendered output.
-
Mr.Docs
-
Other tools
The extra:: namespace is declared normally. A single configuration glob excludes everything inside it from the corpus:
include/jzon/extra/utilities.hppnamespace jzon::extra {
// Filtered out: `exclude-symbols: 'jzon::extra::**'` drops these
// symbols by qualified name, so the `jzon::extra` namespace and its
// members never enter the rendered docs.
void enable_trace();
void disable_trace();
}
include/jzon/parser.hppnamespace jzon {
/** Validate that the document is well-formed JSON.
Parses `source` without constructing a value tree, reporting only
whether the input conforms to RFC 8259. Comments and trailing
commas are rejected.
@param source A null-terminated UTF-8 JSON document.
@return `true` if the document is syntactically valid JSON, `false`
if any syntactic error is encountered.
*/
bool validate(char const* source);
}
docs/mrdocs.ymlexclude-symbols:
- 'jzon::extra::**'
extra:: namespace are not included in the corpusjzon::validate
Validate that the document is well‐formed JSON.
Synopsis
Declared in <jzon/parser.hpp>
bool
validate(char const* source);
Description
Parses source without constructing a value tree, reporting only whether the input conforms to RFC 8259. Comments and trailing commas are rejected.
Return Value
true if the document is syntactically valid JSON, false if any syntactic error is encountered.
Parameters
Name |
Description |
source |
A null‐terminated UTF‐8 JSON document. |
Every declaration in the auxiliary namespace is wrapped in a preprocessor conditional the Doxyfile evaluates as "skip":
include/jzon/extra/utilities.hppnamespace jzon::extra {
#if !defined(JZON_DOCUMENTATION_BUILD)
void enable_trace();
void disable_trace();
#endif
} // namespace jzon::extra
The public header is unchanged:
include/jzon/parser.hppnamespace jzon {
/** Validate that the document is well-formed JSON.
Parses `source` without constructing a value tree, reporting only
whether the input conforms to RFC 8259. Comments and trailing
commas are rejected.
@param source A null-terminated UTF-8 JSON document.
@return `true` if the document is syntactically valid JSON, `false`
if any syntactic error is encountered.
*/
bool validate(char const* source);
} // namespace jzon
DoxyfilePREDEFINED = JZON_DOCUMENTATION_BUILD=1
\cond INTERNAL … \endcond blocks are the alternative; the marker still lives next to every internal declaration. Mr.Docs’s glob covers a whole namespace at once, regardless of how the source is split across files.
Many other idioms collapse into a one-line configuration option or a single doc command: exposition-only "see-below" synopses, hidden friend operators, related-symbol grouping, deprecated-but-documented overloads, and so on. The Commands and Options references are the place to look when you find yourself reaching for a preprocessor conditional.
Handling dependencies
Another consequence of Mr.Docs needing valid C++ is that the public headers of every dependency a project’s public API names must be reachable when the documentation builds. Mr.Docs has to resolve each #include and bind every name that appears in a signature; without those headers, the parse fails, and Mr.Docs can’t build the corpus.
|
Documentation builds often run somewhere different from the project’s build: on CI runners, in containers, or on fresh laptops. Some dependencies are impossible to get there (vendor SDKs, private monorepo paths). Others are impractical: an LLVM-scale dependency takes hours to fetch and configure for a documentation build that itself runs in seconds. This section lists several strategies for handling dependencies in the documentation build. |
Find the dependency
This is the most accurate option: Mr.Docs sees the same headers the compiler would. The cost is that the headers must be available in the documentation environment. The project also needs CMake (or another build system Mr.Docs can drive) to surface the include paths.
The starter project at examples/dependencies/find/ is a small httpd library whose public API mentions a Boost.Asio type:
include/httpd/listener.hpp#ifndef HTTPD_LISTENER_HPP
#define HTTPD_LISTENER_HPP
#include <boost/asio/ip/tcp.hpp>
namespace httpd {
/// A TCP listener bound to a specific endpoint.
class Listener
{
public:
/// Bind the listener to the given endpoint.
explicit Listener(boost::asio::ip::tcp::endpoint const& ep);
/// Return the endpoint the listener is bound to.
boost::asio::ip::tcp::endpoint const& endpoint() const;
private:
boost::asio::ip::tcp::endpoint endpoint_;
};
} // namespace httpd
#endif
The CMakeLists.txt calls find_package(Boost …) like any other project. The example ships a small CMake stub for Boost so the build runs unattended; a real project would rely on the system or vendored Boost install:
CMakeLists.txtcmake_minimum_required(VERSION 3.20)
project(httpd LANGUAGES CXX)
find_package(Boost CONFIG REQUIRED)
add_library(httpd src/listener.cpp)
target_include_directories(httpd PUBLIC include)
target_link_libraries(httpd PUBLIC Boost::asio)
The Mr.Docs configuration hands the CMakeLists.txt to Mr.Docs.
docs/mrdocs.yml## Pattern: a CMake project finds Boost via CMake itself.
source-root: ..
compilation-database: ../CMakeLists.txt
|
When the dependency lives somewhere CMake doesn’t already search, point CMake at it through the |
Shim files
When the dependency can’t be made available in the documentation build environment, you can write small shim headers on disk that forward-declare only what the public API mentions, and add the shim’s directory to includes. LLVM as a dependency is the canonical case: it would take hours to fetch and configure LLVM, only to render docs in seconds. The starter project at examples/dependencies/shim-files/ is a small astutil library that depends on llvm::StringRef:
include/astutil/types.hpp#ifndef ASTUTIL_TYPES_HPP
#define ASTUTIL_TYPES_HPP
#include <llvm/ADT/StringRef.h>
namespace astutil {
/// Look up a symbol by its internal id and return its mangled name.
llvm::StringRef const& symbol_name(int symbol_id);
} // namespace astutil
#endif
The shim lives under docs/shims/. One forward declaration is enough because astutil only takes llvm::StringRef through references in its public API:
docs/shims/llvm/ADT/StringRef.h#ifndef LLVM_ADT_STRINGREF_H
#define LLVM_ADT_STRINGREF_H
// Shim of `llvm::StringRef` for documentation builds.
namespace llvm {
class StringRef;
} // namespace llvm
#endif
The Mr.Docs configuration scans the project’s include/ and adds the shim directory to the Clang include path:
docs/mrdocs.yml## Pattern: a scan-mode project ships a shim for the LLVM headers
source-root: ..
input:
- ../include
includes:
- ../include
- shims
file-patterns:
- '*.hpp'
The shim satisfies the #include because it lives on the include path. The real LLVM tree contributes nothing to the rendered documentation; for documenting a public API, that’s usually what you want.
Shim files are the right form when the shim grows beyond a handful of forward declarations or when several projects share the same set of stubs.
Shim snippets
For one-off stubs that don’t deserve their own file, you can describe the shim contents directly in the configuration file with the missing-include-shims option. The key is the include path; the value is the file contents. MrDocs will include that shim in the filesystem as a convenience. The starter project at examples/dependencies/shim-snippets/ is a small logutil library that uses one type from {fmt}:
include/logutil/buffer.hpp#ifndef LOGUTIL_BUFFER_HPP
#define LOGUTIL_BUFFER_HPP
#include <fmt/format.h>
namespace logutil {
/// Append `text` to a {fmt} memory buffer.
void append(fmt::memory_buffer& out, char const* text);
} // namespace logutil
#endif
The Mr.Docs configuration ships the forward declaration directly:
docs/mrdocs.yml## Pattern: a scan-mode project inlines the shim it needs for {fmt}.
source-root: ..
input:
- ../include
includes:
- ../include
file-patterns:
- '*.hpp'
missing-include-shims:
"fmt/format.h": |
namespace fmt { class memory_buffer; }
|
Mr.Docs wraps each shim in an include guard, so repeated |
Accept missing includes
Some dependencies are too large to mirror as shim files (the AWS SDK, a vendored monorepo tree, a generated SDK with hundreds of headers). For many of those, Mr.Docs can accept that certain include paths will be missing in the documentation environment, and keep parsing. Set missing-include-prefixes to the include-path prefixes you want forgiven. The starter project at examples/dependencies/accept-missing/ is a small s3util library that uses the AWS SDK:
include/s3util/uploader.hpp#ifndef S3UTIL_UPLOADER_HPP
#define S3UTIL_UPLOADER_HPP
#include <aws/core/Aws.h>
namespace s3util {
/// Upload the contents of `path` to `bucket`.
void upload(Aws::String const& bucket, char const* path);
} // namespace s3util
#endif
The configuration forgives the entire aws/ prefix:
docs/mrdocs.yml## Pattern: forgives an entire include prefix.
source-root: ..
input:
- ../include
includes:
- ../include
file-patterns:
- '*.hpp'
missing-include-prefixes:
- "aws/"
When Clang reports a missing #include whose path starts with one of those prefixes, Mr.Docs treats the include as satisfied by an empty file instead of failing the parse. The dependency does not need to be present on disk.
|
Very often, a symbol from one of those includes may still be referenced in your public API (say, However, this is a fallback, not a substitute for the shim mechanisms above. When the missing-symbol references are ambiguous, Mr.Docs replays the original errors. For dependencies whose names need controlled disambiguation, mix in a few targeted shims through |
System dependencies
Parallel to the above strategies sits a special case: the C++ standard library, the C standard library, and platform headers like <Windows.h> or <linux/version.h>. These aren’t usually pinned in a build configuration and they don’t appear in the compilation database either.
|
By default, Mr.Docs uses the same headers the compiler does. It probes the configured compiler to discover the system include paths it would use and feeds them to its own parser. The practical consequence is the same constraint a regular build has: every platform on which you want to generate documentation must also be a platform on which your library compiles. If the library has a |
To generate documentation on a platform without compiling on it, the four strategies from Handling dependencies apply unchanged. Add the platform header’s directory to includes, write a shim that defines the few symbols you use, or forgive the prefix with missing-include-prefixes. The same levers that let you document a project against a missing Boost work for a project against a missing <windows.h>.
For the standard libraries specifically, Mr.Docs also ships a self-contained alternative. Two options swap the system-header probe for a bundled set of headers:
-
use-system-stdlib(defaulttrue) makes Clang find libc++ via-stdlib. Set tofalseto use Mr.Docs’s bundled libc++ headers at<mrdocs-root>/share/mrdocs/headers/libcxxinstead. The path is overridable throughstdlib-includes. -
use-system-libc(defaulttrue) makes Clang use the system C standard library and the platform headers it transitively pulls in. Set tofalseto use Mr.Docs’s bundled libc stubs at<mrdocs-root>/share/mrdocs/headers/libc-stubs. The path is overridable throughlibc-includes.
The bundled headers buy portability: the documentation build resolves the same way on every host because Mr.Docs supplies the headers itself, regardless of whether the host has a real <cstdio> or <windows.h>. The cost is that Mr.Docs’s view of the standard library is now its own environment, distinct from any real toolchain. Doc-only divergences between what Mr.Docs sees and what the compiler sees become a class of bug that doesn’t exist when both consult the same headers. Use the bundled stubs when the portability benefit is worth that risk.