Skip to content

Variable template specializations are not linked to their primary template #1269

Description

@jll63

(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:

  1. 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.
  2. The primary's page has no Specializations section.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions