Advanced C Templates Computer Games Engineering CO 4302

  • Slides: 24
Download presentation
Advanced C++ Templates Computer Games Engineering - CO 4302

Advanced C++ Templates Computer Games Engineering - CO 4302

OVERVIEW ◉ Templates are an extremely powerful part of modern C++ ◉ However, their

OVERVIEW ◉ Templates are an extremely powerful part of modern C++ ◉ However, their use is complex, and there are useful features that do not get used particularly often. ◉ We will cover a few more advanced template techniques here

Template Instantiation 1

Template Instantiation 1

TEMPLATE INSTANTIATION ◉ Writing a template function or class creates no code in itself

TEMPLATE INSTANTIATION ◉ Writing a template function or class creates no code in itself ◉ Example of error due to this: // Some. File. h - seems OK… template <typename T> void My. Func(T param); // Some. File. cpp - creates no code as the template fn is not *used* template <typename T> void My. Func(T param) { /* Do something */ } // Some. Other. File. cpp #include "Some. File. h" My. Func(1); // Compiles, but will cause linker error. Can’t find // the code for My. Func<int>. Some. File. cpp created no // code to call (it couldn’t as T was not yet defined)

TEMPLATE INSTANTIATION ◉ A template function or class is a only a specification for

TEMPLATE INSTANTIATION ◉ A template function or class is a only a specification for code, it generates no actual code. ◉ Two ways to create actual code: ◉ Call the function / use the class where the specification is available in the same source file (typically using include files) ◉ Explicitly instantiate (see next slide) // Some. File. h – full template definition in the header template <typename T> void My. Func(T param) { /* Do something */ } // Some. File. cpp #include "Some. File. h" My. Func(1); // Full definition included in this cpp file, so OK

EXPLICIT TEMPLATE INSTANTIATION ◉ If we know in advance which types will be used

EXPLICIT TEMPLATE INSTANTIATION ◉ If we know in advance which types will be used we can explicitly instantiate them: // Some. File. h template <typename T> void My. Func(T param); --// Some. File. cpp – note this is a cpp file again. template <typename T> void My. Func(T param) { /* Do something */ } // Explicit instantiation, forces creation of code for T=int template void My. Func<int>(int param); --// Some. Other. File. cpp #include "Some. File. h" My. Func(1); // OK, linker will find the explicitly created version

EXAMPLES WITH TEMPLATE CLASSES ◉ Template class fully defined in a header file is

EXAMPLES WITH TEMPLATE CLASSES ◉ Template class fully defined in a header file is typical: // Position. h template <typename T> struct Position { T x, y, z; void Move. X(T distance) { x += distance; } // Code here, not in cpp file }; // Some. File. cpp #include "Position. h" class Position<float> p{10. 0 f, 5. 0 f, 15. 0 f}; p. Move. X(1. 5 f);

EXAMPLES WITH TEMPLATE CLASSES ◉ Explicit instantiation of template class (usage is same) //

EXAMPLES WITH TEMPLATE CLASSES ◉ Explicit instantiation of template class (usage is same) // Position. h template <typename T> struct Position { T x, y, z; void Move. X(T distance); // Code not here, it is in cpp file }; --// Position. cpp template <typename T> void Position<T>: : Move. X(T distance) { x += distance; } // Explicit instantiation of class for T=float class Position<float>;

USE FOR EXPLICIT INSTANTIATION ◉ The typical use of templates involves putting all the

USE FOR EXPLICIT INSTANTIATION ◉ The typical use of templates involves putting all the code in the header file. ◉ Leads to huge header files that makes the interface hard to read. ◉ Explicit instantiation allows the more traditional approach of code in cpp files, interface in header files. ◉ The downside is you must specify all the types that are to be used. ◉ This approach doesn’t really work if the template will be used with arbitrary types. ◉ But is quite appropriate for a class that will use a few known types only (numeric types for example)

TEMPLATE SPECIALISATION ◉ Template specialisation is creating a special version of a template class

