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 ,全部放在头文件 中。这些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
struct is_const : public false_type {};

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

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

is_class

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

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

std::cout << std::boolalpha;
std::cout << is_class::value << std::endl;
std::cout << is_class::value << std::endl;
std::cout << is_class::value << std::endl;
std::cout << is_class::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 char test(int T::*);

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

template
struct is_class 
    : public integral_constant(0)) == 1> {};

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

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

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

template
struct is_class : public integral_constant {};

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

template
struct is_class : public integral_constant {};

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


common_type

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

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

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

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

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

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

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

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

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

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

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

std::cout << 
    typeid(decltype(false ? std::declval() : std::declval())).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 struct PM_traits{};

template
struct PM_traits {
    using member_type = U;
}

int f();

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

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

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

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

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

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

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

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

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

template
struct is_function : public libcpp_is_function {};

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

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

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

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

template
struct libcpp_is_function : public integral_const {};

这也是我们需要的结果。

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

template
struct libcpp_is_function : public integral_const

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

自己动手写一个Type Trait

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

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

struct B {

}

std::cout << has_to_string::value << std::endl; // 1
std::cout << has_to_string::value << std::endl; // 0

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

template
struct has_to_string : std::false_type {};

template
struct has_to_string().to_string())> : std::true_type {};

你可能感兴趣的:(C++11 标准库源代码剖析:连载之二)