Chapter5 Tricky Basics

typename

Prefer typename to class.
In general, typename has to be used whenever a name that depends on a template parameter is a type.

Zero Initialization

For fundamental types such as int, double, or pointer types, there is no default constructor that initializes them with a useful default value, Instead, any noninitialized local variable has an undefined value.

void foo()
{
  int x; // x has undefine value
  int* ptr; // ptr points to anywhere (instead of nowhere)
}
// template suffering the same issue too.
template<typename T> void foo()
{
  T x; // x has undefined value
}

For this reason, it’s possible to call explicitly a default constructor for built-in types that initializes them with zero(or false for bool or nullptr for pointers). As a consequence, you can ensure proper initialization even for built-in types by using {} (e.g T x = T{}). This way of initialization is called value initialization, which means to either call a provided constructor or zero initialize an object. This even works if the consturctor is explicit.

Before C++11, the syntax to ensure proper initialization was T x = T(). Prior to C++17, this mechanism only worked if the constructor selected for the copy-initialization is not explicit. In C++17, mandatory copy elsion avoids the limitation and either syntax can work, but the {} notation can use an initializer-list constructor if no default constructor is available.

Using this->

For class templates with base classes that depend on template parameters, using a name X by itself is not always equivalent to this->X, even though a member X is inherited.

void bar()
{
  std::cout << "free function bar()\n";
}

template<typename T> struct Base
{
  void bar()
  {
    std::cout << "member function Base::bar()\n";
  }
};

template<typename T> struct Derived : Base<T>
{
  void foo()
  {
    bar(); // call free function
    this->bar(); // call derived member function
  }
};

For the moment, as a rule of thumb, we recommend that you always qualify any symbol that is declared in a base that is somehow dependent on a template parameter with this-> or Base<T>::.

Templates for Raw Arrays and String Literals

NOTE: raw array and string literals always need more attention.
先来看一个列子:

template<typename T, int N, int M>
bool less(T(&a)[N], T(&b)[M])
{
  for(int i = 0; i < N && i < M; ++i)
  {
    if(a[i] < b[i])
    {
      return true;
    }
    if(a[i] > b[i])
    {
      return false;
    }
  }
  return N < M;
}

尝试一下调用:

int a[] = {1, 2, 3};
int b[] = {2, 3};
less(a, b); // Ok
less("aa", "bb"); // Ok
const char* sa = "aa";
const char* sb = "sb";
less(sa, sb); // Error

对于前两个调用,less分别实例化为,T是int, N是3, M是2,以及T是const char, N是3, M是3。而对于第三个调用,出现如下错误

x.cc: In function ‘int main()’:
x.cc:51:14: error: no matching function for call to ‘less(const char*&, const char*&)’
   less(sa, sb);
              ^
x.cc:11:6: note: candidate: template<class T, int N, int M> bool less(T (&)[N], T (&)[M])
 bool less(T(&a)[N], T(&b)[M])
      ^~~~
x.cc:11:6: note:   template argument deduction/substitution failed:
x.cc:51:14: note:   mismatched types ‘T [N]’ and ‘const char*’
   less(sa, sb);

原因在于,模板参数以引用传递,所以参数不会decay,原本是const char*的类型将保持不变。解决很简单:

注意: "aa"的类型并不是const char*而是const char[3],至于为什么能赋值给const char*是因为语言规则允许自动转换将const char[]decay为const char*

由于这种情况,有时候不得不为*不定长数组*做特化,下面就列举所有可能的特化

#include <iostream>

using std::cout;

// primary template
template<typename> struct Foo;

// partial specialization for arrays of known bounds
template<typename T, size_t N> struct Foo<T[N]>
{
  static void print()
  {
    cout << "T[" << N << "]\n";
  }
};

// partial specialization for references to arrays of known bounds
template<typename T, size_t N> struct Foo<T(&)[N]>
{
  static void print()
  {
    cout << "T(&)[" << N << "]\n";
  }
};

// partial spec for arrays of unknown bounds
template<typename T> struct Foo<T[]>
{
  static void print()
  {
    cout << "T[]\n";
  }
};

// partial spec for references to  arrays of unknown bounds
template<typename T> struct Foo<T(&)[]>
{
  static void print()
  {
    cout << "T(&)[]\n";
  }
};

// partial spec for pointers
template<typename T> struct Foo<T*>
{
  static void print()
  {
    cout << "T*\n";
  }
};

template<typename T, typename U, typename V>
void foo(int a1[233], int a2[], int (&a3)[233], int (&x1)[], T x2, U& x3, V&& x4)
{
  Foo<decltype(a1)>::print();
  Foo<decltype(a2)>::print();
  Foo<decltype(a3)>::print();
  Foo<decltype(x1)>::print();
  Foo<decltype(x2)>::print();
  Foo<decltype(x3)>::print();
  Foo<decltype(x4)>::print();
}

int main()
{
  int a[233];
  Foo<decltype(a)>::print();

  extern int x[];
  Foo<decltype(x)>::print();

  foo(a, a, a, x, x, x, x);
}

int x[] = {2, 3, 3};

以下是编译及运行结果:
Screenshot_20180219_202316 如你所见,实际上根据语言规则,调用参数声明为array的(无论长度是否已知)都是pointer type。同时还应该注意到,边界未知的array作为模板参数可以是imcomplete type(比如:extern int a[]),并且还可以是通过引用传递(比如:int(&)[])。

Variable Templates

自C++14起,变量也可以作为模板,例如:

template<typename T> T my_type{}; // zero initialization

my_type<int> = 233;
std::cout << my_type<int> << '\n'; // print 233
my_type<float> = 2.33;
std::cout << my_type<float> << '\n'; // print 2.33

template<size_t N> std::array<int, N> my_arr{};
my_arr<3>[0] = 2;
my_arr<3>[1] = 3;
my_arr<3>[2] = 3;

std::cout << my_arr<3>[0] << '\n';

template<auto Ha> constexpr decltype(Ha) Moha = Ha;
std::cout << Moha<"+1s"> << '\n'; // Error

注意: 这里要提到一点,非类型模板参数只支持整型,所以上面Moha<"+1s">是不能通过编译的,同理Moha<2.33>也是错误的。

那么这东西有什么用呢? 如果你了解过C++17,你可能已经知道了_v后缀,比如:std::is_same_v,它的实现就是利用了variable templates

template<typename T, typename U> struct is_same
{
  constexpr static bool value = false;
};

template<typename T> struct is_same<T, T>
{
  constexpr static bool value = true;
};

template<typename T, typename U> constexpr bool is_same_v = is_same<T, U>::value;

Template Template Parameters

即使你对元编程不熟悉,你也应该是到这个东西,因为你早已使用了无数次了。 参考之前写的Traits and Policies。需要注意的一点是, 从C++17开始可以使用typename来替代class,比如

template<typename T, template<typename> typename Policy>
// equal to 
template<typename T, template<typename> class Policy>

用途也是上面链接的那样。