Mastering Doxygen

My hope for this document is that it will provide you with enough information if you are just getting started with documenting a C++ project. I make no claims to this being all you will need, the information presented here comes from a lot of trial and error and arriving at a workflow that seemed to work out. Your mileage will vary, things will break, but if you are patient (and belligerent) enough, the end result is totally worth it. With that, let’s begin!

What is Doxygen, and How do I Approach it?

Doxygen is a documentation (doxy) generation (gen) system. You should approach it with fear, awe, and humility. And remember to never look it in the eyes.

Doxygen on its own is a fascinating tool. It’s stupendously flexible, and immensely powerful. I mean let’s think about what it’s actually doing: it’s parsing and extracting documentation from C++ (among other possible languages), which in its own right cannot even be parsed using pushdown automata. Read this amusing SO answer for why it’s so complex!

With a little appreciation for what Doxygen is actually doing for you, don’t take for granted that it likely isn’t going to work perfectly out-of-the-box. It will get most things right with almost no effort! But if your code is complicated, expect failure, taking solace in the fact that Doxygen’s exceptional flexibility allows for you to fix the errors.

Recipe for Success
  1. Start the documentation as early as you can. Don’t document it once you want to release.
  2. Since you’re reading this page, I’d say the easiest thing to “just get the docs going” would be to use the STDIN approach provided by Exhale.
  3. When things aren’t appearing correctly, add the line GENERATE_HTML = YES to your input to exhaleDoxygenStdin and look at the actual Doxygen HTML pages.
    • If it is broken there, there’s no way it’s going to work with Exhale.
    • If it is working there but not with Exhale, you at least have a starting point.
  4. DO NOT IGNORE THE ERRORS FROM DOXYGEN. I’ve never understood why people think that it’s ok for their documentation build to be emitting hundreds of warnings. Fix them, or live with it. The choice is yours.
Recipe for Failure
  1. Assume that you can just plug-and-play and never read any documentation on the documentation systems.
  2. Ignore the warnings and errors from Doxygen, Breathe, Exhale, and Sphinx.

Crash Course on Documentation with Doxygen

There is a lot to make sure you do in terms of the documentation you write in a C++ file to make Doxygen work. First and foremost, there is a comprehensive Doxygen manual that describes anything and everything. Depending on the kind of person you are, browsing the manual may be the best option. I personally went the “hardcore” (aka overwhelming) approach of just reading the entire generated Doxyfile. You can acquire a shiny new Doxyfile by executing doxygen -g in your terminal in a directory where there is no Doxyfile present.

Tip

If you take this approach, open the Doxyfile in a text editor and view it with make syntax. That will at least make it bearable.

The Core Variables

Amidst all of the options, there are really only a handful that you need to get things started.

INPUT = ../some/path

You need to tell Doxygen where to look for your code! This can be either a relative or absolute path. Relative paths are preferred, because an absolute path means you will be the only person who can actually build the documentation. Where relative paths are concerned, these are relative to where Doxygen executes from.

In pure Doxygen, this is typically where the Doxyfile is. In Exhale, these are relative to where conf.py is.

Note

Where Exhale is concerned, this is the only required Doxygen configuration when using exhaleDoxygenStdin.

Tip

Exhale sets all of these for you, they are described here for you to know what they are doing.

OUTPUT_DIRECTORY = ./a/different/path

This tells Doxygen where to store the output of the documentation it is generating. Supposing you specified OUTPUT_DIRECTORY = ./doxyoutput, and you specified to Doxygen GENERATE_HTML = YES, GENERATE_LATEX = YES, and GENERATE_XML = YES, it would create the folder ./doxyoutput, with subdirectories such as html or xml.

For Exhale, since you already needed to supply the path to the xml output directory for Breathe, this configuration is inferred. Or rather, Exhale searches for OUTPUT_DIRECTORY when using exhaleDoxygenStdin and raises an exception if it is found.

RECURSIVE = YES
Assuming your project has more than one directory, you specify INPUT to be the top-level of where your header files are, and setting this to YES tells Doxygen to recurse the directory structure.
FULL_PATH_NAMES = YES
In pure Doxygen, you may not want this. In Exhale, you always do. When set to NO, Doxygen performs some clever renaming, and discards all parts of paths that can be removed while still keeping files unique. The consequence for Exhale is that when this is done, there is no way to know the original directory structure.
STRIP_FROM_PATH = ../some/path

However, if you ask for FULL_PATH_NAMES, you will be displeased by the results. This variable informs Doxygen to strip out a common prefix path from all the paths generated in the documentation.

Warning

