cppalliance / mrdocs

MrDocs: A Clang/LLVM tool for building reference documentation from C++ code and javadoc comments.
https://mrdocs.com
Other
66 stars 16 forks source link

identify enable_if #565

Open alandefreitas opened 2 months ago

alandefreitas commented 2 months ago

Motivation

This issue is somewhat related to #564. Many if not most libraries pre-C++20 use SFINAE for specializations. This can happen with or without library support (std::enable_if). These specializations can be enabled in several ways:

// enabled via the return type
template<class T>
typename std::enable_if<std::is_trivially_default_constructible<T>::value>::type 
construct(T*) 
{
    std::cout << "default constructing trivially default constructible T\n";
}

// enabled via a parameter
template<class T>
void destroy(
    T*, 
    typename std::enable_if<std::is_trivially_destructible<T>::value>::type* = 0)
{
    std::cout << "destroying trivially destructible T\n";
}

// enabled via a non-type template parameter
template<class T,
         typename std::enable_if<!std::is_trivially_destructible<T>{} && (std::is_class<T>{} || std::is_union<T>{}), bool>::type = true>
void destroy(T* t)
{
    std::cout << "destroying non-trivially destructible T\n";
    t->~T();
}

// enabled via a type template parameter
template<class T,
     typename = std::enable_if_t<std::is_array<T>::value>>
void destroy(T* t)
{
    for (std::size_t i = 0; i < std::extent<T>::value; ++i)
        destroy((*t)[i]);
}

These conditions to enable function can become much more complex than that, which makes the documentation generated by MrDocs impossible to read. From Boost.Buffers:

image

For this reason, in documentation, these conditions are seen as independent function constraints that are documented as part of the function requirements in the function details/notes (examples below).

As with #564, whether a condition that leads to substitution failure is a purposeful semantic description of the function requirements might be a matter of intentionality. Luckily, unlike #564, the use of std::enable_if provides the developer with a way to express that intentionality. In fact, clang already has a feature where it interprets std::enable_if as requirements to provide better error messages.

SFINAE in cppreference

Cppreference documents constraints that are usually implemented with SFINAE with notes in the function details. For instance, we can consider this vector::vector overload:

image

This overload should only be enabled if InputIt satisfies LegacyInputIterator.

Here's how libstdc++ implements this constraint:

template<typename _InputIterator,
    typename = std::_RequireInputIter<_InputIterator>>
_GLIBCXX20_CONSTEXPR
vector(_InputIterator __first, _InputIterator __last,
         const allocator_type& __a = allocator_type());

where std::_RequireInputIter<_InputIterator>> does the job of std::enable_if.

And here's how libc++ implements it:

template <class _InputIterator,
          class _Alloc,
          class = enable_if_t<__has_input_iterator_category<_InputIterator>::value>,
          class = enable_if_t<__is_allocator<_Alloc>::value> >
vector(_InputIterator, _InputIterator, _Alloc) -> vector<__iter_value_type<_InputIterator>, _Alloc>;

When we look at the documentation of these constrained functions, the function overloads omit their constraints and the function description usually ends with a comment such as:

image

The comment always follows the pattern "This overload participates in overload resolution only if [the constraint]" but the constraint is sometimes complemented by its rationale.

SFINAE in Doxygen

SFINAE is usually documented in doxygen with macros to omit the std::enable_if portion of the function definition. For instance, boost::urls::ref is documented with:

template<class Rule>
constexpr
#ifdef BOOST_URL_DOCS
__implementation_defined__
#else
typename std::enable_if<
    is_rule<Rule>::value &&
    ! std::is_same<Rule,
        detail::rule_ref<Rule> >::value,
    detail::rule_ref<Rule> >::type
#endif
ref(Rule const& r) noexcept;

and boost::urls::lut_chars is documented with:

template<class Pred
#ifndef BOOST_URL_DOCS
  ,class = typename std::enable_if<
      detail::is_pred<Pred>::value &&
  ! std::is_base_of<
      lut_chars, Pred>::value>::type
#endif
>
constexpr
lut_chars(Pred const& pred) noexcept;

In the first case, the return value is replaced with __implementation_defined__.

image

It would usually be replaced with the real return value but the ultimate return value detail::rule_ref<Rule> is an implementation detail that should also be omitted. In this case, the whole expression can be appropriately replaced with __implementation_defined__.

In the second case, the condition is simply omitted:

image

The developer is responsible for manually describing the condition again in the function details.

However, the solution above does not apply to MrDocs because it expects valid C++ code. If two function declarations have their conditions removed, this will be interpreted as an error because this is a function redeclaration and the functions can't be documented separately. Or in other cases, the functions might become ambiguous and the code that instantiates the functions will also lead to an error.

Proposed solutions

As with #564, neither doxygen nor mrdocs currently have any feature to make that easier. Although there's a viable alternative people have been using for Doxygen, it's far from ideal when there are many constrained functions in a codebase. So this is also a feature where mrdocs has potential to be much more helpful than doxygen.

MrDocs could implement the following solutions:

This is a general idea. Considering the complexity of the problem, implementation requirements can only be completely identified when we start working on it. We will certainly find more obstacles.

alandefreitas commented 4 days ago

@sdkrystian I'll keep this issue open because we still need to be able to extract the constraint. But I'm moving it to P1 because P0 is about Boost.URL, and we should already have what we need there.