Template is a meta-programming feature that allows us to write code without writing code.
template<typename T>
T GetMax(T a, T b)
{ return a > b ? a : b; }
As an example, this function template has requirements: (implicit requirements)
We use the "<" operator, so the type should support that operator.
We pass and return by value, so the type should be copy-able .
C++20 introduced concepts to specify the requirements for templates making cryptic error messages meaningful whenever we have a template compilation error.
template<typename T>
concept SupportsLessThan = requires(T x) { x < x; };
and now the GetMax function can be written as:
template<typename T>
requires std::copyable<T> && SupportsLessThan<T>
T GetMax(T a, T b)
{ return a > b ? a : b; }
We have to explicitly specify the type when declaring an object from a templated class.
Class Template Argument Deduction (CTAD): C++17
std::vector<int> v { 0, 8, 15 }; // since C++11
std::vector v2 { 0, 8, 15 }; // deduces std::vector<int>
std::vector v3 { "all", "right"}; // deduces std::vector<const char*>
std::vector v4 { v.begin(), v.end() }; // deduces std::vector<vector<int>::iterator> // a vector of iterators to another vector no the // elements and that's probably not what we want
std::vector<int>v5 { v.begin(), v.end() }; // that copies the elements since we explicitly defined vector<int>
template<typename T, typename... Types> // typename... represents multiple types
void print(T firstArg, Types... args) // Types... represents multiple arguments of different types
{ std::cout << sizeof...(args) << '\n'; } // this will tell us the number of arguments // by using the operator sizeof...
if we need to check something in the template world, we have a compile time if statement: if constexpr(condition){}.
if we want to write a generic function that would add any element of any type to any type of a collection just like that:
void Add(auto& coll, const auto& val)
{ coll.push_back(val); }
Note: this function is an abbreviated template function that could also be written as
template<typename T, typename ElemT>
void Add(T& coll, const ElemT& val)
{ coll.push_back(val); }
We can also write another function with insert() in case the collection type does not support push_back().
void Add(auto& coll, const auto& val)
{ coll.insert(val); }
But now we have an ambiguity problem where the compiler wouldn't be able to resolve which one to call. Overload Resolution will not be able to determine that one function is better fit than the other. To fix that we use concepts as type constraints. That way the overload resolution can prefer one implementation over the other.
First we define the concept:
template<typename Coll>
concept HasPushBack = requires(Coll c, Coll::value_type v)
{ c.push_back(v); };
Then we write the constrained function:
This way(the long way):
template<typename T, typename ElemT>
requires HasPushBack<T>
void Add(T& coll, const ElemT& val)
{ coll.push_back(val); }
OR this way (the abbreviated way):
void Add(HasPushBack auto& coll, const auto& val)
{ coll.push_back(val); }
Now overload resolution can tell and prefer one function over the other.
Another way to achieve same result with one function only is to use the compile time if statement:
void Add(HasPushBack auto& coll, const auto& val)
{
if constexpr (requires{ coll.push_back(val); }) // compile time if
{ coll.push_back(val); }
else
{ coll.insert(val); }
}
Generic code is very useful and can save a lot of time when used the right way and I think this set of new features such as concepts are opening new doors in the template world.
I will have to see how all these feature impact compilation time and I will definitely put them to good use accordingly.