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

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.

graph TD %% Define styles for visual clarity classDef input fill:#D1E8FF,stroke:#005CFF,stroke-width:2; classDef artifact fill:#FFF5D1,stroke:#FFA500,stroke-width:2; classDef output fill:#D1FFD1,stroke:#008000,stroke-width:2; %% Define Inputs subgraph Inputs CF[Configuration File] CL[Command Line Arguments] end class CL,CF input %% Define Artifacts subgraph Processes P[Configuration Options] CD[Compilation Database] C[Corpus] G[Generator] end class P artifact class CD,C,G artifact %% Define Outputs subgraph Outputs D[Documentation] end class D output %% Relationships CF -->|Define| P CL -->|Define| P P -->|Defines| CD CD -->|Extract Symbols| C C -->|Feeds| G G -->|Produces| D %% Highlight dependencies for clarity P -->|Influences| G

The four steps are:

  1. You provide a configuration file (or command-line arguments) that point at the source code and pick the output format.

  2. The configuration defines a compilation database Mr.Docs reads to drive Clang. The Usage patterns section walks the three ways to supply that database.

  3. Clang extracts every symbol with its doc comments into one corpus.

  4. 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.hpp
namespace logr::detail {
struct scope_token { ~scope_token(); };
}
include/logr/log.hpp
namespace 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.yml
implementation-defined:
  - 'logr::detail::**'
Preview·Synopsis hides the detail type behind a placeholder, and the detail type has no page
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:

Doxyfile
PREDEFINED = LOGR_DOCUMENTATION_BUILD=1

The __MRDOCS__ macro is also available, as Mr.Docs’s equivalent of the project-wide doc-mode macros that other tools rely on. You can also use the defines option in the mrdocs.yml file to define extra macros. These macros are defined whenever Mr.Docs is parsing, so a header can switch declarations conditionally when a documentation build needs a different one.

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 _implementation_defined_ excluded type. However, the Mr.Docs configuration in the corresponding tabs is the idiomatic, recommended approach because it avoids duplicate declarations and the project-wide doc-mode macro.

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.hpp
namespace 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
namespace 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 see-below configuration option also accepts a glob of symbol names for cases where the doc-command treatment is not practical, for example when the same rule has to apply across many types or when the source can’t be changed. The @implementationdefined command and the matching implementation-defined configuration option work the same way.

Preview·The type keeps its page, but the members collapse into a "see below" placeholder
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.hpp
namespace 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
Doxyfile
PREDEFINED = 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.hpp
namespace 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.yml
auto-function-objects: true

The auto-function-objects option tags variables as algorithm function objects automatically when the pattern is unambiguous. For records that fall outside the heuristic or to have more control over the feature, apply the @functionobject doc command directly to the variable to force the treatment.

Preview·The _fn struct and its constexpr instance merge into a single function entry
numerics::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 operator<.

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.hpp
namespace 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
Doxyfile
PREDEFINED = 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.hpp
namespace 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.hpp
namespace 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.yml
exclude-symbols:
  - 'jzon::extra::**'
Preview·The symbols in the extra:: namespace are not included in the corpus
jzon::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.hpp
namespace 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.hpp
namespace 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
Doxyfile
PREDEFINED = 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.txt
cmake_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 cmake: option, the CMAKE_PREFIX_PATH variable, or the <PackageName>_ROOT variables.

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 #include directives are safe.

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, Aws::String appearing in a parameter type). When the reference is unambiguous (a type or a namespace), Mr.Docs considers that symbol to be an external declaration or namespace.

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 missing-include-shims.

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 #include <windows.h> guarded behind #ifdef _WIN32, you can only document that branch from a machine that has the Windows SDK on disk.

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 (default true) makes Clang find libc++ via -stdlib. Set to false to use Mr.Docs’s bundled libc++ headers at <mrdocs-root>/share/mrdocs/headers/libcxx instead. The path is overridable through stdlib-includes.

  • use-system-libc (default true) makes Clang use the system C standard library and the platform headers it transitively pulls in. Set to false to use Mr.Docs’s bundled libc stubs at <mrdocs-root>/share/mrdocs/headers/libc-stubs. The path is overridable through libc-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.