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*
的类型将保持不变。解决很简单:
- 为
less
写一个const char*
的重载
- 将
const char*
写为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};
以下是编译及运行结果:
如你所见,实际上根据语言规则,调用参数声明为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>
用途也是上面链接的那样。