6

In a class Foo I have the following template functions :

class Foo
{
  public:
    template <typename T>
    static typename boost::enable_if<boost::is_abstract<T>, T*>::type allocate();
    template <typename T>
    static typename boost::disable_if<boost::is_abstract<T>, T*>::type allocate();
};

There is two declarations, but, for the user, only one function.

What is the usual way to document this kind of declaration with Doxygen ?

Idea #1 :

class Foo
{
  public:
    /** \brief This function throws a runtime error */
    template <typename T>
    static typename boost::enable_if<boost::is_abstract<T>, T*>::type allocate();
    /** \brief This function allocates an instance of the given type */
    template <typename T>
    static typename boost::disable_if<boost::is_abstract<T>, T*>::type allocate();
};

Idea #2 :

class Foo
{
  public:
    /** \brief This function allocates an instance of the given type if not abstract, throws an exception instead */
    template <typename T>
    static typename boost::enable_if<boost::is_abstract<T>, T*>::type allocate();
    template <typename T>
    static typename boost::disable_if<boost::is_abstract<T>, T*>::type allocate();
};
Caduchon
  • 4,574
  • 4
  • 26
  • 67
  • No chance to document template stuff in general with doxygen. Any kind of specialization is not handled in a expected fashion. Have you found a way to comment on template parameters at all? See also: http://stackoverflow.com/questions/3435225/c-meta-programming-doxygen-documentation?rq=1 – Klaus Apr 13 '16 at 12:11
  • @Klaus : For the moment, I have no other problem to document template functions with Doxygen. Maybe I didn't do complex things with templates before today... – Caduchon Apr 13 '16 at 12:18
  • For your example, instead of two overloads, you could just have one and then do `BOOST_STATIC_ASSERT_MSG(!boost::is_abstract::value, "T must be concrete")` in it. This will generate a compile error if the requirements for `T` are not met, which is preferable to a runtime exception. The C++11 equivalent would be `static_assert(!std::is_abstract::value, "T must be concrete")`, and in C++17: `static_assert(!std::is_abstract_v, "T must be concrete")`. – Emile Cormier May 15 '22 at 03:13
  • @EmileCormier actually, I need a runtime check in my context because the type is only known at runtime and `new T` must be called by `allocate` if not abstract. During the execution, the other function will never be called (some check before), but the compiler doesn't know it and want to write the branch. – Caduchon May 16 '22 at 07:38
  • @Caduchon Ah, now I understand. You could use _tagged dispatching_ in that case. See the bottom part of my updated answer. – Emile Cormier May 16 '22 at 17:57

1 Answers1

1

I'll answer this in terms of std::enable_if, but it should work just as well with boost::enable_if.

You can use macros that wrap enable_if and define them differently whether Doxygen is running or not.

In your Doxyfile, add the following to your configuration:

PREDEFINED = GENERATING_DOXYGEN

EXPAND_AS_DEFINED = ENABLE_IF \
                    ENABLED_TYPE

In your C++ project, define the following macros:

#ifdef GENERATING_DOXYGEN
#define ENABLE_IF(cond)
#define ENABLED_TYPE(T, cond) T
#else
#define ENABLE_IF(cond) typename std::enable_if<(cond)>::type
#define ENABLED_TYPE(T, cond) typename std::enable_if<(cond), T>::type
#endif

You may then use the macros as follows:

class Foo
{
  public:
    /** \brief Throws a runtime error
        \tparam T An abstract type
        \detail Only participates in overload resolution
                when std::is_abstract<T>::value==true */
    template <typename T>
    static ENABLED_TYPE(T*, std::is_abstract<T>::value) allocate();

    /** \brief Allocates an instance of the given type
        \tparam T A concrete type
        \detail Only participates in overload resolution
                when std::is_abstract<T>::value==false */
    template <typename T>
    static ENABLED_TYPE(T*, !std::is_abstract<T>::value) allocate();
};

The ENABLED_TYPE macros will expand to the undecorated return type (in your case, T*) when Doxygen is running. When compiling, the macros will expand to std::enable_if<cond, T>::type to perform SFINAE.

Instead of ENABLE_IF, you may want to name the macro REQUIRES to make your code a bit more readable. You may also want to prefix your macros with the name of your project to avoid clashing with macros defined in other libraries or in whatever super-project may want to consume your project.


What you can also do is have a single public function, and then use tag dispatching for your two cases:

class Foo
{
  public:
    /** \brief Allocates an instance of the given type
        \tparam T Must be a concrete type (checked at run time) */
    template <typename T>
    static T* allocate()
    {
      do_allocate(std::is_abstract<T>());
      // ...
    }

  private:
    template <typename T>
    T* do_allocate(std::false_type)
    {
      return new T;
    }

    template <typename T>
    T* do_allocate(std::true_type)
    {
      throw std::runtime_error("T must be concrete");
    }
};

With C++17, you could simply do if constexpr:

class Foo
{
  public:
    /** \brief Allocates an instance of the given type
        \tparam T Must be a concrete type (checked at run time) */
    template <typename T>
    static T* allocate()
    {
      if constexpr (std::is_abstract<T>())
      {
        throw std::runtime_error("T must be concrete");
      }
      else
      {
        return new T;
      }
    }
};
Emile Cormier
  • 28,391
  • 15
  • 94
  • 122
  • This is an interesting solution to the problem, though admittedly I'd absolutely *hate* to be in a codebase where people are wrapping otherwise visible/idiomatic uses of `enable_if` inside of macros, purely just so a third-party tool like doxygen can generate something decent. IMO code shouldn't be written strangely just to assist a tool. – Human-Compiler May 15 '22 at 03:30
  • You should also mention the version of doxygen you used. – albert May 15 '22 at 08:05
  • @albert The Doxygen config options I used in my answer have been supported for over a decade. – Emile Cormier May 15 '22 at 18:19
  • @Human-Compiler This whole SFINAE business already *is* some strange-looking code to begin with. Hopefully C++20 constraints will improve things when it gains better support and adoption. – Emile Cormier May 15 '22 at 18:23
  • @EmileCormier it might be that the settings have been supported for over a decade but the underlying logic might have been wrong and has been corrected, so it is always good to mention the used version. – albert May 16 '22 at 07:47