在《Writing libraries with Generic-Text-Mapping》一文裡,我介紹了怎樣撰寫具備 generic text mapping 機制的 library。在該文文末,我提到了目前的缺憾是,沒辦法做出漂亮的 doxygen 文件:

我們可以使用 @copydoc,讓 StrLengthA() 使用 StrLengthW() 的那一份文件源碼。但是,使 StrLength() 為主,並以 function 的方式呈現,筆者就真的找不到解法了。因此,目前筆者所能想出來的,最好的 doxygen 文件寫法如下:

/**
 * Count the length of given string @p str, in number of characters.
 *
 * @param[in] str string to count length
 * @return Returns the length, number of characters, of string @p str.
 */
size_t StrLengthA(const char* str);

/**
 * @copydoc StrLengthA
 * @note This is the @c wchar_t version of StrLengthA().
 */
size_t StrLengthW(const wchar_t* str);

/**
 * @see Expand to StrLengthA() or StrLengthW() according to current
 *      generic-text-mapping configuration.
 * @hideinitializer
 */
#if (defined(UNICODE) || defined(_UNICODE))
#   define StrLength StrLengthW
#else
#   define StrLength StrLengthA
#endif

由於我們根本不需要讓 StrLength() 如 macro 般呈現,且 doxygen 會做 C/C++ preprocessing,根據設定,會在文件上顯示 StrLength 會被展開成為 StrLengthW 或是 StrLengthA,因此使用 @hideinitializer 把 macro 的右邊隱藏起來。

基本上,這只能算是折衷的辦法,看文件的人,還是必須了解 generic text mapping 的來龍去脈,才能理解 StrLength 這個 macro 為甚麼要這麼說明。而且,StrLength 會被歸類在 define 一類,而不是理解上的 function,這樣會讓人無法按圖索驥,訪知有此函式可用。

最近,我終於摸索出,如何使用 doxygen 做出如 MSDN 那般,能夠漂亮展現 generic text mapping 型函式的文件寫法。這個方法的訣竅在於:Doxygen 可以理解依靠 preprocessor 的 conditional parsing,故我們準備一個假的函式宣告,專門給 doxygen 產生文件即可。

#ifdef DOXYGEN_ONLY

/**
 * Count the length of given string @p str, in number of characters.
 *
 * @param[in] str string to count length
 * @return Returns the length, number of characters, of string @p str.
 */
size_t StrLength(const TCHAR* str); // fake prototype for doxygen only

#else /* DOXYGEN_ONLY */

#if (defined(UNICODE) || defined(_UNICODE))
#   define StrLength StrLengthW
#else
#   define StrLength StrLengthA
#endif

#endif /* DOXYGEN_ONLY */

/**
 * ASCII version of StrLength().
 */
size_t StrLengthA(const char* str);

/**
 * Wide-char version of StrLength().
 */
size_t StrLengthW(const wchar_t* str);

然後在 Doxyfile 裡打開 ENABLE_PREPROCESSING,並於 PREDEFINED 定義 DOXYGEN_ONLY,如下:

# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will.
# evaluate all C-preprocessor directives found in the sources and include.
# files.

ENABLE_PREPROCESSING   = YES

# The PREDEFINED tag can be used to specify one or more macro names that.
# are defined before the preprocessor is started (similar to the -D option of.
# gcc). The argument of the tag is a list of macros of the form: name.
# or name=definition (no spaces). If the definition and the = are.
# omitted =1 is assumed. To prevent a macro definition from being.
# undefined via #undef or recursively expanded use the := operator.
# instead of the = operator.

PREDEFINED             = DOXYGEN_ONLY

如此一來,當 doxygen 在產生文件的時候,會去抓 size_t StrLength(const TCHAR* str); 這一段,相反地,在真的要 compile 程式時,因為沒有定義 DOXYGEN_ONLY,所以 compiler 看不見這個假 prototype,反而是看到 generic text mapping 用以定義 StrLength 要對應到 StrLengthA 還是 StrLengthW 的部份。

