C++ Templates and Concepts Cheat Sheet

The SciEng library makes extensive use of templates and the new C++20 “concepts” feature. But the syntax of concepts is still new to me, so I thought I’d create a cheat sheet to remind me. If you are new to templates and concepts, then I recommend proper tutorial, not this page. This page is just if you need some syntax reminders.
However, if you want to know what templates and concepts do, just so you can decide whether to go hunting for some tutorials, here’s a quick description.
C++ is strongly typed, meaning the type of everything is known at compile time. But sometimes, you don’t know what a type will be at write time. For example, you might want to write a function that can accept doubles, floats or even std::complex types or you might want to write a container class that can hold any type. That’s where templates are used. It can also be useful if you want a value to be constant at compile time, but you don’t know it at write time, such as when you are creating an array on the stack rather than the heap.
Concepts are a new C++20 feature, which allow you to limit the types you can pass as a templated type, based on the features they posses. For example, if you had a templated maths function, you could apply the std::floating_point concept, which would mean a user couldn’t accidentally pass an integer or some other random class into your function.
A really useful thing about templates and concepts is that they are dealt with at compile time, not run time. This means they don’t cost any run time execution time. You can also specialize templates, to give different behaviour for different types. This is a bit like overloading a function. It turns out that this has spawned a whole branch of C++ called template metaprogramming, where branches and calculations are resolved at compile time, rather than run time.
For template specialization I recommend this tutorial Template Specialization in C++
For concepts, I recommend these tutorials: How to write your own C++ concepts? Part I. How to write your own C++ concepts? Part II.
The cheat sheet code
This set of code should compile in any modern C++ compiler and acts as a syntax guide for templates and concepts.
#include<array>
#include<type_traits>
#include<tuple>
//I use constexpr to ensure I can check these things all work as expected at compile time
//constexpr is not required for templates, but search on youtube for constexpr all the things
//basic template declarations
//Can use typename equivalently to class
template<class T, class U, int N>
constexpr auto myFunction(T thing1, std::array<U, N> thing2)
{
return std::make_pair(thing1, thing2);
}
template<class T, int N>
struct MyContainer
{
using value_type = T;
static constexpr size_t size = N;
};
//specializations
template<>
constexpr auto myFunction<double, double, 3>(double thing1, std::array<double, 3> thing2)
{
return std::make_pair(thing1, std::array<double, 3>{thing1, thing1, thing1});
}
template<>
constexpr auto myFunction(float thing1, std::array<float, 3> thing2) //can omit the parameters in angle backets if they can be deduced
{
return std::make_pair(thing1, std::array<float, 3>{0.0f, 0.0f, 0.0f});
}
template<>
struct MyContainer<double, 0>
{
using value_type = double;
static constexpr size_t size = 1; //for this specialization, make the size 1
};
//This is an overload, not a specialization, it won't be called if someone tries myFunction<float, float, 3>()
//but will be called if someone tries myFunction()
constexpr auto myFunction(int thing1, std::array<int, 3> thing2)
{
return std::make_pair(thing2[thing1], thing2);
}
//this will not be called by myFunction() because an overload takes precidence over a specialization
template<>
constexpr auto myFunction(int thing1, std::array<int, 3> thing2) //can omit the parameters in angle backets if they can be deduced
{
return std::make_pair(thing2[thing1], std::array<int, 3>{thing1, thing1, thing1});
}
//partial specialization. Only doable for classes. There is no requirement for the order
template<int N>
struct MyContainer<bool, N>
{
//make bools a bit array
using value_type = uint8_t;
static constexpr size_t size = N/8 + (N%8 > 0 ? 1 : 0);
};
//you can now use auto in a template and also in arguments, which
// is useful particulary for passing functions at compile time
template<auto FUNC>
constexpr auto applyFunc(auto v)
{
return FUNC(v);
}
//Use an already defined concept
//concepts in a requires statement
template<class T, class U>
constexpr double add(T t1, U t2)
requires (std::floating_point<T> && std::floating_point<U>)
{
return double(t1 + t2);
}
//a version without the constraint that returns int
template<class T, class U>
constexpr int add(T t1, U t2)
{
return int(t1 + t2);
}
//concepts used like types
template<std::floating_point T, std::floating_point U>
constexpr double multiply(T t1, U t2)
{
return t1 * t2;
}
template<class T, class U>
constexpr int multiply(T t1, U t2)
{
return int(t1 * t2);
}
//requires statements go straight after the template part of the definition for classes
template<class T> requires(std::floating_point<T>) //std::floating_point is a concept, but you can use compile-time constants and traits
class MyFloatingPointBasedClass
{
};
//can use this form too
template<std::same_as<int> T>
class MyIntBasedClass
{
};
//Create a concept
//from other concepts and compile time constants
template<class T>
concept IsFloatingNotDouble = std::floating_point<T> && !std::is_same_v<T, double>;
//using a requires expression, essentially creates an object of the templated type and all the code
//in the braces must compile for the concept to be true
template<class T>
concept IsArray = requires(T t)
{
t.size();
t[0];
};
//using a requires expression with std::same as
template<class T>
concept IsDoubleArray = requires(T t)
{
t.size();
{ t[0] } -> std::same_as<double&>; // the return type of t[0] must be a reference to double
//{ t[0] } -> std::convertible_to<double>; // this gives a looser constraint, double, float, int or references to these would all be valid returns
};
//Sometimes you might not want to create an object
template<class T>
concept HasValueType = requires
{
typename T::value_type;
};
int main()
{
//Test all the things work as expected
//functions
static_assert(myFunction(4, std::array<double, 1>{2.0}).second[0] == 2.0); //standard template, just returns the two objects as a pair
static_assert(myFunction(4.0, std::array<double, 3>{2.0, 2.0, 2.0}).second[0] == 4.0); //when specialised for double std::array<double, 3> the array in the pair is full of the value passed
static_assert(myFunction(4.0f, std::array<float, 3>{2.0, 2.0, 2.0}).second[0] == 0.0f); //when specialised for float std::array<float, 3> the array in the pair is full of 0.0f
static_assert(myFunction(1, std::array<int, 3>{1, 2, 3}).second[1] == 2); // this calls the override int, std::array<int, 3> function
static_assert(myFunction<int, int, 3>(1, std::array<int, 3>{1, 2, 3}).second[1] == 1); //this calls the specialization for int, std::array<int, 3>
//classes/structs
static_assert(MyContainer<float, 2>::size == 2); //standard version
static_assert(std::is_same_v<MyContainer<float, 2>::value_type, float>); //standard version
static_assert(MyContainer<double, 2>::size == 2); //standard version
static_assert(MyContainer<double, 0>::size == 1); //double, 1 specialization
static_assert(MyContainer<bool, 12>::size == 2); //bool partial specialization
static_assert(std::is_same_v<MyContainer<bool, 12>::value_type, uint8_t>); //bool partial specialization
//constraints
auto const sum1 = add(1.0, 2.5f); //double float - matches constraint
auto const sum2 = add(1, 1.5); //int double, doesn't match contraint
static_assert(std::is_same_v<decltype(sum1), const double>);
static_assert(std::is_same_v<decltype(sum2), const int>);
auto const product1 = multiply(1.0, 2.5f); //double float - matches constraint
auto const product2 = multiply(1, 1.5); //int double, doesn't match contraint
static_assert(std::is_same_v<decltype(product1), const double>);
static_assert(std::is_same_v<decltype(product2), const int>);
static_assert(IsFloatingNotDouble<float>);
static_assert(!IsFloatingNotDouble<double>);
static_assert(IsArray<std::array<int, 3>>); //a std::array<int,3> matches the IsArray concept
static_assert(!IsArray<double>);// a plain double does not match the IsArray concept
static_assert(IsDoubleArray<std::array<double, 10>>);//a std::array<double,10> matches the IsArray concept
static_assert(!IsDoubleArray<std::array<int, 10>>);//a std::array<int,10> does not match the IsArray concept
static_assert(HasValueType<std::array<int, 3>>);
static_assert(!HasValueType<double>);
const int v = 1;
const int w = applyFunc <[](int i) {return i + 1;}> (v); //pass a lamda in as the auto template parameter
static_assert(w == 2);
}
Phil Rosenberg