C++11 标准库源代码剖析:连载之二

Type Traits

Trait在英语中特指a particular quality of your personality,大致相当于汉语中的逼格的意思。那C++的逼格又是什么呢?C++之父Bjarne Stroustrup 对此的解释是:

Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine policy or implementation details.

翻译成人话就是:

trait 是一个小型对象,它的主要目的就是携带信息,而这些信息会被其它的对象或算法使用,用来决定某个 policyimplementation 的细节。

Trait在标准库中大量运用,比如我们熟悉的C++ 98 STL中的iterator traitchar trait等。C++ 11又增加了type trait

C++ 11 Type Traits

Type trait,顾名思义,就是对萃取类型信息的trait。C++ 11标准库定义了超过80种type trait ,全部放在头文件 <type_traits> 中。这些trait大致可以分为一下几类:

Categories Trait
Primary type is_void, is_null_pointer, is_class, is_function ...
Composite type is_fundamental, is_arithmetic, is_object ...
Type properties is_const, is_volatile, is_trivival, is_empty ...
Supported operations is_constructible, is_default_constructible, is_assignable ...
Property queries alignment_of, rank, extent
Type relationships is_same, is_base_of, is_convertible ...
Const-volatility specifiers remove_cv, remove_const, add_cv, add_const ...
Reference remove_reference, add_lvalue_reference, add_rvalue_referene ...
Pointers remove_pointer, add_pointer
Sign modifiers make_signed, make_unsigned
Arrays remove_extent, remove_all_extent
Miscellaneous decay, enable_if, common_type ...

由于篇幅有限,我们不可能列出全部的trait,不过在cppreference.com上有完整的列表,有兴趣的同学不妨去观摩一下。

Type Traits的实现

研究这些trait的源代码是一件很有意思的事情,你会看到各种让你脑洞大开拍案惊奇的编码技巧。下面让我们一起去看看这些代码长什么样。

is_const

is_const检查一个类型声明有没有const修饰符

template<class T>
struct is_const : public false_type {};

// 针对const类型的特化版本
template<class T>
struct is_const<const T> : public true_type {};

这个没啥难度,无非就是个模板特化罢了。

is_class

如果要你来写一个trait,判断某个类型是否是一个classstruct,比如有如下代码:

struct A {};
class B {};
enum class C {};

std::cout << std::boolalpha;
std::cout << is_class<A>::value << std::endl;
std::cout << is_class<B>::value << std::endl;
std::cout << is_class<C>::value << std::endl;
std::cout << is_class<int>::value << std::endl;

我希望输出如下:

true
true
false
false

你该怎么做?

有点晕菜是不是?考虑一下什么是classclass无非就是一组数据以及用以操纵这些数据的函数的集合。对于类中的数据,C++允许你定义一个指向类成员变量的指针,这是class所特有的属性,那可不可以针对这些特有属性,在模板特化上做文章呢?答案是肯定的,而且这也正是is_class的实现原理:

// helper class, sizeof(two) = 2
struct two {
    char c[2];
};

namespace is_class_imp {

    // 这个函数接受一个指向类成员变量的指针为参数
    template<class T> char test(int T::*);

    // 这个函数接受任何形式的参数
    template<class T> two test(...);
}

template<class T>
struct is_class 
    : public integral_constant<bool, sizeof(is_class_imp::test<T>(0)) == 1> {};

上面的代码重载了函数test(),第一个重载函数接受一个,呃...,那个“T冒号冒号星号”是啥?...int T::*定义了一个int类型的指向类成员变量的指针,也就是说函数接受一个类成员变量指针作为参数,当然也接受一个结构体成员变量指针(C++中structclass其实是一样的)作为参数。第二个test是个可变参数函数,接受任意数量和类型的参数。

当编译器看到sizeof(is_class_imp::test<T>(0))的时候,首先需要决定匹配哪个test函数。如果模板参数T确实是一个classstruct,那int T::*就是合法的C++表达式。至于T中有没有int类型的成员变量,编译器根本不关心。

