(Written by Claude Code, on behalf of @jll63.)
Variable template specializations are not linked to their primary template. Class and function templates are.
Three symptoms, all traceable to the same missing link:
- Every specialization is listed as a separate top-level entry in the enclosing namespace's
Variables table, alongside the primary and under the same name. Class templates suppress the specializations there and list them on the primary's page instead.
- The primary's page has no
Specializations section.
- The specialization's heading and synopsis drop the template argument list. For an explicit specialization this yields a declaration that is not valid C++:
template<> constexpr bool IsWrapper = true; — the <int> is gone.
Version: MrDocs 0.8.0+e31308f6c944 (develop-release nightly), Linux x86_64.
Reproducer
include/repro.hpp:
namespace demo {
template<class T, class U>
struct wrapper {};
// ---- variable template ----
/** Variable template primary
@tparam T A type.
*/
template<class T>
constexpr bool IsWrapper = false;
/** Variable template partial specialization
@tparam T The first argument.
@tparam U The second argument.
*/
template<class T, class U>
constexpr bool IsWrapper<wrapper<T, U>> = true;
/** Variable template explicit specialization */
template<>
constexpr bool IsWrapper<int> = true;
// ---- class template, for contrast: rendered correctly ----
/** Class template primary
@tparam T A type.
*/
template<class T>
struct box {};
/** Class template partial specialization
@tparam T The first argument.
@tparam U The second argument.
*/
template<class T, class U>
struct box<wrapper<T, U>> {};
/** Class template explicit specialization */
template<>
struct box<int> {};
} // namespace demo
mrdocs.yml:
source-root: .
input:
- include
file-patterns:
- '*.hpp'
include-symbols:
- 'demo::**'
multipage: false
compile_commands.json:
[
{"directory": ".", "command": "c++ -std=c++20 -Iinclude -c include/repro.hpp -o repro.o", "file": "include/repro.hpp"}
]
mrdocs --config=mrdocs.yml --generator=adoc --output=out compile_commands.json
Actual output
The namespace tables — box appears once, IsWrapper three times:
=== Types
| link:#demo-box-0a[`box`] | Class template primary
| link:#demo-wrapper[`wrapper`] |
=== Variables
| link:#demo-IsWrapper-0e[`IsWrapper`] | Variable template primary
| link:#demo-IsWrapper-004[`IsWrapper`] | Variable template explicit specialization
| link:#demo-IsWrapper-00f[`IsWrapper`] | Variable template partial specialization
box gets a Specializations section and argument-qualified names:
[#demo-box-0a]
== demo::box
=== Synopsis
template<class T>
struct box;
=== Specializations
| link:#demo-box-04[`box<wrapper<T, U>>`] | Class template partial specialization
| link:#demo-box-0c[`box<int>`] | Class template explicit specialization
IsWrapper gets neither. All three pages are titled demo::IsWrapper, and the two specializations lose their arguments:
[#demo-IsWrapper-0e]
== demo::IsWrapper <- primary; no Specializations section
template<class T>
constexpr bool IsWrapper = false;
[#demo-IsWrapper-004]
== demo::IsWrapper <- explicit specialization
template<>
constexpr bool IsWrapper = true; <- not valid C++; should be IsWrapper<int>
[#demo-IsWrapper-00f]
== demo::IsWrapper <- partial specialization
template<
class T,
class U>
constexpr bool IsWrapper = true; <- should be IsWrapper<wrapper<T, U>>
Expected output
The same shape as box: Variables lists IsWrapper once; the primary's page carries a Specializations section linking IsWrapper<wrapper<T, U>> and IsWrapper<int>; each specialization's heading and synopsis include its argument list.
Where it goes wrong
--generator=xml on the same input shows the template arguments are extracted for variable specializations, but Primary is never set, so nothing downstream can pair them up:
| kind |
symbol |
<args> |
<primary> |
<specializations> |
| record |
box (primary) |
– |
– |
yes |
| record |
box<int> |
yes |
yes |
– |
| record |
box<wrapper<T, U>> |
yes |
yes |
– |
| variable |
IsWrapper (primary) |
– |
– |
no |
| variable |
IsWrapper<int> |
yes |
no |
– |
| variable |
IsWrapper<wrapper<T, U>> |
yes |
no |
– |
Two consumer-side gaps look relevant, both of which treat variables differently from records and functions:
SpecializationFinalizer::build() (src/mrdocs/Metadata/Finalizers/SpecializationFinalizer.cpp) dispatches on isRecord(), isFunction() and isGuide(), with no branch for variables. Correspondingly, VariableSymbol (include/mrdocs/Metadata/Symbol/Variable.hpp) has neither the Specializations vector nor the IsListedOnPrimary flag that Record.hpp and Function.hpp both declare — so there is nowhere to record the back-pointer, and nothing to suppress the specialization from the parent scope's table.
data/mrdocs/addons/generator/common/partials/symbol/name-text.hbs renders the argument list only under {{#if template.primary}}. That gate explains symptom 3 directly: with Primary unset, the arguments are present in the model but never printed.
ASTVisitor::populate(TemplateInfo&, VarDeclTy const*, VarTemplateDecl const*) (src/mrdocs/AST/ASTVisitor.cpp, ~line 1395) does call generateID(getInstantiatedFrom(VTD), Template.Primary) in the same branch that populates Template.Args, and the args do come through — so the ID resolution itself appears to be what fails. I did not chase it further than that.
Impact
Boost.OpenMethod's reference is hit by this: boost::openmethod variables lists IsVirtualAny twice, and the specialization's page shows constexpr bool IsVirtualAny = true; with the <virtual_any<Any, Registry>> missing.
As a side effect the Doxygen-style tagfile emits two <compound> entries with the identical name demo::IsWrapper, which likely confuses tools consuming it for cross-referencing.
(Written by Claude Code, on behalf of @jll63.)
Variable template specializations are not linked to their primary template. Class and function templates are.
Three symptoms, all traceable to the same missing link:
Variablestable, alongside the primary and under the same name. Class templates suppress the specializations there and list them on the primary's page instead.Specializationssection.template<> constexpr bool IsWrapper = true;— the<int>is gone.Version:
MrDocs 0.8.0+e31308f6c944(develop-releasenightly), Linux x86_64.Reproducer
include/repro.hpp:mrdocs.yml:compile_commands.json:[ {"directory": ".", "command": "c++ -std=c++20 -Iinclude -c include/repro.hpp -o repro.o", "file": "include/repro.hpp"} ]Actual output
The namespace tables —
boxappears once,IsWrapperthree times:boxgets aSpecializationssection and argument-qualified names:IsWrappergets neither. All three pages are titleddemo::IsWrapper, and the two specializations lose their arguments:Expected output
The same shape as
box:VariableslistsIsWrapperonce; the primary's page carries aSpecializationssection linkingIsWrapper<wrapper<T, U>>andIsWrapper<int>; each specialization's heading and synopsis include its argument list.Where it goes wrong
--generator=xmlon the same input shows the template arguments are extracted for variable specializations, butPrimaryis never set, so nothing downstream can pair them up:<args><primary><specializations>box(primary)box<int>box<wrapper<T, U>>IsWrapper(primary)IsWrapper<int>IsWrapper<wrapper<T, U>>Two consumer-side gaps look relevant, both of which treat variables differently from records and functions:
SpecializationFinalizer::build()(src/mrdocs/Metadata/Finalizers/SpecializationFinalizer.cpp) dispatches onisRecord(),isFunction()andisGuide(), with no branch for variables. Correspondingly,VariableSymbol(include/mrdocs/Metadata/Symbol/Variable.hpp) has neither theSpecializationsvector nor theIsListedOnPrimaryflag thatRecord.hppandFunction.hppboth declare — so there is nowhere to record the back-pointer, and nothing to suppress the specialization from the parent scope's table.data/mrdocs/addons/generator/common/partials/symbol/name-text.hbsrenders the argument list only under{{#if template.primary}}. That gate explains symptom 3 directly: withPrimaryunset, the arguments are present in the model but never printed.ASTVisitor::populate(TemplateInfo&, VarDeclTy const*, VarTemplateDecl const*)(src/mrdocs/AST/ASTVisitor.cpp, ~line 1395) does callgenerateID(getInstantiatedFrom(VTD), Template.Primary)in the same branch that populatesTemplate.Args, and the args do come through — so the ID resolution itself appears to be what fails. I did not chase it further than that.Impact
Boost.OpenMethod's reference is hit by this:
boost::openmethodvariables listsIsVirtualAnytwice, and the specialization's page showsconstexpr bool IsVirtualAny = true;with the<virtual_any<Any, Registry>>missing.As a side effect the Doxygen-style tagfile emits two
<compound>entries with the identical namedemo::IsWrapper, which likely confuses tools consuming it for cross-referencing.