• Home
  • /
  • Blog
  • /
  • Types-, Non-Types, and Templates as Template Parameters

February 27, 2019

I assume you saw the additional keywords typename or template used before a name in a template. Me too. Honestly, I was pretty surprised. Today’s post is about dependent names and various template parameters.

 

 optical illusion 311130 1280

Before I write about dependent names, I should write about template parameters.

Template Parameter

Template parameters can be types, non-types, and templates.

Types

Okay, types are the most often used template parameters. Here are a few examples:

std::vector<int> myVec;
std::map<std::string, int> myMap;
std::lock_guard<std::mutex> myLockGuard;

Non-Types

Non-types can be a

  • lvalue reference
  • nullptr
  • pointer
  • enumerator
  • integral type

Integrals are the most used non-types. std::array is the typical example because you have to specify at compile time the size of a std::array:

std::array<int, 3> myArray{1, 2, 3};

Templates

Templates can be template parameters. In this case, they are called template parameters. The container adaptors std::stack, std::queue, and std::priority_queue use, per default, a std::deque to hold their arguments, but you can use a different container. Their usage is straightforward.

 

std::stack<int> stack1;
stack1.push(5);
    
std::stack<double, std::vector<double>> stack2;
stack2.push(10.5);

 

Their definition may look a little bit weird.

 

// templateTemplateParameters.cpp

#include <iostream>
#include <list>
#include <vector>
#include <string>

template <typename T, template <typename, typename> class Cont >   // (1)
class Matrix{
public:
  explicit Matrix(std::initializer_list<T> inList): data(inList){  // (2)
    for (auto d: data) std::cout << d << " ";
  }
  int getSize() const{
    return data.size();
  }

private:
  Cont<T, std::allocator<T>> data;                                 // (3)                               

};

int main(){

  std::cout << std::endl;

                                                                    // (4)
  Matrix<int, std::vector> myIntVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 
  std::cout << std::endl;
  std::cout << "myIntVec.getSize(): " << myIntVec.getSize() << std::endl;

  std::cout << std::endl;

  Matrix<double, std::vector> myDoubleVec{1.1, 2.2, 3.3, 4.4, 5.5}; // (5)
  std::cout << std::endl;
  std::cout << "myDoubleVec.getSize(): "  << myDoubleVec.getSize() << std::endl;

  std::cout << std::endl;
                                                                    // (6)
  Matrix<std::string, std::list> myStringList{"one", "two", "three", "four"};  
  std::cout << std::endl;
  std::cout << "myStringList.getSize(): " << myStringList.getSize() << std::endl;

  std::cout << std::endl;

}

 

Matrix is a simple class template that can be initialized by a std::initializer_list (line 2). A Matrix can be used with a std::vector (line 4 and line 5) or a std::list (line 6) to hold its values. So far, nothing special. 

templateTemplateParameters

But hold, I forget to mention lines 1 and line 3. Line 1 declares a class template that has two template parameters. Okay, the first parameter is the type of the elements, and the second parameter stands for the container. Look at the second parameter: template <typename, typename> class Cont >. This means the second template argument should be a template requiring two template parameters. The first template parameter is the type of elements the container stores, and the second is the defaulted allocator a container of the standard template library has. Even the allocator has a default value, such as in the case of a std::vector. The allocator depends on the type of elements.

template<
    class T,
    class Allocator = std::allocator<T>
> class vector;

 

Line 3 shows the usage of the allocator in this internally used container. The matrix can use all containers, which are of the kind: container< type of the elements, allocator of the elements>. This is true for the sequence containers such as std::vector, std::deque, or std::liststd::array and std::forward_list would fail because std::array needs an additional non-type for specifying its size at compile-time, and std::forward_list does not support the size method.

Preparation done. Now, I can write about dependent names.

Dependent Names

First of all. What is a dependent name? A dependent name is essentially a name that depends on a template parameter. Let me show what that means. Here are a few examples based on cppreference.com:

 

template<typename T>
struct X : B<T> // "B<T>" is dependent on T
{
    typename T::A* pa; // "T::A" is dependent on T
    void f(B<T>* pb) {
        static int i = B<T>::i; // "B<T>::i" is dependent on T
        pb->j++; // "pb->j" is dependent on T
    }
};

 

Now, the fun starts. A dependent name can be a type, a non-type, or a template parameter. The name lookup is the first difference between non-dependent and dependent names.

  • Non-dependent names are looked up at the point of the template definition.
  • Dependent names are looked up when the template arguments are known. This means at the point of template instantiation.

If you use a dependent name in a template declaration or template definition, the compiler has no idea whether this name refers to a type, a non-type, or a template parameter. In this case, the compiler assumes that the dependent name refers to a non-type, which may be wrong. This is the case in which you have to help the compiler.

Before I show you two examples, I must add an exception to this rule. You can skip my following few words if you want to get a general idea and jump directly to the following subsection. The exception is: if the name refers in the template definition to the current instantiation, the compiler can deduce the name at the point of the template definition. Here are a few examples:

 

template <class T> class A {
    A* p1;    // A is the current instantiation
    A<T>* p2; // A<T> is the current instantiation
    ::A<T>* p4; // ::A<T> is the current instantiation
    A<T*> p3; // A<T*> is not the current instantiation
};
template <class T> class A<T*> {
    A<T*>* p1;  // A<T*> is the current instantiation
    A<T>* p2;   // A<T> is not the current instantiation
};
template <int I> struct B {
    static const int my_I = I;
    static const int my_I2 = I+0;
    static const int my_I3 = my_I;
    B<my_I>* b3;  // B<my_I> is the current instantiation
    B<my_I2>* b4; // B<my_I2> is not the current instantiation
    B<my_I3>* b5; // B<my_I3> is the current instantiation
};

 

Finally, I came to the critical idea of my post. If a dependent name could be a type, a non-type, or a template, you have to give the compiler a hint.

Use typename if the Dependent Name is a Type

After such a long introduction, the following program snippet makes it pretty clear.

 

template <typename T>
void test(){
    std::vector<T>::const_iterator* p1;          // (1)
    typename std::vector<T>::const_iterator* p2; // (2)
}

 

Without the typename keyword in line 2, the name std::vector<T>::const_iterator in line 2 would be interpreted as a non-type and, consequently, the * stands for multiplication and not for a pointer declaration. Exactly this is happening in line (1).

Similarly, if your dependent name should be a template, you have to give the compiler a hint.

Use .template if the Dependent Name is a Template

Honestly, this syntax looks a little bit weird.

 

template<typename T>
struct S{
    template <typename U> void func(){}
}
template<typename T>
void func2(){
    S<T> s;
    s.func<T>();             // (1)
    s.template func<T>();    // (2)
}

 

Same story as before. Compare lines 1 and 2. When the compiler reads the name s.func (line 1), it interprets it as non-type. This means the < sign stands for the comparison operator but not opening the square bracket of the template argument of the generic method func. In this case, you must specify that s.func is a template, such as in line 2: s.template func

Here is the essence of this post in one sentence: When you have a dependent name, use typename to specify that it is a type or .template to specify that it is a template.

What’s next?

The following rules in the C++ Core Guidelines are C-style programming and source files. Let’s see in my next post what this means.

 

 

 

Leave a Reply


Your email address will not be published. Required fields are marked

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Related Posts