等等!你又发现了问题,“test函数只有声明,没有定义,没有定义的函数该怎么编译?” 答案是根本不需要,编译器关心的是如何求出表达式sizeof(...)的值,而求解sizeof(...)只需要知道is_class_imp::test<T>(0)的返回类型,不需要看到函数的定义。所以如果T是个classstruct,那int t::*就是合法的类型定义,且精确匹配第一个重载函数,于是编译器用第一个函数的返回类型去求sizeof,于是is_class的声明就会被替换成

template<class T>
struct is_class : public integral_constant<bool, true> {};

如果T不是一个classstruct,那int T::*就是一个非法的类型定义,根据SFINAE规则,编译器不会报错,而是试着匹配第二个重载函数,也就是test的三个点版本,而这个版本是可以匹配任何参数类型的,is_class的声明会被替换成

template<class T>
struct is_class : public integral_constant<bool, false> {};

看到这里,相信你已经明白了is_class的实现原理,无非就是利用了重载函数的匹配规则而已。值得注意的是,上面代码中的test函数只有声明,没有定义。其实文件type_traits中声明了众多的辅助函数,却没有一个定义,因为根本不需要。正如前面反复强调的,编译器只是在做类型推导,唯一需要知道的就是参数类型和返回类型,至于有没有定义,编译器完全不关心。


common_type

common_type返回所有模板参数的最大公共类型,比如

common_type<int, float>::type           // float,因为int可以转换成float
common_type<int, float, double>::type   // double,因为int, float都可以转换成double

这似乎是一件很复杂的事。确实很复杂,不过我们有一个巧妙的方法可以化繁为简,先看源代码:

// 类声明,注意三个点,这说明这个类可以有任意多个模板参数
template<class ...T> struct commont_type;

// 针对只有一个模板参数的特化
template<class T>
struct common_type<T> {
    typedef typename std::decay<T>::type type;
};

// 针对两个模板参数的特化
template<class T, class U>
struct common_type<T, U> {
private:
    static T&& t();
    static U&& u();
    static bool f();
public:
    typedef typename std::decay<decltype(f() ? t() : u())>::type type;
};

// 针对三个或以上模板参数的特化
template<class T, class U, class ...V>
struct common_type<T, U, V...> {
    typedef typename common_type<typename common_type<T, U>::type, V...>::type type;
};

代码首先声明了一个模板类,然后分别针对模板参数的个数为一个和两个的情形做了特化,对于三个以上的模板参数的情况,则用递归的方法定义。然而...好像哪里不对?

  1. 哪里能看出来推导公共类型了?
  2. 这行代码有问题: typedef typename std::decay<decltype(f() ? t() : u())::type type,函数f()根本没有定义,所以三目运算符? :根本没法求值。

恭喜你,你有一只火眼金睛(另一只不是,所以看不到代码的精妙之处)。让我来告诉你怎么回事,这两个问题其实是一个问题。我们先从f() ? t() : u()说起,我再说一遍,编译器在解析模板时,做的是类型推导,所以f()根本不需要定义(即使有定义,编译器也不知道返回值是true还是false,只有到运行时才知道)。那问题又来了,不知道f()的返回值,编译器该如何求解三目运算符呢?答案还是不需要,编译器此时需要知道的是三目运算符的返回类型(而不是返回值),以满足解析decltype(...)的需要。问题是,不知道返回值,返回类型也无从谈起。这似乎进入了死胡同,别急,C++编译器是你的贴心小棉袄,它会尽一切可能编译你的代码,为了让编译进行下去,编译器会自动检查冒号两边的类型,尽可能将其中一个类型转换为另一个类型,并将这个类型作为三目表达式的返回类型,传入decltype(...)中。如果你还有疑问,可以做一个简单的测试:

std::cout << 
    typeid(decltype(true ? std::declval<int>() : std::declval<double>())).name() << std::endl;  // double

std::cout << 
    typeid(decltype(false ? std::declval<int>() : std::declval<double>())).name() << std::endl; // double

在我的XCode 9.2中,上面两行代码都输出d,也就是double。这就证明了编译器在三目表达式时,自动对参数类型进行了转换,并返回最大公共类型。

用三目运算符来推导最大公共类型,我只能用 顶(丧)礼(心)膜(病)拜(狂) 来形容。在标准库中,类似的使用奇技淫巧例子还有很多,这里就不一一介绍了。知乎上有一篇关于C++奇技淫巧的讨论帖子,有兴趣的同学可以狠戳这里