利用這個技巧,我們可以真的產生一個名叫 StrLength 的函式的文件,且其文件是真的寫在 StrLengh() 處,不再有 @copydoc 技法該以誰為主的問題,更避開了 doxygen 處理 @copydoc 時,在產生出來的 function list 裡,會連 @brief 以外的內容都展示出來的 bug。

最後,讓我們再加上一個表格,於 StrLength() 裡,表列各種版本:

#ifdef DOXYGEN_ONLY

/**
 * Count the length of given string @p str, in number of characters.
 *
 * @param[in] str string to count length
 * @return Returns the length, number of characters, of string @p str.
 *
 * @remarks Generic-Text Routine Mappings:
 * <div class="generic_text_mapping_table">
 * <table>
 * <tr>
 * <th>TCHAR routine</th>
 * <td>StrLength()</td>
 * </tr>
 * <tr>
 * <th>_UNICODE & _MBCS not defined</th>
 * <td>StrLengthA()</td>
 * </tr>
 * <tr>
 * <th>_MBCS defined</th>
 * <td>StrLengthA()</td>
 * </tr>
 * <tr>
 * <th>_UNICODE defined</th>
 * <td>StrLengthW()</td>
 * </tr>
 * </table>
 * </div>
 */
size_t StrLength(const TCHAR* str); // fake prototype for doxygen only

#else /* DOXYGEN_ONLY */

#if (defined(UNICODE) || defined(_UNICODE))
#   define StrLength StrLengthW
#else
#   define StrLength StrLengthA
#endif

#endif /* DOXYGEN_ONLY */

/**
 * ASCII version of StrLength().
 */
size_t StrLengthA(const char* str);

/**
 * Wide-char version of StrLength().
 */
size_t StrLengthW(const wchar_t* str);

呈現出來的效果如下:

Generic Text Mapping Doc Demo - strlength.h File Reference

補充:

Doxygen 的 DISTRIBUTE_GROUP_DOC 可以讓同一個 member group 的所有函式,共享 group 裡第一個函式的文件。這在處理 function overloading 上,非常的好用,而我發現在處理 generic-text-mapping 上,也很好用。

我們還是需要將假造的 StrLength() 宣告,並隱藏 generic-text-mapping 使用的「依據 _UNICODE 定義與否,#define 不同版本的 StrLength()」的那段 preprocessing code。,先將假造的 StrLength() 宣告擺在最前面,把文件寫在那邊,然後在後面補上 StrLengthA()StrLengthB() 的宣告,但不要加任何文件

如此一來,StrLengthA()StrLengthW() 就會自動分享 StrLength() 的文件內容。比起寫「ANSI version of StrLength()」還要來的漂亮。

/** @{ */

#ifdef DOXYGEN_ONLY

/**
 * Count the length of given string @p str, in number of characters.
 *
 * @param[in] str string to count length
 * @return Returns the length, number of characters, of string @p str.
 *
 * @remarks Generic-Text Routine Mappings:
 * <div class="generic_text_mapping_table">
 * <table>
 * <tr>
 * <th>TCHAR routine</th>
 * <td>StrLength()</td>
 * </tr>
 * <tr>
 * <th>_UNICODE & _MBCS not defined</th>
 * <td>StrLengthA()</td>
 * </tr>
 * <tr>
 * <th>_MBCS defined</th>
 * <td>StrLengthA()</td>
 * </tr>
 * <tr>
 * <th>_UNICODE defined</th>
 * <td>StrLengthW()</td>
 * </tr>
 * </table>
 * </div>
 */
size_t StrLength(const TCHAR* str); // fake prototype for doxygen only

#else /* DOXYGEN_ONLY */

#if (defined(UNICODE) || defined(_UNICODE))
#   define StrLength StrLengthW
#else
#   define StrLength StrLengthA
#endif

#endif /* DOXYGEN_ONLY */

size_t StrLengthA(const char* str);

size_t StrLengthW(const wchar_t* str);

/** @} */

請注意多加的 /** @{ *//** @} */,這對括號會將裡面的東西,合併成一個 member group。最後呈現出來的效果如下:

Generic Text Mapping Doc Demo - strlength.h File Reference - with DISTRIBUTE_GROUP_DOC