类模板参数的默认值

在定义类模板的时候,可以给模板参数指定一个默认值,

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_deletestd::unique_ptr 代码的简化。其中 std::default_delete 是标准库提供的用于删除指针的类,大多数情况用在函数对象上。
可以看到 std::unique_ptr 的第二个模板参数的默认值是实例化的 std::default_delete<_Tp>,所以在使用的时候只需要提供第一个模板参数就可以,

std::unique_ptr<int> uniptr_int;

当到达了 uniptr_int 作用域结束的地方,析构函数会利用 _Dp 来删除内部实际的指针。
同时,标准库也提供了数组版本的 std::default_deletestd::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;,那么 _Tpint,进而 _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,也就是说数组版本的 _Dpstd::default_delete<int[]>
这是怎么一回事呢?
原因是我们看待模板方角度出了问题,应该把重点放在实例化模板的地方。
对于 std::unique_ptr 模板原型和数组偏特化版本,我们过于看重 template <typename _Tp, typename _Dp = std::default_delete<_Tp>>template <typename _Tp, _Dp> 这两行,从而认为数组偏特化版本的第二个模板参数默认值就是直接从原型中拷贝过来的。实际上,这两行是模板声明的必要语法,作用是告诉编译器下面是个模板,还确定了需要推导的模板参数的个数;而编译器用来推导模板参数的是 class unique_ptrclass 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 DefaultArgTestclass 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


参考

[1] partial template specialization
[2] std::void_t

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容