TEMPLATE SPECIALISATION ◉ Template specialisation is creating a special version of a template class or function for specific types: template<typename T> class X { /* Basic template version of class */ }; template<typename T> class X<T*> { /* Special version for pointer types */ }; template<> class X<int> { /* Version for ints only */ }; ◉ The pointer types example is a partial specialisation. Still a template class but the types have been partially defined or restricted. The final example is fully specialised. ◉ You can only fully specialise functions. But you can get the effects of partial specialised functions from overloading. See: http: //www. gotw. ca/publications/mill 17. htm template<typename T> void sort(T* elts, int num. Elts) { … } // Generic version // Version of sort for Rectangles (fully specialised) template<> void sort<Rectangle>(Rectangle* elts, int num. Elts) { … }

USE OF TEMPLATE SPECIALISATION ◉ Specialisation allows you to write custom or optimised code

USE OF TEMPLATE SPECIALISATION ◉ Specialisation allows you to write custom or optimised code for specific types you know will be used in advance. ◉ With a generic non-specialised version to catch all the other cases ◉ Watch out when mixing specialised functions and function overloading, they are quite similar. ◉ The rules for deciding which function will be called when there are specialisations / overloads are complex and not always intuitive. ◉ Use template classes rather than functions to avoid such problems ◉ However, the next topic, SFINAE, requires overloading so we need to do it sometimes…

Meta-Programming 2

Meta-Programming 2

conditional, SIMPLE COMPILE-TIME TYPE SELECTION ◉ std: : conditional ◉ Examples: allows you to

conditional, SIMPLE COMPILE-TIME TYPE SELECTION ◉ std: : conditional ◉ Examples: allows you to select a type based on a boolean // Variable declaration, vary type based on condition // size will be a double or float debending on precision conditional<precision==High, double, float>: : type size; // C++14 defines a _t suffix that lets you drop the : : type // Applies to several functions introduced in this section conditional_t<precision==High, double, float> size; // Typedefs make it simpler using Float. Type = conditional_t<precision== High, double, float>; Float. Type size; // A different example, inheritance My. Class : conditional<setting, Class. A, Class. B>: : type {…}

OVERLOAD RESOLUTION ◉ When we call a function and there are several possible functions

OVERLOAD RESOLUTION ◉ When we call a function and there are several possible functions that match the signature, there are specific rules to determine which function to use ◉ Try to use ordinary functions first ◉ Then, broadly speaking, try to use the most specialized template function (the one with the least template variables) ◉ For complete rules see “Best viable function” here: http: //en. cppreference. com/w/cpp/language/overload_resolution ◉ Example on next slide…

OVERLOAD RESOLUTION ◉ Simple example of overload resolution with templates template<typename T, typename U>

OVERLOAD RESOLUTION ◉ Simple example of overload resolution with templates template<typename T, typename U> void f(T x, U y) { … } // (1) template<typename T> void f(T x, T y) { … } // (2) void f(int x, int y) { … } // (3) int main() { f( 1, 2 ); // Calls (3), ordinary functions come first where possible f( 1, 'b'); // Calls (1), the only matching signature f('a', 'b'); // Calls (2), as it is more specialised than (1) }

OVERLOAD RESOLUTION FOR INTROSPECTION ◉ Advanced template work tends to involve type introspection ◉

OVERLOAD RESOLUTION FOR INTROSPECTION ◉ Advanced template work tends to involve type introspection ◉ The code analyses the types it is using to select the code path to use ◉ Here’s a simple example using overload resolution only: template<typename T, typename U> bool is_same() { return false; } template<typename T> bool is_same<T, T>() { return true; } bool a = is_same<int, float>(); // false bool b = is_same<string, string>(); // true (selects the more specialised version) template<typename T, typename U> void Do. Thing(T a, U b) { if (is_same<T, U>()) /* Special code for same types */ }

SFINAE ◉ When checking overloaded functions to find the best match, an invalid function

SFINAE ◉ When checking overloaded functions to find the best match, an invalid function call might be considered, e. g. : template <typename T> typename T: : Result. Type multiply(T a, T b) { return a * b; } // (1) int multiply(int a, int b) { return a * b; } // (2) multiply(4, 5); // Will ultimately call (2), but will also consider (1) ◉ Using int for T gives int: : Result. Type multiply(int a, int b) ◉ This is not a valid function call, int: : Result. Type does not exist ◉ However, the invalid call is simply ignored, it is not an error. ◉ This is called Substitution Failure is Not An Error, SFINAE

