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
Start the documentation as early as you can. Don’t document it once you want to release.
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.
When things aren’t appearing correctly, add the line
GENERATE_HTML = YES
to your input toexhaleDoxygenStdin
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.
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
Assume that you can just plug-and-play and never read any documentation on the documentation systems.
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 whereconf.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 = ./_doxygen
, and you specified to DoxygenGENERATE_HTML = YES
,GENERATE_LATEX = YES
, andGENERATE_XML = YES
, it would create the folder./_doxygen
, with subdirectories such ashtml
orxml
.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 forOUTPUT_DIRECTORY
when usingexhaleDoxygenStdin
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 toYES
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 toexhaleDoxygenStdin
, 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 |
---|---|
|
Add link to another item being documented. |
|
Add brief documentation to a given construct. |
|
Add documentation for a specific parameter. |
|
Add documentation for a specific template parameter. |
|
Add documentation for a specific exception that can be thrown. |
|
Add documentation for the return value. |
Explicit Control Over Contstructs (e.g., Adding Documentation Apart from Definition ) |
|
|
To document a |
|
To document a |
|
To document an |
|
To document a function. |
|
To document a variable or |
|
To document a |
|
To document a |
|
To document a file. |
|
To document a |
Inline Formatting (see File and Namespace Level Documentation in Exhale) |
|
|
Bold a single word (e.g. |
|
Emphasize a single word (e.g. |
|
Teletype a single word (e.g. |
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!!!
Hyperlinks
I’ll confess that reStructuredText hyperlinks are probably the most confusing. Best to leave the explaining to the official documentation.
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.
Not every directive requires (or supports) a primary argument.
Not every directive requires (or supports) specifications.
- Admonitions
Sphinx enables you to include a few different admonitions. Note that which
html_theme
you choose inconf.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 sharedREADME.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
:
afterparagraph
.