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
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

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

Post

Pros and cons of various type_traits idioms

+7
−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());
}
History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

General comments (11 comments)
General comments
Lundin‭ wrote over 3 years ago

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.

dmckee‭ wrote over 3 years 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.

Lundin‭ wrote over 3 years ago · edited over 3 years 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‭ wrote over 3 years 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.

jrh‭ wrote over 3 years 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‭ wrote over 3 years ago · edited over 3 years ago

It seems like some projects are going in the direction of "offload every possible decision to the build script and templates" but in my opinion that's taking it much too far. I usually end up unscrambling the templates and 45 minutes later finding some UB or "it's just an int / double all the time". If it seems like too much work to document and provide sample code for fancy templates (including a list of acceptable types) that might be a sign that it's a bit too much.

dmckee‭ wrote over 3 years ago

@jrh I wouldn't call it controversial at all. Doing anything sophisticated with templates results in code that is hard to read, harder to debug, and simply brutal on edit-compile-test cycle times. We don't reach for this stuff first and we've got it in less than 1% of our headers (and plain template code in about 5%). But this question is about doing what you can to improve the readability issue.

jrh‭ wrote over 3 years ago · edited over 3 years ago

It might be too hard for me to say without seeing some more realistic code, but I'm not 100% sure what these examples are trying to achieve; are you designing a codebase where you want to be able to switch between working with doubles or floats by changing something in the build script? Or is this just a convenience / error proofing setup for a codebase that uses a mix of both? Or is the float / double thing just a standin for something else (for the sake of a minimal example)?

dmckee‭ wrote over 3 years ago

@jrh The sample code is made up for this post. I used is_floating_point because I can spell it without a search (I really don't use this stuff much). Where it appears in my project there is a cascade of SFINAE tests that cover quite a few classes of possible inputs (floating point, other numeric, strings, things derived from an interface in our code, maps, other containers). Even with type_traits we end up with six similar implementations. Doing one for each type we use? ::shudder::

anatolyg‭ wrote over 3 years ago

Maybe use concepts instead of all that SFINAE nonsense?

dmckee‭ wrote over 3 years ago · edited over 3 years ago

@anatolyg That's all well and good if c++20 is an option for you; the project that prompted this question is for work and the customer's requirements effectively lock us in to c++11. Indeed, due to customer requirements my whole shop is running 5+ years behind the standard (we're just starting to discuss what c++17 features to start using for those projects where requirements allow it). Concepts simply aren't on the menu.