Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

identify enable_if #565

Open
alandefreitas opened this issue Apr 12, 2024 · 0 comments
Open

identify enable_if #565

alandefreitas opened this issue Apr 12, 2024 · 0 comments
Assignees
Labels
Feature Something new that it should do

Comments

@alandefreitas
Copy link
Collaborator

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:

  • When MrDocs builds the corpus, as a post-processing step, it would identify these purposeful constraints that use std::enable_if and this information would be included in the Info object.
  • This automatic detection could be disabled by a config option. Although automatic detection should work most of the time, disabling automatic detection might be useful when the documentation is about std::enable_if itself.
  • The documentation template pages for these constrained functions should include a message similar to the one in cppreference. Alternatively, the constraints could be rendered as requires clauses, as if the library was a post-C++20 library.
  • Some other forms of detection of intentional SFINAE could be enabled over time (boost::enable_if and so on).

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 alandefreitas added the Feature Something new that it should do label May 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature Something new that it should do
Projects
Status: No status
Development

No branches or pull requests

3 participants