在定义类模板的时候,可以给模板参数指定一个默认值,
template <typename Tp, std::size_t kNum = 5>
class Foo
{
public:
std::array<Tp, kNum> arr;
};
如上所示,第二个是非类型模板参数 std::size_t,其默认值为 5。我们可以这样使用 Foo,
Foo<char, 20> foo_char20; // arr 为 std::array<char, 20>
Foo<char> foo_char5; // arr 为 std::array<char, 5>,由于默认值的存在,可以不用指定第二个参数
还可以对 Foo 进行偏特化,
template <std::size_t kNum>
class Foo<int, kNum>
{
public:
std::array<int, kNum> arr;
};
此时,对于偏特化 Foo 的第二个模板参数 kNum,其默认值依然是 5,
Foo<int, 20> foo_int20; // 对偏特化版本进行实例化,arr 为 std::array<int, 20>
Foo<int> foo_int5; // 对偏特化版本进行实例化,arr 为 std::array<int, 5>
注意,在偏特化的 Foo 中,kNum 不可以再指定默认值,只能从原型那里获取默认值。
std::unique_ptr 模板参数的默认值
template <typename _Tp>
struct default_delete
{
void operator()(_Tp* ptr) const
{
delete ptr;
}
};
template <typename _Tp, typename _Dp = std::default_delete<_Tp>>
class unique_ptr
{
public:
typedef _Tp element_type;
typedef _Dp deleter_type;
};
以上是 GCC 中 std::default_delete 和 std::unique_ptr 代码的简化。其中 std::default_delete 是标准库提供的用于删除指针的类,大多数情况用在函数对象上。
可以看到 std::unique_ptr 的第二个模板参数的默认值是实例化的 std::default_delete<_Tp>,所以在使用的时候只需要提供第一个模板参数就可以,
std::unique_ptr<int> uniptr_int;
当到达了 uniptr_int 作用域结束的地方,析构函数会利用 _Dp 来删除内部实际的指针。
同时,标准库也提供了数组版本的 std::default_delete 和 std::unique_ptr,即提供了一个数组版本的偏特化,
template <typename _Tp>
struct default_delete<_Tp[]>
{
void operator()(_Tp* ptr) const
{
// 对数组指针进行释放
delete[] ptr;
}
};
template <typename _Tp, typename _Dp>
class unique_ptr<_Tp[], _Dp>
{
public:
typedef _Tp element_type;
typedef _Dp deleter_type;
};
但是,如果仔细观察两个版本的 std::unique_ptr,会发现一个与常识矛盾的地方。
数组版本的第二个模板参数的默认值与原型是相同的,所以可以这样子看待数组版本的偏特化,
// 实际上,这样写是无法通过编译的,偏特化版本参数的默认值必须从原型获得,即这个默认值是不能够写出来的
template <typename _Tp, typename _Dp = std::default_delete<_Tp>>
class unique_ptr<_Tp[], _Dp> { };
如果我们这样子使用 std::unique_ptr<int[]> uniptr_intarr;
,那么 _Tp 是 int,进而 _Dp 就是 std::default_delete<int>;如果到达了 uniptr_intarr 作用域的尽头,析构函数为释放指针调用的是 delete ,而不是 delete[],这样会造成内存泄漏。
可是,通过进一步的测试,发现实际情况与上述的理解是不一样的,
std::cout << std::boolalpha
<< std::is_same<std::unique_ptr<int[]>::deleter_type, std::default_delete<int[]>>::value << '\n';
输出是 true
,也就是说数组版本的 _Dp 是 std::default_delete<int[]>。
这是怎么一回事呢?
原因是我们看待模板方角度出了问题,应该把重点放在实例化模板的地方。
对于 std::unique_ptr 模板原型和数组偏特化版本,我们过于看重 template <typename _Tp, typename _Dp = std::default_delete<_Tp>>
和 template <typename _Tp, _Dp>
这两行,从而认为数组偏特化版本的第二个模板参数默认值就是直接从原型中拷贝过来的。实际上,这两行是模板声明的必要语法,作用是告诉编译器下面是个模板,还确定了需要推导的模板参数的个数;而编译器用来推导模板参数的是 class unique_ptr
和 class unique_ptr<_Tp[], _Dp>
这两行,而第一个可以理解成 class unique_ptr<_Tp, _Dp>
的简略写法(这种简略写法将在下文证实)。
关注点转移之后就可以解释默认值的含义了。模板参数的默认值是告诉编译器,在推导参数的时候该采取怎样的行为。std::unique_ptr 的第二个模板参数的默认值 std::default_delete<_Tp> 的含义是,将 class unique_ptr<_Tp, _Dp>
的第一个实参 _Tp 当作 std::default_delete 的模板参数。对于 std::unique_ptr<int> uniptr_int;
第一个实参是 int,所以第二个实参就是 std::default_delete<int>;由于数组偏特化版本采用了原型的含义,对于 std::unique_ptr<int[]> uniptr_int
,第一个实参是 int[],请注意我们关注的是 class unique_ptr<_Tp[], _Dp>
而不是 template <typename _Tp, typename _Dp>
,前者的第一个实参是 int[],而后者的第一个实参是 int,因此第二个实参是 std::default_delete<int[]>。
可以再举一个简单的例子来进一步佐证,
template <typename Tp1, typename Tp2 = Tp1>
class DefaultArgTest
{
public:
using type = Tp2;
DefaultArgTest() {
std::cout << __PRETTY_FUNCTION__ << '\n';
}
};
// 数组版本的偏特化
template <typename Tp1, typename Tp2>
class DefaultArgTest<Tp1[], Tp2>
{
public:
using type = Tp2;
DefaultArgTest() {
std::cout << __PRETTY_FUNCTION__ << '\n';
}
};
DefaultArgTest<int> dat_int;
DefaultArgTest<int[]> dat_intarr;
std::cout << std::boolalpha
<< std::is_same<decltype(dat_int)::type, int>::value << '\n'
<< std::is_same<decltype(dat_intarr)::type, int[]>::value << '\n';
__PRETTY_FUNCTION__ 是 GCC 编译器添加到每个函数的静态字符串常量(如果用到的话),字符串的内容是该函数的签名。
上述代码的输出是,
DefaultArgTest<Tp1, Tp2>::DefaultArgTest() [with Tp1 = int; Tp2 = int]
DefaultArgTest<Tp1 [], Tp2>::DefaultArgTest() [with Tp1 = int; Tp2 = int []]
true
true
从第一条输出可以证实上文中的简略写法,即 class DefaultArgTest
是 class DefaultArgTest<Tp1, Tp2>
的简略写法。但是在代码中我们只能写成前者,因为每个偏特化版本都需要原型作为基础,如果我们写成后者,编译器会认为这是一个偏特化版本,因而就没有了原型,这就造成了冲突。
std::void_t
std::void_t 是 C++17 标准库中提供的,定义于 <type_traits>,其原型如下,
template <typename...>
using void_t = void;
std::void_t 的作用是,利用 SFINAE 来判定某些类型是否满足我们的要求,
template <typename, typename = std::void_t<>>
struct HasTypeMember : std::false_type { };
template <typename Tp>
struct HasTypeMember<Tp, std::void_t<typename Tp::type>> : std::true_type { };
上述代码中,有两点需要注意的:之所以要利用默认值,是为了在使用 HasTypeMember 的时候只提供一个实参即可 HasTypeMember<SomeType>::value
;如果偏特化版本没有出错,原型和偏特化版本的第二个参数都是 void,此时编译器依然会选择偏特化版本,因为偏特化版本比原型更加精确,详见partial ordering。
如此一来,就可以利用 HasTypeMember 来检测一个类的内部是否具有 type 这个类型。而这种检测一般都用在编译时,
class TypeMember
{
public:
using type = int;
};
class NoTypeMember { };
template <typename Tp, typename = std::enable_if_t<HasTypeMember<Tp>::value>>
class TypeMemberTest
{
public:
typename Tp::type data;
};
TypeMemberTest<TypeMember> has_type_member; // OK
TypeMemberTest<NoTypeMember> no_type_member; // ERROR, 编译失败
话题之外
上文探讨了 std::unique_ptr 的数组偏特化版本,我们提供的实参是 int[],需要阐明的是 int*,int[] 和 int[size] 是三种不同的类型,
std::cout << std::boolalpha
<< std::is_same<int*, int[]>::value << '\n'
<< std::is_same<int*, int[3]>::value << '\n'
<< std::is_same<int[], int[3]>::value << '\n';
输出如下,
false
false
false
但是对于一个具体的数组 int arr[3];
,当将其作为实参传递给函数时会自动的退化成指针,所以在大多数情况下我们都会认为 int[],int[size] 和 int* 是同一类型,
void Func1(int arr[3])
{
std::cout << std::is_same<decltype(arr), int*> << '\n';
}
void Func2(int arr[])
{
std::cout << std::is_same<decltype(arr), int*> << '\n';
}
int arr[4]; // 注意数组维度是 4
std::cout << std::boolalpha;
Func1();
Func2();
输出如下,
true
true
但是在模板中,就不能按照经验来了,这三个不同类型就会体现出其不同来。由于 std::is_same 也是模板,所以才能够将这三种不同类型反映出来。
因此,我们不能够这样使用 std::unique_ptr<int[3]> uniptr_intarr3;
,因为 std::unique_ptr 没有这种形式的偏特化版本。
函数与函数指针也不是同一类型,可以通过类似的方法进行证实,这里就略过了。请参考 std::decay。