17

One of the (so very many) unfortunate design flaws of C++ is that it is basically impossible to separate implementation from interface when using template metaprogramming.

All over my library I have things like:

template <typename Ma, typename Mb>
typename boost::enable_if_c<
            detail::IsMatrix<Ma>::val and detail::IsMatrix<Mb>::val and
            detail::MatrixDimensionCheck<Ma,Mb>::isStaticMatch, 
        bool>::type
operator==(const Ma &a, const Mb &b) {
    return detail::matrixEqual(a,b);
}

If this is unreadable, I don't blame you. Most of this mess is simply defining the return type to be bool if the arguments are matrices and match dimension, and be undefined if they are something else (thus relying on SFINAE to prevent this operator from hiding other important things).

Since the guts of what is essentially a static type-checking function are now embedded into the signature of my ordinary C++ function, these implementation guts will appear in the generated documentation.

I don't want the user to have to read this. All they need to know is that this function returns a bool (which is almost impossible to tell by reading the above). In the docs, I can explain succinctly, in plain English, that this operator accepts only matrices.

Is there a way to persuade Doxygen to render that type mess as bool? (I'm assuming there is more or less no way to clean this up in the code directly, but ideas are welcome if you can think of something).

trbabb
  • 1,894
  • 18
  • 35
  • 1
    There are reasons why I prefer hand-written documentation over "automatically generated" documentation. I almost always spend more time setting up the documentation system/fixing it to being acceptable while in the meantime I've could've written all the documentation in an actual nice format already. – orlp Aug 08 '13 at 08:17
  • 7
    @nightcracker: Until of course you change anything, such as parameters, functions, etc, without updating the documentation. Then it gets out of sync and becomes worse than useless. Also, Doxygen supports hand-written documentation just fine. – Nicol Bolas Aug 08 '13 at 08:24
  • 2
    Maybe related is [this](http://stackoverflow.com/questions/3435225/c-meta-programming-doxygen-documentation) question. – elemakil Aug 08 '13 at 08:24
  • Bjarne Stroupstrup has a comment in his FAQ: *"Like all powerful techniques they [Template metaprogramming] are easily overused"* Don't get me wrong, I love template metaprogramming. But it must be used with common sense. Its only an advise, not a critic about your code. – Manu343726 Aug 08 '13 at 08:27
  • if you have this 'all over your library' can't you somehow make it a single reusable entity? – stijn Aug 08 '13 at 08:39
  • 1
    The (pretty cumbersome, I agree) way I'd go with a library whose implementation relies heavily on such unhidable template magic is to just make a separate header file containing just the clean *"as if"* version of the interface and run doxygen over that one. Of course this can become pretty cumbersome with a large library, but I've done this for a small single-file header-only library and it works quite good. Of course you have to keep both files in-sync, but so is always the case when the documentation cannot be generated directly from the source. – Christian Rau Aug 08 '13 at 10:44
  • @nightcracker Yet I enjoy all the boilerplate structuring, formatting and HTMLization that doxygen provides, wouldn't want to write all this myself. Then just providing a clean pseudo source code version to doxygen is much easier. – Christian Rau Aug 08 '13 at 10:47
  • Another possiblity might be preprocessor directives (that doxygen can work with) to dependently define all this SFINAE madness to a simple bool. Yet in this case you cannot use the plain source code for much information anymore (since it is even more unreadable than before) and *have* to rely on the generated documentation. – Christian Rau Aug 08 '13 at 10:50

5 Answers5

3

What about:

#ifdef DOXYGEN
    #define RETURN_TYPE(Test, Type1) Type1
#else
    #define RETURN_TYPE(Test, Type1) typename boost::enable_if_c< Test, Type1 >::type
#endif

template <typename Ma, typename Mb>
RETURN_TYPE((detail::IsMatrix<Ma>::val 
        and detail::IsMatrix<Mb>::val 
        and detail::MatrixDimensionCheck<Ma,Mb>::isStaticMatch), bool) 
operator==(const Ma &a, const Mb &b) { return detail::matrixEqual(a,b); }

IMHO, it's even easier to read & understand than the initial C++ code. Please notice the double parenthesis in the first macro argument to avoid the compiler to break on the comma in the "Test". You can get rid of it if you reorder the return type first (Type1), and use variable arg macro for the test.

xryl669
  • 3,376
  • 24
  • 47
  • 3
    I am not a fan of a documentation that forces change in the code. – Raffi Sep 12 '13 at 18:19
  • 2
    Usually I agree, but in that case, the macro actually gain visibility. The meaning of the code is more clear with a macro, than without. The fact that the macro changes behavior when run by Doxygen is a good benefit to the code (that can be done in Doxygen's script if you don't want to pollute your code) – xryl669 Sep 13 '13 at 13:05
2

Well, the only way I may achieve this is by duplicating the function definition rather than using the automatic feature of doxygen, and using the @fn command instead. For your example, something like

/*!@fn template <typename Ma, typename Mb> bool operator==(const Ma &a, const Mb &b)
 * @brief My equality operator
 * @note The operator is available if the types @c Ma and @c Mb match. 
 *       It will be discarded otherwise 
 */
 template <typename Ma, typename Mb>
   typename boost::enable_if_c<
     detail::IsMatrix<Ma>::val and detail::IsMatrix<Mb>::val and
     detail::MatrixDimensionCheck<Ma,Mb>::isStaticMatch, 
   bool>::type
 operator==(const Ma &a, const Mb &b) {
    return detail::matrixEqual(a,b);
 }

should do.

albert
  • 8,285
  • 3
  • 19
  • 32
Raffi
  • 3,068
  • 31
  • 33
1

Re persuading Doxygen to show bool as return type: The only way I know of is Raffi's answer, adding that you then probably want to hide the actual function from Doxygen (several ways to do this).

Re cleaning up: This could look something like

template <typename Ma, typename Mb>
typename bool_isEqual<Ma, Mb>::type 
operator==(const Ma &a, const Mb &b)
...

Where bool_isEqual encapsulates all the template type logic and typedefs type to bool when adequate. (The name bool_isEqual is chosen because there are assumed to be other template functions with a similar structure that return bool but have other conditions.)

If this is done consistently, it is probably readable enough.

Gerhard
  • 518
  • 5
  • 9
1

I found following method to be very clear:

  1. In Doxyfile add PREDEFINED = DOXYGEN
  2. In your source code surround SFINAE functions with ///@cond .... ///@endcond
  3. Put a simple templated function declaration in your source code within #ifdef DOXYGEN, so it will be invisible to normal compilation. Observe:

    ///@cond
    template <typename Ma, typename Mb>
    typename boost::enable_if_c<
        detail::IsMatrix<Ma>::val and detail::IsMatrix<Mb>::val and
        detail::MatrixDimensionCheck<Ma,Mb>::isStaticMatch, 
        bool>::type
    operator==(const Ma &a, const Mb &b) {
        return detail::matrixEqual(a,b);
    }
    ///@endcond
    #ifdef DOXYGEN
        ///Documentation for your function...
        template<typename Ma, typename> operator==(const Ma &a, const Mb &b);
    #endif
    
Cem Kalyoncu
  • 14,120
  • 4
  • 40
  • 62
-1

I think this might work for you. Obviouslty it's a simpler example than yours, but the basic idea of using a documented template function without enable_if to call another "hidden" function that isn't documented but provides SFINAE.

// Ignore this function in doxygen
template <typename T>
typename boost::enable_if<boost::is_unsigned<T>, bool>::type
test_hidden(T t) {
    return true;
}

template <typename T>
typename boost::disable_if<boost::is_unsigned<T>, bool>::type
test_hidden(T t) {
    return false;
}

// Document this function
template <typename T>
bool test(T t)
{
    return test_hidden(t);
}

int main()
{
   unsigned int a = 1;
   int b = 0;

   std::cout << test(a) << std::endl; // true
   std::cout << test(b) << std::endl; // false

   return 0;
}
dunc123
  • 2,595
  • 1
  • 11
  • 11
  • SFINAE only applies to template signatures, not template bodies, so this is not equivalent. The compiler will not try other templates if the expansion of the template body is invalid; it will simply error and halt. – trbabb Aug 08 '13 at 19:49
  • I agree that this method does have some limitations. However the compiler will try different implementations of `test_hidden` in my example which might be sufficient in some cases. I've updated my answer to show this. – dunc123 Aug 09 '13 at 08:08