Notifications
Mark all as read
Q&A

Pros and cons of vaious type_traits idoms

+5
−0

My work tasks have recently started requiring me to use the type_traits header to restrict the classes that may be used in template functions, methods, and classes. And while I used it for a long time now, I learned c++ on the job.

I've seen at least four patterns (see below) for actually coding these things and don't know what (if any) reasons there are for preferring one idiom.

I describe the four idioms I've seen as

  1. enable_if on the return type (functions and methods)
  2. Flow control or static_assert on type traits. (functions and methods)
  3. enable_if in typedef or using statement (can be used in functions and methods, but I think it is more common with classes)
  4. For more complex traits (like interator_traits) you can use tag dispatch to a separate implementation

What are the pros and cons of these options? Is there a developing consensus on which are clearer or more maintainable?


So example code (in c++11) showing the idioms I've encountered (the bodies of the functions are unimportant here, but resemble simplified versions of things that have come up at work).

#include <cmath>
#include <iterator>
#include <list>
#include <type_traits>
#include <vector>

// Method 1: enable_if on return type
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
almostEqual1(const T f1, const T f2, const T epsilon = 1e-6)
{
    return std::fabs(f1-f2) < static_cast<T>(epsilon);
}


// Method 2: if (constexpr ...) or static_assert on a type trait
template <typename T>
bool almostEqual2(const T f1, const T f2, const T epsilon = 1e-6)
{
    static_assert( std::is_floating_point<T>::value,
                   "Arguments must be of floating point type" );
    return std::fabs(f1-f2) < static_cast<T>(epsilon);
}


// Method 3: using with enable_if
// Perhaps used more with template classes?
template <typename T>
bool almostEqual3(const T f1, const T f2, const T epsilon = 1e-6)
{
    using enable = typename
                   std::enable_if<std::is_floating_point<T>::value, void>::type;
    return std::fabs(f1-f2) < static_cast<T>(epsilon);
}


// Method 4 (iterators or customm traits): tag dispatching with separate implementation
template <typename Iterator>
void AlgoImpl(Iterator first, Iterator last, std::random_access_iterator_tag )
{
    // ...
}

template <typename Iterator>
void Algo(Iterator first, Iterator last )
{
    using category = typename std::iterator_traits<Iterator>::iterator_category;
    AlgoImpl(first, last, category());
}


//
int main(void)
{
    // almostEqual1(1,2);  // doesn't compile
    almostEqual1(1.0,2.0);
    almostEqual1(1.0f,2.0f);
    // almostEqual1(1.0f,2.0); // doesn't compile

    // almostEqual2(1,2); // asserts at compile time
    almostEqual2(1.0,2.0);
    almostEqual2(1.0f,2.0f);
    // almostEqual2(1.0f,2.0); // doesn't compile

    // almostEqual3(1,2); // doesn't compile
    almostEqual3(1.0,2.0);
    almostEqual3(1.0f,2.0f);
    // almostEqual3(1.0f,2.0); // doesn't compile

    std::list<float> l;
    std::vector<float> v;
    // Algo(l.begin(),l.end()); // doesn't compile
    Algo(v.begin(),v.end());
}
Why should this post be closed?

9 comments

Yet another option is to KISS and not use templates at all, but to provide & overload functions for the supported types only. Because restricting the type-generic interface to only support certain types kind of goes against common sense. Lundin‭ 23 days ago

@Lundin The flip side of KISS is DRY. Restricting the type-generic interface does make sense for things like writing a specialized STL-like algorithm that only works on random access containers. In principle you could accomplish that with polymorphsim by making the iterator classes members of a clever inheritance tree, but (a) I didn't have the freedom to write the iterators that way and (b) then it wouldn't work on c-arrays. dmckee‭ 23 days ago

@dmckee DRY is probably the most overrated and dangerous design principle out there, though. Often it causes more problems than it solves, due to overly complex and obscure code. Maintaining a template metaprogramming hell will almost certainly cause more bugs than maintaining 2 functions with code repetition. Lundin‭ 22 days ago

Besides, lets say you drop templates and overload int + float functions only. You will most likely need to write the implementation of those functions differently, because these types are compared & promoted differently. Lundin‭ 22 days ago

I don't know if this is a controversial opinion or not, but I think when you get sufficiently complicated with template programming, that becomes its own language, and just like macros, I think that language is much less readable than the rest of C++. The debugging options are far worse and the error messages are cryptic jrh‭ 22 days ago

Show 4 more comments

0 answers

Sign up to answer this question »