荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: bso (BSO's So Open), 信区: Program
标  题: Boost源码剖析之:型别分类器——type_traits
发信站: 荔园晨风BBS站 (Wed Aug 16 10:55:40 2006), 站内

动机

1. 分派

下面有一个模板函数,假设一个动物收容组织提供了它,他们接受所有无家可归的可怜的小
动物,于是他们向外界提供了一个函数接受注册。函数看起来像这样:

template //T表示接受的是何种动物
void AcceptAnimals(T animal)
{
        ...  //do something
};但是,如果他们想将猫和狗分开处理(毕竟饲养一只猫和饲养一只狗并不相同。他们可能
会为狗买一根链子,而温顺的猫则可能不需要)。一个可行的方法是分别提供两个函数:
AcceptDog和AcceptCat,然而这种解决办法并不优雅(想想看,注册者可能既有一只猫又有
一只狗,这样他不得不调用不同的函数来注册,而且,如果种类还在增多呢,那样会导致向
外提供的接口的增多,注册者因此而不得不记住那些烦琐的名字,而这显然没有只需记住
AccpetAnimal这一个名字简单)。如果想保持这个模板函数,并将它作为向外界提供的唯一
接口,则我们需要某种方式来获取型别T的特征(trait),并按照不同的特征来采用不同的策
略。这里我们有第二个解决办法:
约定所有的动物类(如class Cat,class Dog)都必须在内部typedef一个表明自己身份的型别
,作为标识的型别如下:


struct cat_tag{}; //这只是个空类,目的是激发函数重载,后面会解释

struct dog_tag{}; //同上于是,所有狗类都必须像这样:
class Dog
{
        public:
        typedef  dog_tag  type; //型别(身份)标志,表示这是狗类,如果是猫类则为
typedef cat_tag type;
        ...
}然后,动物收容组织可以在内部提供对猫狗分开处理的函数,像这样:
template
void Accept(T dog,dog_tag) //第二个参数为无名参数,只是为了激发函数重载
{...}
template
void Accpet(T cat,cat_tag) //同上
{...}于是先前的Accept函数可以改写如下:

template
void Accept(T animal)  //这是向外界提供的唯一接口
{
        1   Accept(animal,typename T::type()); //如果T为狗类,则typename
T::type就是dog_tag,那么
        //typename T::type()就是创建了一个dog_tag类的临时对象,
        //根据函数重载的规则,这将调用Accept(T,dog_tag)的版本,
        //这正是转向处理狗的策略
        //如果T为猫类,则typename T::type为cat_tag,由上面的推导,
        //这将调用Accept(T,cat_tag)的版本,转向处理猫的策略
}                               //typename 关键字告诉编译器T::type是个型别而不是
静态成员所有型别推导,函数重载,都在编译期完成,你几乎不用耗费任何运行期成本(除
了创建dog_tag,cat_tag临时对象的成本,然而经过编译器的优化,这种成本可能也会消失
)就拥有了可读性和可维护性高的代码。“但是,等等!”你说:“traits在哪?”,
typename T::type其实就是traits,只不过少了一层封装而已,如果像这样作一些改进:
template
struct AnimalTraits
{
        typedef T::type type;
};
%ENDOCDE%
于是1处的代码可以写成Accept(animal,typename AnimalTraits::type());

2. 效率

通常为了提高效率,为某种情况采取特殊的措施是必要的,例如STL里面的copy,原型像这
样:
%CODE{"cpp"}%
template
IterOut copy(IterIn first,IterIn last,IterOut dest){//将[first,last)区间内的元素
拷贝到以dest开始的地方
        return copy_opt(first,last,dest,ptr_category(first,dest));
//ptr_category用来萃取出迭代器的类别以进行
        //适当程度的优化
}copy_opt有两个版本,其中一个是针对如基本型别的数组作优化的,如果拷贝发生在char
数组间,那么根本用不着挨个元素赋值,基于数组在内存中分布的连续性,可以用速度极快
的memmove函数来完成。ptr_category有很多重载版本,对可以使用memmove的情况返回一个
空类如scalar_ptr的对象以激发函数重载。其原始版本则返回空类non_scalar_ptr的对象。
copy_opt的两个版本于是像这样:
template
IterOut copy(IterIn first,IterIn last,IterOut dest,scalar_ptr){ ...}  //使用
memmove
template
IterOut copy(IterIn first,IterIn last,IterOut dest,non_scalar_ptr){ ...} //按部
就班的逐个拷贝其实通常为了提高效率,还是需要分派。
3. 使某些代码能通过编译

这或许令人费解,原来不能通过编译的代码,经过traits的作用就能编译了吗?是的,考虑
std::pair的代码(为使代码简洁,忽略大部分):

template
struct pair
{
        T1 first;
        T2 second;
        2 pair(const T1 & nfirst, const T2 & nsecond) //如果T1或T2本身是引用,则
编译错误,因为没有“引
        :first(nfirst), second(nsecond) { }         //用的引用”(C++之父Bjarne
Stroustrup已经向C++标准
        ...                                    //委员会提交一份提案建议将“引用
的引用”看作“引用”,
};                                      //或许以后这会合法化)。这里可以使用一个
traits(boost库里面的名字为add_reference)来避免这样的错误。这个traits内含一个
typedef,如果add_reference的T为引用,则typedef T type;如果不是引用,则typedef
T& type;这样,2处的代码可写成:
pair(add_reference::type nfirst,add_reference::type nsecond)。这对所有的型别都能
通过编译。
Boost库中的Traits

Boost中的Traits十分完善,可分为几大类:1. Primary Type Categorisation(初级型别分
类) 2. Secondary Type Categorisation(次级型别分类) 3. Type Properties(型别属性)
4. Relationships Between Types(型别间关系) 5. Transformations Between Types(型别
间转换) 6. Synthesizing Types(型别合成) 7. Function Traits(函数traits)

由于其中一些traits只是简单的模板偏特化,故不作介绍,本文仅介绍一些技术性较强的
traits。由于traits的定义往往重复代码较多,所以必要时本文仅剖析其底层机制。所有源
码均摘自相应头文件中,为使源码简洁,所有的宏均已展开。由于traits技巧与编译平台息
息相关,某些平台可能不支持模板偏特化。这里我们假设编译器是符合C++标准的。在我的
VC7.0上,以下代码均通过编译并正常工作。

1. 初级型别分类

is_array<>(boost/type_traits/is_array.hpp)定义
template is_array{static const bool value=false;};  //缺省
template is_array{static const bool value=true;}; //偏特化注解
C++标准允许整型作为模板参数,上面的N就是这样。这也说明出现在模板偏特化版本中的模
板参数(在本例中为typename T,size_t N两个)个数不一定要跟缺省的(本例中为typename
T一个)相同,但是出现在类名称后面的参数个数却要跟缺省的个数相同(is_array,T[N]为一
个参数,与缺省的相同)。

is_array::value  //true,with T=int,N=10,使用偏特化版本
is_array::value   //false,with T=int ,使用缺省版本
is_class<>(.../is_class.hpp)  //省略前面的boost/type_traits,下同定义
template
struct is_class_impl //底层实现,原因是根据不同的编译环境可能有不同的底层实现,
我的编译环境
{                //为VC7.0,其他底层实现从略。
        template static ::boost::type_traits::yes_type
is_class_tester(void(U::*)(void));
        template static ::boost::type_traits::no_type is_class_tester(...);
//...表示任意参数列表
        static const bool value = ::boost::type_traits::ice_and<  //ice_and<>相
当于逻辑与
        3  sizeof(is_class_tester(0)) == sizeof(::boost::type_traits::yes_type),
        ::boost::type_traits::ice_not< ::boost::is_union::value >::value
//is_union<>的判断需要编译器
        >::value                                                 //的支持,可当作
总为false
};
template
struct is_class
{
        static const bool value=is_class_impl::value; //所有实现都在
is_class_imp中
};注解
::boost::type_traits::yes_type为一个typedef: typedef char yes_type; 所以
sizeof(yes_type)为1.

::boost::type_traits::no_type为一个struct: struct no_type{char padding[8]; };
sizeof(no_type)为8。他们

一般被用作重载函数的返回值型别,这样通过检查返回值型别的大小就知道到底调用了哪个
函数,他

们被定义在boost/type_traits/detail/yes_no_type.hpp中。

is_class_impl中有两个static函数,第一个函数仅当模板参数U是类时才能够被具现化,因
为它的参数类型是void(U::*)(void),即指向成员函数的指针。第二个函数具有不定量任意
参数列表,C++标准说只有当其它所有的重载版本都不能匹配时,具有任意参数列表的重载
版本才会被匹配。所以,如果T为类,则void (T::*)(void)这种型别就存在,所以对
is_class_tester(0)的重载决议将是调用第一个函数,因为将0赋给任意型别的指针都是合
法的。而如果T不是类,则就不存在void(T::*)(void)这种指针型别,所以第一个函数就不
能具现化,这样,对is_class_tester(0)的重载决议结果只能调用第二个函数。

现在注意3处的表达式sizeof(is_class_tester(0))
==sizeof(boost::type_traits::yes_type)。按照以上的推论,如果T为类,
is_class_tester(0)实际调用第一个重载版本,返回yes_type,则表达式评估为true。如果
T不是类,则is_class_tester(0)调用第二个重载版本,返回no_type,则表达式评估为
false。这正是我们想要的。一个值得注意的地方是:在sizeof的世界里,没有表达式被真
正求值,编译器只推导出表达式的结果的型别,然后给出该型别的大小。对于
sizeof(is_class_tester(0))编译器实际并不调用函数的代码来求值,而只关心函数的返回
值型别。所以声明该函数就够了。另一个值得注意之处是is_class_tester的两个重载版本
都用了模板函数的形式。第一个版本用模板形式的原因是如果不那样做,而是这样static
yes_type is_class_tester(void(T::*)(void));则当T不是类时,该traits将不能通过编译
,原因很简单,当T不是类时void (T::*)(void)根本不存在。然而,使用模板时,当T不是
类时该重载版本会因不能具现化而根本不编译,C++标准允许不被使用的模板不编译(具现化
)。这样编译器就只能使用第二个版本,这正合我们的意思。而第二个版本为模板是因为第
一个版本是模板,因为在3处对is_class_tester的调用是这样的:is_class_tester(0),如
果第二版本不是模板则这样代码只能解析为对is_class_tester模板函数(即第一个版本)的
调用,重载解析也就不复存在。

“等等!”你意识到了一些问题:“模板函数的调用可以不用显式指定模板参数!”好吧,
也就是说你试图这样写:

template static ::boost::type_traits::yes_type is_class_tester(void(U::*)(void))
; //模板

static ::boost::type_traits::no_type is_class_tester(...); //非模板

然后在3标记的那一行这样调用:is_class_tester(0)(原来是is_class_tester(0)),是的
,我得承认,这的确构成了函数重载的条件,也的确令人欣喜的通过了编译,然而结果肯定
不是你想要的!你会发现对所有型别is_class::value现在都是0。也就是说,编译器总是调
用is_class_tester(..);这是因为,当调用的函数的所有重载版本中有一个或多个为模板时
,编译器首先要尝试进行模板函数具现化而非重载决议,而在尝试具现化的过程中,编译器
会进行模板参数推导,0的型别被编译器推导为int(0虽然可以赋给指针,但0的型别不可能
被推导为指针型别,因为指针型别可能有无数种,而事实上C++是强类型语言,对象只能属
于某一种型别),而第一个函数的参数型别void (U::*)(void)根本无法与int匹配(因为如果
匹配了,那么模板参数U被推导为什么呢?)。所以第一个版本具现化失败后编译器只能采用
非模板的第二个版本。结果如你所见,是令人懊恼的。然而如果你写的是
is_class_tester(0)你其实是显式具现化了is_class_tester每一个模板函数(除了那些不能
以T为模板参数具现化的),而它们都被列入接受重载决议的侯选单,然后编译器要做的就只
剩下重载决议了。(关于编译器在含有模板函数的重载版本时是如何进行重载决议的,可参见
C++ Primer的Function Templates一节,里面有极其详细的介绍)。

以上所将的利用函数重载来达到某些目的的技术在type_traits甚至整个Boost库里多处用到


初级型别分类还有:is_void<>,is_integral<>,is_float<>,is_pointer,is_reference<>,
is_union<>,is_enum<>,

is_function<>。请参见Boost提供的文档。

2.次级型别分类

is_member_function_pointer<>(.../is_member_function_pointer.hpp)

定义(.../detail/is_mem_fun_pointer_impl.hpp)

template

struct is_mem_fun_pointer_impl //缺省版本

{


static const bool value = false;

};

template //偏特化版本,匹配无参数的成员函数

struct is_mem_fun_pointer_impl { static const bool value = true; };

template //匹配一个参数的成员函数

struct is_mem_fun_pointer_impl { static const bool value = true; };

.etc. ... //其它版本只是匹配不同参数个数的成员函数的偏特化而已,参见源文件。

template

struct is_mem_function_pointer

{ static const bool value=is_mem_fun_pointer_impl::value; };

注解

假设你有一个类X,你这样判断:is_mem_function_pointer::value;则编译器先将

is_mem_function_pointer的模板参数class T推导为int (X::*)(int),然后将其传给
is_mem_fun_pointer_impl

,随后编译器寻找后者的偏特化版本中最佳匹配项为is_mem_fun_pointer_impl其中R=int,

T=X,T0=int。该偏特化版本的value=true;

次级型别分类还有:is_arithmetic<>,is_fundamental<>,is_object<>,is_scalar<>,
is_compound<>。请参见

Boost提供的文档。

3.型别属性

is_empty<>(.../is_empty.hpp)

定义

template

struct empty_helper_t1 : public T //如果T是空类,那么派生类的大小就是派生部分的
大小

{ //即sizeof(int)*256

empty_helper_t1();

int i[256];

}; //

struct empty_helper_t2 { int i[256]; }; //大小为sizeof(int)*256

//通过比较以上两个类的大小可以判断T是否为空类,如果它们大小相等则T为空类。反之则
不为空

//这里一个值得注意的地方是:若定义一个空类E,则sizeof(E)为1(这一个字节是用于在
内存中唯一

//标识该类的不同对象。如果sizeof(E)为0,则意味着不同的对象在内存中的位置没有区别
,这显然有

//违直观)。然而如果有另一个非空类继承自E,那么这一个字节的内存就不需要。也就是
说派生类的

//大小等于派生部分的大小而非多出一个字节。

template

struct empty_helper //这个辅助类的作用是:如果T不是类则使用该缺省版本

{ //如果T是类则使用下面的偏特化版本。而判断T是否为类的工作则由上面讲过


static const bool value = false; //的is_class<>traits来做。

};

template

struct empty_helper

{

5 static const bool value = (sizeof(empty_helper_t1) == sizeof(empty_helper_t2))
;

};

template

struct is_empty_impl

{

typedef typename remove_cv::type cvt; //remove_cv将T的const volatile属性去掉,这
是因为在

//public 后面不能跟const volatile T。


static const bool value = (::boost::type_traits::ice_or< //ice_or<>相当于逻辑或

4 ::boost::detail::empty_helper::value>::value //cvt作为基类,不能有cv修饰符

, BOOST_IS_EMPTY(cvt) //该宏被简单地定义为false,但对结果无影响

>::value);

};

注解

在标记4处,如果is_class::value为true(即T为类)则empty_helper::value>::value实际决
议为empty_helper,这将采用偏特化版本,转到5,则结论出现。否则T不是类,则采用缺省
版本,结果value为false。

is_polymorphic<>(.../is_polymorphic.hpp)

is_plymorphic<>基于一个基本事实:一个多态的类里面会有一个虚函数表指针(一般称为
vptr),它指向一个虚函数表(一般称为vtbl)。后者保存着一系列指向虚函数的函数指针以
及运行时类型识别信息。一个虚函数表指针通常占用4个字节(32寻址环境下的所有指针都占
用4个字节)。反之,如果该类不是多态,则没有这个指针的开销。基于这个原理,我们可以
断定:如果类X不是多态类(没有vtbl及vptr),则如果从它派生一个类Y,Y中仅含有一个虚
函数,这会导致sizeof(Y)>sizeof(X)(这是因为虚函数的首次出现导致编译器必须在Y中加
入vptr的缘故)。反之,如果X原本就是多态类,则sizeof(Y)==sizeof(X)(因为这种情况下
,Y中其实已经有了从X继承而来的vtbl及vptr,编译器所要做的只是将新增的虚函数纳入到
vtbl中去)。

定义

template

struct is_polymorphic_imp1 //当T为类时使用这个版本

{

typedef typename remove_cv::type ncvT;

struct d1 : public ncvT //ncvT是将T的const volatile修饰符去掉后的型别,因为
public后不能跟这样的

{ //修饰符

d1(); //该类里没有虚函数

~d1()throw();

char padding[256];

}; //d1中没有虚函数

struct d2 : public ncvT //在d2中加入一个虚函数


{

d2();

virtual ~d2()throw(); //加入一个虚函数,如果ncvT为非多态则会导致vptr的加入从而多
占用4字节

char padding[256];

};

static const bool value = (sizeof(d2) == sizeof(d1)); //如果T为多态类则valu为
true

};

template

struct is_polymorphic_imp2 //当T并非类时采用这个版本

{static const bool value = false;}; //既然T不是类,那么就不存在多态,所以总是
false

template

struct is_polymorphic_selector //这个selector根据is_class的真假来选择判断的方式

{

template

struct rebind //如果is_class为false则由is_polymorphic_imp2<>来判断

{ //这将导致结果总是false

typedef is_polymorphic_imp2 type; //使用_imp2

};

};

template <>

7 struct is_polymorphic_selector //当is_class为true时使用该特化版本

{

template

struct rebind //如果is_class为true,则由is_polymorphic_imp1<>来作判断

{

typedef is_polymorphic_imp1 type; //使用_imp1

};

};

template

struct is_polymorphic_imp // is_polymorphic<>完全由它实现

{

6 typedef is_polymorphic_selector< ::boost::is_class::value> selector; //选择
selector

8 typedef typename selector::template rebind binder; //

9 typedef typename binder::type imp_type;

static const bool value = imp_type::value;

};

注解

6处,如果T为类,则is_class::value为true,则那一行实际上就是:

typedef is_polymorphic_selector selector;

这将决议为is_polymorphic_selector<>的第二个重载版本7,其中的template rebind将判
断的任务交给is_polymorphic_imp1<>,所以8行的binder其实就是
is_polymorphic_selector::rebind。而9行的imp_type其实就是is_polymorphic_imp1,结果
正如预期。如果T不是类,按照类似的推导过程,最终会推导至
is_polymorphic_imp2::value,这正是false。

“嗨!这太烦琐了!”你抱怨道:“可以简化!”。我知道,你可能会想到使用
boost::ct_if(ct_if是?:三元操作符的编译期版本,像这样使用typedef ct_if::value
result,则当CompileTimeBool为true时result为TypeIfTrue,否则result为TypeIfFalse。
ct_if<>的实现很简单,模板偏特化而已)。于是你这样写:


typedef typename boost::ct_if<

boost::is_class::value,

is_polymorphic_imp1, //is_class::value为true时的型别

is_polymorphic_imp2, // is_class::value为false时的型别

>::type imp_type;

static const bool value = imp_type::value;

这在我的VC7.0环境下的确编译通过并正常工作,但是有一个小问题:假如T不是class,比
如,T是一个int,则编译器的型别推导将is_polymorphic_imp1赋给ct_if<>的第二个模板参
数,在这个过程中编译器会不会具现化is_polymorphic_imp1(或者,换句话说,编译器会不
会去查看它的定义),如果具现化了,那么其内部的struct d1 : public ncvT会不会也跟着
具现化为struct d1:public int,如果是这样,那么将会有编译期错误,因为C++标准不允
许有public int这样的东西出现。事实上我的编译器没有报错,即是说它并没有去查看
is_polymorphic_imp1的定义。

但我不知道C++标准关于这点怎么说。然而Boost库所用的方法是标准所保证的。

型别属性traits还有:alignment_of<>, is_const<>, is_volatile<>, is_pod<>,
has_trivial_constructor<>等。

4. 型别间关系

is_base_and_derived<>( boost/type_traits/ is_base_and_derived.hpp)

template

struct bd_helper

{

template

static type_traits::yes_type check(D const volatile *, T); //两个重载函数

static type_traits::no_type check(B const volatile *, int);

};

template

struct is_base_and_derived_impl2

{

struct Host

{

operator B const volatile *() const; //该转换操作符当对象为const对象时才起作用

operator D const volatile *();

};


static const bool value sizeof(bd_helper::check(Host(), 0)) =
sizeof(type_traits::yes_type);
};

以上就是is_base_and_derived<>的底层机制。下面我就为你讲解它所仰赖的机制,假设有
这样的类继承体系:

struct B {};

struct B1 : B {};

struct B2 : B {};

struct D : private B1, private B2 {}; //将D*转换为B1*会导致访问违规,私有基类部
分无法访问

首先是一些术语: //但是后面解释了这为什么不会发生

SC - Standard Conversion

UDC - User-Defined Conversion

一个user-defined转换序列由一个SC后跟一个UDC后再跟一个SC组成。其中头尾两个SC都可
以为到自身的转换(如:D->D), 10处将一个缺省构造的Host()交给bd_helper::check函数。


对于static no_type check(B const volatile *, int),我们有如下可行的隐式转换序列


C -> C const (SC - Qualification Adjustment) -> B const volatile* (UDC) //C表示
Host()

C -> D const volatile* (UDC) -> B1 const volatile* / B2 const volatile* ->

B const volatile* (SC - Conversion)

对于static yes_type check(D const volatile *, T),我们有如下转换序列:

C -> D const volatile* (UDC)

C++标准说,在重载决议中选择最佳匹配函数时,只考虑标准转换(SC)序列,而这个序列直
到遇到一个UDC为止,对于第一个函数,将C->C const与C->C比较,显然选择后者。因为后
者是前者的一个真子集。因此,去掉第一个转换序列我们得到:

C -> D const volatile* (UDC) -> B1 const volatile* / B2 const volatile* ->

B const volatile* (SC - Conversion)

C -> D const volatile* (UDC)

这里采用选择最短序列的原则,选择后者,这表明编译器甚至根本不需要去考虑向B转换的
多重路径,或者访问限制,所以转换二义性和访问违规也就不会发生。结论是如果D继承自
B,则选择yes_type check()。

如果D不是继承自B,则对于static no_type check(B const volatile *, int)编译器的给
出的转换为:

C -> C const -> B const volatile*(UDC)

对于static yes_type check(D const volatile *, T)编译器给出:

C -> D const volatile* (UDC)

这两个都不错(都需要一个UDC),然而由于static no_type check(B const volatile *,
int)为非模板函数,所以被编译器选用。结论是如果D并非继承自B,则选择no_type
check()。

另外,在我的VC7.0环境下,如果将Host的operator B const volatile *() const的const
拿掉,则结果将总是false。

可惜这样的理解并不属于我,它们来自Boost源代码中的注释。

is_convertible<>(boost/type_traits/is_convertible.hpp)

定义

template< typename From >

struct does_conversion_exist

{

template< typename To > struct result_

{


static no_type _m_check(...); //当不存在从From到To的任何转型时调用它

static yes_type _m_check(To); //只要转型存在就调用它

static From _m_from; //这只是个声明,所以并不占用空间,且没有开销。

enum { value = sizeof( _m_check(_m_from) ) == sizeof(yes_type) };

};

};

template<> struct does_conversion_exist{ //这是个为void准备的特化版本,因为不能
声明

template< typename To > struct result_{ //void _m_from

enum { value = ::boost::is_void::value }; //只有void可以向void“转换”

};

};

//is_convertible<>完全使用does_conversion_exist<>作底层机制,所以略去。

注解

does_conversion_exist<>也使用了与is_class_impl<>一样的技术。所以注解从略。该技术
最初由Andrei Alexandrescu发明,请参见Modern Design C++(<>--侯捷、於春景译)。


型别间关系traits还有:is_same<>,它只是简单的模板偏特化。

5. Transformations Between Types(型别间转换) 6. Synthesizing Types(型别合成) 7.
Function Traits(函数traits)的机制较为单纯,请参见Boost提供的文档或头文件。

Traits是泛型世界中的精灵:小巧,精致。Traits也是泛型编程中最精微的东西,它们往往
仰赖于一些编译期决议的规则,C++标准,和神奇的模板偏特化。这也导致了它们在不同的
平台上可能有不同表现,更常见的是,在某些平台上根本无法工作。然而,由于它们的依据
是C++标准,而编译器会越来越符合标准,所以这些问题只是暂时的。Traits也是构建泛型
世界的基本组件之一,它们往往能使设计变得优雅,精致,甚至完美。

--
  →  小白脸公司正式成立.....en.....来吧  ←
    →玩游戏?炒股?灌水?---->>>>>梦想农庄欢迎各位的光临!!!!!!←
↘                                          ↙
    →http://192.168.116.111/LTM/
 【 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  】


※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.116.111]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店