C++

C++ - Templates

Non-Type Template Parameters, requires, concept

Posted by Rico's Nerd Cluster on February 10, 2023

typename Is Required By “Dependent Names”

Dependent names are names that are dependent on the type of a template parameter. To instantiate instances of them, or to simply use them as types, one needs to use the keyword typename.

Consider the below snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
// namespace MyClass {  // can't be namespace, because typename T expects a type, such as class/struct
struct MyClass {
    struct constant {
        static constexpr int value = 42;
    };
};

template <typename T>
void exampleFunction() {
    typename T::constant myConstant; // No 'template' keyword needed
    // value is a static member, and it's accessible to the class itself
    std::cout << "Value: " << myConstant.value << std::endl;
}
  • :: means type within a type
  • typename T can’t take in a namespace. It has to be a type. It is to create an object of the type T
  • typename T::constant myConstant can access static members, like myConstant.value

Non-Type Template Parameters

Non Type template parameters are compile time constants that customizes a struct. Some notes include:

  • Foo<9> is a different instantiation of Foo<10>
1
2
3
4
5
6
7
8
9
10
11
12
template <int N>
struct Foo{
    static const int i = N;
};

int main()
{
    Foo<9> f;
    std::cout << f.i << std::endl;
    // Print the static class member
    std::cout << Foo<10>::i << std::endl;
}

Specialized Template Function To Provide One Implementation

1
2
3
4
template <>
inline size_t hash_vec<2>::operator()(const Eigen::Matrix<int, 2, 1>& v) const {
    return size_t(((v[0] * 73856093) ^ (v[1] * 471943)) % 10000000);
}

template Keyword Is Required To Disambiguate

Consider the below snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <type_traits> // For std::remove_pointer

template <class T>
void f(T &x) {
    // Resolve the base type of T if T is a pointer
    using BaseType = typename std::remove_pointer<T>::type;
    // So BaseType is FTemplates

    // template function variable() needs to be accessed with an explicit "template"
    // BaseType::template is needed to indicate constant is a nested template inside Basetype
    x->template variable<typename BaseType::template constant<3>>();
}

struct FTemplates {
    // Nested template
    template <int N>
    struct constant {
        static constexpr int value = N;
    };

    // Member template
    template <typename T>
    void variable() {
        std::cout << "variable: " << T::value << std::endl;
    }
};

int main() {
    FTemplates f_tmp;
    FTemplates *f_ptr = &f_tmp;
    // Call f with a pointer to f_templates
    f(f_ptr); // Outputs: variable: 3
    return 0;
}

Concepts For T [C++ 20]

When enforcing type constraints, in C++ 17, one needs to use std::enable_if. That results in a longer template signature. In C++ 20, one just needs to use Concepts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <type_traits>
#include <concepts>

template <std::integral T>
void print_20(T x) {
    std::cout << x << " is an integer\n";
}

template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
print_17(T x) {
    std::cout << x << " is an integer\n";
}

int main(){
    // print_17("123");   // See compiler error: no matching function for call to ‘print(const char [4])’
    print_17(123);     // can compile fine
    print_20(456);
}
  • ✅ More readable.
  • ✅ More efficient (compiler checks constraints upfront).
    • In C++17, std::enable_if is checked during template instantiation, where errors are usually versbose.
    • In C++20, concepts are checked before template instantiation. Errors are clearer.

The requires Clause [C++20]

Puts constraints on template parameters. It takes a “boolean concept”

  • compile-time predicate std::integral<T> is equivalent to concept integral = std::is_integral_v<T>;
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
T add(T a, T b) requires std::integral<T> {
    return a + b;
}

// Can be before the function signature, or after
template<typename T>
requires std::integral<T>
T add(T a, T b) {
    return a + b;
}
  • Or, (dim == 2):
1
2
3
template <int dim>
requires (dim == 2 || dim == 3)
void foo(int i){}
  • Commonly, it works with a custom concept. Otherwise, we will have to use SFINAE, with enable_if, type_traits, which come with lower readability:
1
2
3
4
5
6
7
8
9
10
11
12
 template <class T>
 concept Check = requires {
     T().clear();
 };
 
 template <Check T>
 struct G {};
 
 G<std::vector<char>> x;      // 成功
 G<std::string> y;            // 成功
 // 由于std::array没有clear操作,所以编译失败
 G<std::array<char, 10>> z;   // 失败
  • type = std::conditional_t<predicate, Type1, type2>
1
2
3
4
5
6
7
8
9
10
11
12
13
template<bool Flag>
std::conditional_t<Flag, int, double> getValue() {
    if constexpr (Flag)
        return 42;  // int
    else
        return 3.14;  // double
}

template <int dim>
class NearestNeighborGrid {
public:
    using Point = std::conditional_t<dim == 2, Eigen::Vector2f, Eigen::Vector3f>;
};

Template Alias

One not super commonly used feature in C++ is template alias.

1
2
3
4
5
template <typename T>
struct A;

template <typename T>
using B = A<T>;

One note is that C++ standard has NOT specified if an template alias is the same as the template itself. On some compilers (e.g., x86-64 clang 10.0.1), the below code snippet can compile fine. But on some others, A and B will be treated as the same template, so the template specialization would yield errors (x86-64 gcc 14.1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
struct A;

template <typename T>
using B = A<T>;

template <template<typename> class Cont>
struct C;

template<>
struct C<A>{};


template<>
struct C<B>{};

int main(){}


Gitalking ...