Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics

Dashboard
Notifications
Mark all as read
Q&A

Pros and cons of various type_traits idioms

+6
−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 are there 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 developer 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 does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

11 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‭ 3 months 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‭ 3 months 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‭ 3 months 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‭ 3 months 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‭ 3 months ago

Show 6 more comments

1 answer

+3
−0

I'm new to type_traits and #2 made sense right away. Everything else was confusing, but I don't know a good way to avoid #4. Maybe you even need #1 sometimes.

#1 : enable_if on the return type

When I saw #1 I had to spend a minute looking for the return type. And I don't mean that I had trouble figuring out what the function returns... I can read the body and see that "return" is followed by a comparison, so it probably returns a boolean... I mean that I was expecting to see a "template-ret.type-fn.name-args-body" function declaration, and what I got was "template-big.mess-fn.name-- woah didn't we skip something?" It's not so bad the 12th time I see it, and I guess that's what idioms are for, but it threw me at first. Even now though it really bothers me that enable_if is trying to do two things at once: first, it wants to tell me which types T are allowed; second, it wants to tell me which type the function returns. It feels icky. Plus if I just read the enable_if line I can't even figure out what the function's return type is... it looks like it's supposed to be enable_if<..,T>::type, which is the second template argument of enable_if<..,T> which is here a floating-point type right? Not a boolean? I can work based on only intuition from the function body until I learn how to read these declarations, so it's not a big deal, but it bothers me.

#2 : static_assert

I like #2 a lot. You read the function declaration like normal, then you reference all the trait-related stuff like an appendix. Often I would bet that trait requirements can be intuited from the name of the function and the names of its arguments, so you won't even need to read the appendix. If you have multiple trait requirements, you don't need to break a pair of angle braces over multiple lines and create a new indented block of code between them... you just list all the requirements at the start of the function body, no abnormal indentation required.

There might be a problem with the error messages from static_assert, since you can't put the type that the compiler deduced into the string literal. I am not sure how big of a problem that is. This would be a good place for an experienced type_trait user to weight in. The other possible caveat is that, depending on how smart the C++ compiler is... my system packages GCC 4.8.5, which has experimental support for C++11... depending on how smart your compiler is, it may or may not be able to overload functions based on trait requirements listed in the function body. You know, all those times when you want to overload a function so that it does two different things depending on the traits of the generic types passed to it? That actually might be very often. I don't know. You might need to use #1 for that.

#3 : enable_if with using

I think that #3 has many of the benefits of #2, but it's still going to bother me until the idiom really gets dug into my subconscious. I can list the trait requirements like an appendix, I don't need a multi-line angle brace block, I can see the function's return type right there... but I mean when I see a type defined with using I expect that type to be used somewhere! That's the whole point! I know that this code will produce a compilation error under the desired conditions, but it just isn't semantically correct and I don't like it. Again, that's sort of the idea with idioms, but still I would avoid this when possible. I can see how this is used more often in class definitions, though, since you can't stick a static_assert in a class definition unless maybe you put it in the constructor or something.

#4 : iterator_traits

It took a while to warm up to this but I feel ok now. It isn't ideal to split up a single piece of functionality into two functions like that, but if you are going to implement a different AlgoImpl for each type of iterator then no harm has been done. If you really do just want a single function that enforces the iterator's random-access abilities at compile time, I think something like static_assert would be preferable but I was looking through cppreference.com and I could not find any built-in functions like is_random_access_iterator<T>. You can roll your own functions with tag dispatch, but at that point you're starting to build your own mini-language and it's probably best just to use the tools that the STL gives you.

So that's my thoughts ^_^ Not quite a full answer to your question but a newcomer's perspective might at least help you to idiotproof your code.

Why does this post require moderator attention?
You might want to add some details to your flag.

1 comment

If anything a beginners reaction (at least one as thoughtful as this) is an advantage because any code I produce along this line in likely to be read by juniors with limited exposure from time to time. For what it's worth, I encountered #1 first but am leaning more and more towards using static_assert for exactly the reasons you discuss. dmckee‭ about 1 month ago

Sign up to answer this question »