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.
Post History
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 t...
#2: Post edited
Pros and cons of vaious type_traits idoms
- 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 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).- ```c++
- #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());
- }
- ```
- 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).
- ```c++
- #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());
- }
- ```
#1: Initial revision
Pros and cons of vaious type_traits idoms
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). ```c++ #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()); } ```