is_function

最后来一道硬菜:is_function。这个trait检查某个类型是否是function,比如下面代码所示:

// Sample code comes from http://en.cppreference.com/w/cpp/types/is_function

strcut A { int fun(); };

template<typename T> struct PM_traits{};

template<class T, class U>
struct PM_traits<U T::*> {
    using member_type = U;
}

int f();

// 1. A是个class,不是function;
is_function<A>::value;                      // false

// 2. int(int)表示一个以int为参数,并返回int的function类型;
is_function<int(int)>::value                // true

// 3. f是个function的名字,decltype(f)是个function类型
is_function<decltype(f)>::value             // true

// 4. 显然int不是一个function
is_function<int>::value                     // false

// 5. T被解析成 int(),是个function
using T = PM_traits<decltype(&A::fun)>::member_type;
is_function<T>::value                       // true

是不是觉得很神奇?我们来看一下它的实现代码:

namspace libcpp_is_function_imp {
    template<calss T> char    test(T*);
    template<class T> two     test(...);
    template<calss T> T&      source(int);
}

// 如果T是class, union, void, reference或null pointer,
// 则第二个模板参数的值为true,而针对这种情况,有一个特化的版本
template<class T, bool = is_class<T>::value ||
                         is_union<T>::value ||
                         is_void<T>::value  ||
                         is_reference<T>::value ||
                         is_nullptr_t<T>::value>
struct libcpp_is_function : public integral_const<bool,     
      sizeof(libcpp_is_function_imp::test<T>(libcpp_is_function_imp::source<T>(0))) == 1>
{};

// 针对class, union, void, reference和null pointer的特化版本
template<class T>
struct libcpp_is_function<Tp, true> : public false_type {};

template<class T>
struct is_function : public libcpp_is_function<T> {};

这段代码比较难懂,需要详细解释一下:

如果你对一个class, union, void, referencenull pointer,执行is_function操作,此时libcpp_is_function的第二个模板参数为true,而针对这种情况定义了一个特化版本,该特化版本继承于false_type,这是我们需要的结果。

除去第一种情况,编译器会激活非特化版本,此时编译器会对模板类integral_const的第二个模板参数进行类型推导:

如果T是一个function对象,比如void(void),则libcpp_is_function_imp::source<T>(0))的返回值为void(void)&。在编译器眼里,函数对象和函数指针是一种类型,也就是说void(void)void(*)(void)是一种类型,编译器于是会匹配参数为T*的重载版本test(T*),于是,sizeof(...)表达式被替换成sizeof(test<void(void)>(void(*)(void)),进而替换成sizeof(char),最终,类的声明被替换成:

template<class T>
struct libcpp_is_function : public integral_const<bool, true> {};

这也是我们需要的结果。

如果T不是一个function对象,比如为int,这时source函数的返回类型为int&。由于int&int*不是同一个类型,编译器只能匹配test(...)函数,于是类的声明就成了:

template<class T>
struct libcpp_is_function : public integral_const<bool, false>

这仍然是我们需要的结果。

自己动手写一个Type Trait

看了那么多,下面我们自己动手写一个traithas_to_string,我们希望达到如下的效果:

struct A {
    std::string to_string();
};

struct B {

}

std::cout << has_to_string<A>::value << std::endl; // 1
std::cout << has_to_string<B>::value << std::endl; // 0

各位同学可以开动脑筋,看看能否写出惊天地泣鬼神的代码。作为参考,这里给出一种可能的实现:

template<typename T, typename = std::string>
struct has_to_string : std::false_type {};

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

推荐阅读更多精彩内容

  • Scala与Java的关系 Scala与Java的关系是非常紧密的!! 因为Scala是基于Java虚拟机,也就是...
    灯火gg阅读 3,413评论 1 24
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,569评论 18 139
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,709评论 0 38
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 我在密林深处——(六)老爸的香菇我的难受 那时我大概十来岁!夏天多雨的时节,树林子里的隐秘处,藏着各种美味的蘑菇。...
    然乐花阅读 3,358评论 2 1