SFINAE USES ◉ SFINAE can be used for more powerful introspection and other meta

SFINAE USES ◉ SFINAE can be used for more powerful introspection and other meta -programming techniques. template <typename T> // Used if T has an iterator member, discarded SFINAE otherwise bool has_iterator(typename T: : iterator*) { return true; } // Need to confirm that T: : iterator will be a type since T is unknown template <typename T> //. . . accepts any pointer but is less specialized than above bool has_iterator(T*) { return false; } // so not used for iterator* template <typename T> // Helper function – above functions need a dummy parameter bool has_iterator() { return has_iterator<T>(nullptr); } bool a = has_iterator<int>(); // false bool b = has_iterator<vector<int>>(); // true,

COMPILE TIME INTROSPECTION ◉ Those is_same and has_iterator examples work at runtime ◉ We

COMPILE TIME INTROSPECTION ◉ Those is_same and has_iterator examples work at runtime ◉ We can create more efficient / powerful code by generating compile-time results ◉ Using overload resolution again for a compile-time is_same: template<typename T, typename U> struct is_same { static constexpr bool value = false; }; template<typename T> struct is_same<T, T> { static constexpr bool value = true; }; const bool a = is_same<int, float>: : value; // These values available compile-time const bool b = is_same<string, string>: : value;

COMPILE TIME INTROSPECTION ◉ Compile time version of has_iterator : template <typename T> struct

COMPILE TIME INTROSPECTION ◉ Compile time version of has_iterator : template <typename T> struct has_iterator { template <typename U> static char test(typename U: : iterator *); template <typename U> static double test(U*); static constexpr bool value = sizeof(test<T>(nullptr)) == 1; // Test size of // return value }; const bool a = has_iterator<int>: : value; // Compile-time false const bool b = has_iterator<vector<int>>: : value; // Compile-time true

enable_if ◉ The most common SFINAE- related feature is enable_if, which is a struct

enable_if ◉ The most common SFINAE- related feature is enable_if, which is a struct that may or may not define a type based on a condition ◉ Example of use on next slide // Only if the bool is true will enable_if have a : : type defined template <bool, typename T = void> // void is an optional nicety struct enable_if // We default to a structure without a definition of "type" { }; template <typename T> // Specialisation of the above struct enable_if<true, T> // If the first template parameter is true, "type" will be { // defined to be the same as T, the second template parameter typedef T type; };

enable_if USAGE ◉ enable_if is used to limit the possible definitions of a function

enable_if USAGE ◉ enable_if is used to limit the possible definitions of a function ◉ Use the enable_if somewhere in the function signature: 1. A template parameter (example below), 2. the return value, 3. an actual parameter template <typename T, // Ordinary 1 st template parameter typename enable_if<has_iterator<T>: : value, int>: : type = 0> // 2 nd parameter becomes int or invalid depending on has_iterator result. // If it is invalid SFINAE discards this function, otherwise it is an unused int void Output(T v) { for (auto& e : v) std: : cout << e; } // Function for containers template <typename //. . . so void Output(T v) { int a; T, // No way to make a "default" version of the Output function enable_if<!has_iterator<T>: : value, int>: : type = 0> we use enable_if again with a reversed condition std: : cout << v; } // Function for non-containers vector<int> v; Output(a); Output(v); // Selects function based on has_iterator

META-PROGRAMMING ◉ Although we showed the code for most of the functions in this

META-PROGRAMMING ◉ Although we showed the code for most of the functions in this section, most are already implemented in the STL ◉ Most are in <type_traits> ◉ You probably noticed that the methods required for compile-time meta-programming are unpleasantly intricate, hacky even. ◉ Sadly this is a feature of C++ meta-programming and real world examples can be worse. ◉ For this reason, restrict use of such techniques for important low level support structures / language extensions rather than daily code. ◉ Compile-time evaluation means no overhead, but debugging very difficult

MORE DETAIL A talk that covers these points and more: Cpp. Con 2014: Walter

MORE DETAIL A talk that covers these points and more: Cpp. Con 2014: Walter E. Brown Modern Template Metaprogramming: A Compendium Video Part 1 Video Part 2 Slides