Exhale requires that you specify this variable through exhale_args. If it is detected in the input to exhaleDoxygenStdin, an exception is raised. This is a detail specific to hosting on Read the Docs that in all honesty I’ve never found the cause of. It likely has to do with the environment setup.

So in recap, really the only required variables you need to give are INPUT and OUTPUT_DIRECTORY. I highlight the above variables to indicate what the defaults Exhale expects out of your configuration.

Additional Variables with Important Impacts

ALIASES

In particular, the two aliases Exhale provides come from Breathe, and allow you to wield full-blown reStructuredText (including directives, grid tables, and more) in a “verbatim” environment. The aliases as sent to Doxygen:

# Allow for rst directives and advanced functions e.g. grid tables
ALIASES  = "rst=\verbatim embed:rst:leading-asterisk"
ALIASES += "endrst=\endverbatim"

This allows you to do something like this in your code:

/**
 * \file
 *
 * \brief This file does not even exist in the real world.
 *
 * \rst
 * There is a :math:`N^2` environment for reStructuredText!
 *
 * +-------------------+-------------------+
 * | Grid Tables       | Are Beautiful     |
 * +===================+===================+
 * | Easy to read      | In code and docs  |
 * +-------------------+-------------------+
 * | Exceptionally flexible and powerful   |
 * +-------+-------+-------+-------+-------+
 * | Col 1 | Col 2 | Col 3 | Col 4 | Col 5 |
 * +-------+-------+-------+-------+-------+
 *
 * \endrst
 */

Note

This \rst environment is actually quite useful as an override. Doxygen by default enables Markdown. For the most part, you can ignore this, but in the times where Markdown and reStructuredText create conflicts, being able to force reStructuredText is the only solution.

ENABLE_PREPROCESSING = YES
Its rather unlikely you will ever get a full C++ project to produce the expected documentation without using the preprocessor.
MACRO_EXPANSION = YES
Similarly, if you use macros you’ll want to make sure that Doxygen expands them.
SKIP_FUNCTION_MACROS = NO
Though it is not always capable of actually doing the macros, try and let Doxygen’s preprocessor do what it can.
EXPAND_ONLY_PREDEF = NO
Unless you want to enumerate every single preprocessor constant / macro expansion, tell Doxygen to try and expand everything it can.
PREDEFINED

Exhale adds the following two predefined preprocessor symbols:

# extra defs for to help with building the _right_ version of the docs
PREDEFINED  = DOXYGEN_DOCUMENTATION_BUILD
PREDEFINED += DOXYGEN_SHOULD_SKIP_THIS

These are useful for when you either have code that is breaking the Doxygen documentation (e.g. heavy templating / metaprogramming), or need to control the compilation trajectory to where a docstring lives. For example

#if !defined(DOXYGEN_SHOULD_SKIP_THIS)
    // forward declarations in particular will make Doxygen think that the
    // class is defined in a different file!
    class Forward;
    struct Declaration;
#endif // DOXYGEN_SHOULD_SKIP_THIS

// platform specific code
#if defined(__APPLE__) || defined(DOXYGEN_DOCUMENTATION_BUILD)
    /// This method is only needed on Apple
    void they_think_they_are_special();

    /**
     * This definition changes depending on the platform, but we can just
     * document it once.
     *
     * - Apple: ``12``
     * - Linux: ``21``
     * - Windows: ``0``
     */
    #define SOME_CONSTANT 12
#elif defined(__linux__)
    #define SOME_CONSTANT 21
#else
    #define SOME_CONSTANT 0
#endif

If / when the Doxygen preprocessor is not expanding things correctly, use this list to predefine what things should be expanding to. For example, a macro I like to use originally taken from Wenzel Jakob’s NanoGUI for making namespaces a little more readable looks like this:

#define NAMESPACE_BEGIN(name) namespace name {
#define NAMESPACE_END(name) }

Doxygen gets confused by this, but for say namespace nanogui we can just predefine it for Doxygen:

PREDEFINED += NAMESPACE_BEGIN(nanogui)="namespace nanogui {"
PREDEFINED += NAMESPACE_END(nanogui)="}"

Adding Documentation to the Code

See the Doxygen docblocks documentation for all of the different options you have at your disposal. I’ll call attention to a couple of useful commands commonly used in documenting specific aspects:

