Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Comments on Pros and cons of various type_traits idioms
Parent
Pros and cons of various type_traits idioms
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
-
enable_if
on the return type (functions and methods) - Flow control or
static_assert
on type traits. (functions and methods) -
enable_if
intypedef
orusing
statement (can be used in functions and methods, but I think it is more common with classes) - 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());
}
Post
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.
1 comment thread