Doxygen Command Doxygen Documentation Action
\ref Add link to another item being documented.
\brief Add brief documentation to a given construct.
\param Add documentation for a specific parameter.
\tparam Add documentation for a specific template parameter.
\throw Add documentation for a specific exception that can be thrown.
\return Add documentation for the return value.
Explicit Control Over Contstructs (e.g., Adding Documentation Apart from Definition )
\struct To document a struct.
\union To document a union.
\enum To document an enum type.
\fn To document a function.
\var To document a variable or typedef or enum value.
\def To document a #define.
\typedef To document a typedef.
\file To document a file.
\namespace To document a namespace.
Inline Formatting (see File and Namespace Level Documentation in Exhale)
\b Bold a single word (e.g. \b bold).
\em Emphasize a single word (e.g. \em emphasis).
\c Teletype a single word (e.g. \c computeroutput).

Doxygen Documentation Pitfalls

File Documentation is Necessary for More than just Files!

If you want a file documented, you must have \file somewhere in a documentation string in the file. However, if you want something like an enum or define to show up in the documentation, you must document the file (even if the file level documentation is empty)! From the Doxygen documentation reiteration:

Let’s repeat that, because it is often overlooked: to document global objects (functions, typedefs, enum, macros, etc), you must document the file in which they are defined.

Associating Documentation with the Right File

Classes, Structs, Enums, and Unions typically need additional care in order for them to appear in the hierarchy correctly. If you have a file in a directory, the Doxygen FAQ explains that you need to specify this location:

You can also document your class as follows:

/**
 * \class MyClassName include.h path/include.h
 *
 *  Docs for MyClassName
 */

So a minimal working example of the file directory/file.h defining struct thing might look like:

#pragma once
/** \file */

/**
 * \struct thing file.h directory/file.h
 *
 * \brief The documentation about the thing.
 */
 struct thing {
    /// The thing that makes the thing.
    thing() {}
 };

Features Available by Using Sphinx / Breathe / Exhale by way of reStructuredText

Especially if you already know Markdown, reStructuredText syntax can be a little frustrating. I love both equally for different reasons, but certain actions had to take place in writing Exhale that necessitate using reStructuredText. The following is a mini-guide on the syntax, with links to more resources.

Basic Formatting

Bold Text
Bold text is done with two asterisks: **bold**.
Italic Text

Italic text is done with one asterisk: *italic*.

Danger

Unlike most Markdown parsers, _italic_ with underscores is not going to work. It has to do with how hyperlinks work.

Teletype Text

Teletype text is done with two backticks: ``teletype text``

Danger

Single backticks will not do teletype text! This also has to do with how hyperlinks in reStructuredText work.

Listings

See the official documentation.

Tables

Tip

Everything from here on may cause issues with Doxygen. Use the \rst verbatim environment described in the Doxygen Aliases section.

Use grid tables!!!

Useful Directives

reStructuredText is particularly sensitive to whitespace. Where directives are concerned, it may be uncomfortable for you but you actually indent by three spaces. The reason is simple: it lines up visually.

Every directive starts with two ., followed by a single space, then the directive, followed by two :. So it looks like this:

.. directive:: primary argument
   :specifications:

   There is exactly *ONE* blank line between the specifications and the text that is
   a part of the directive.
  1. Not every directive requires (or supports) a primary argument.
  2. Not every directive requires (or supports) specifications.
Admonitions

Sphinx enables you to include a few different admonitions. Note that which html_theme you choose in conf.py determines how they are displayed. With the admonitions, there are no arguments or specifications. If it is a short note you can specify it all on one line. If it is longer, make sure you keep the blank line between the directive and the text.

Note

.. note::

   This is a note!

Tip

.. tip::

   This is a tip!

Warning

.. warning::

   This is a warning!

Danger

.. danger::

   This is a danger (aka super-warning)!
Indexing / Including Other Files

The two directives you will use for this will be .. toctree:: and .. include::.

toctree
Toctrees are “Table of Contents” trees. See the Sphinx Toctree Docs.
include

I learned of the include directive by way of writing Exhale, and call attention to it because of the :start-after: and :end-before: specifiers. It’s particularly nice to use in order to have a shared README.rst for your code and documentation.

View the source code of exhale/docs/index.rst to see how that works.

Code Listings

If you hail from Markdown, keep in mind that it’s actually very similar. Instead of using fenced code blocks, you’re using a directive.

.. code-block:: cpp

   // This code is highlighted using the cpp lexer
   void foo() { /* ... */ }

results in

// This code is highlighted using the cpp lexer
void foo() { /* ... */ }

You have another option, which is to use two colons after a paragraph and then indent by four spaces. This is also similar to Markdown, only the two colons are required. The downside to this approach is you are at the disposal of Sphinx to determine what the language is.

This is a paragraph::

    def foo():
        pass

This is a paragraph:

def foo():
    pass

Noting that in the above output there is a single : after paragraph.