[笔记]读书笔记 C++设计新思维《二》技术(Techniques)(二)

简介: [笔记]读书笔记 C++设计新思维《二》技术(Techniques)(二)

2.3 局部类(Local Classes)

这是一个有趣而少有人知道的C++特性。你可以在函数中定义class,像下面这样:

void Fun ( ){
  class Local{
    .. . member variables ...
    ... member function definitions ...
  };
  .. code using Local ...
}

不过还是有些限制:

  1. local class不能定义static成员变量 (不能定义静态成员变量)
  2. 也不能访问non-static局部变量。(非静态局部变量)

local classes令人感兴趣的是,可以在template函数中被使用。定义于template函数内的 local classes可以运用函数的template参数。以下所列代码中有一个MakeAdapter template function,可以将某个接口转接为另一个接口.MakeAdaptery在其local class的协助下实作出一个接口。这个localclass内有泛化型别的成员。

class Interface
{
  public:
  virtual void Fun ( ,=0;
};
template <class T, class P>
Interface* MakeAdapter (const T& obj, const P& arg)(
    class Local : public Interface{
      public:
        Local (const T& obj, const P& arg)
        : obj_(obj) , arg_ ( arg){ }virtual void Fun ( )
        {
          obj_.call (arg_) ;
        )
      private :
        T obj_;P arg_;
    };
  return new Local ( obj, arg ) ;
}

事实证明,任何运用local classes 的手法,都可以改用“函数外的template classes”来完成。换言之,并非一定得local classes不可。

不过 local classes可以简化实作并提高符号的地域性。Local classes 倒是有个独特性质:它们是“最后一版”(也即 Java口中的final)。外界不能继承一个隐藏于函数内的 class。如果没有local classes,为了实现Java final,你必须在编译单元(译注:也就是个别文件)中加上一个无具名的命名空间(namespace)。

我将在第11章运用local classes 产生所谓的“弹簧垫”函数(trampoline functions)。

2.4 常整数 映射为 型别( Mapping Integral Constants to Types)

下面是最初由Alexandrescu ( 2000b)提出的一个简单template,对许多泛型编程手法很有帮助:

template <int v>
struct Int2Type
{
  enum {value = v } ;
} ;

Int2Type会根据引数所得的不同数值来产生不同型别。这是因为“不同的template具现体”本身便是“不同的型别”。因此Int2Type<0>不同于Int2Type<1>,以此类推。用来产生型别的那个数值是一个枚举值( enumerator)。

当你想把常数视同型别时,便可采用上述的Int2Type。这么一来便可根据编译期计算出来的结果选用不同的函数。实际上你可以运用一个常数达到静态分派(static dispatching) 功能。

一般而言,符合下列两个条件便可使用Int2Type:

  • 有必要根据某个编译期常数调用一个或数个不同的函数。
  • 有必要在编译期实施“分派”( dispatch)。

如果打算在执行期进行分派(dispatch),可使用if-else或switch语句。大部分时候其执行期成本都微不足道。然而你还是无法常常那么做,因为 if-else语句要求每一个分支都得编译成功,即使该条件测试在编译期才知道。困惑了吗?读下去!

假想你设计出…个泛形容器Niftycontainer,它将元素型别参数化:

template <class T> 
class NiftyContainer
{
  ...
}

现在假设Niftycontainer内含指针,指向型别为T的对象。为了复制Niftycontainer里的某个对象,你想调用其 copy构造函数(针对non-polymorphic型别)或虚函数clone ()(针对polymorphic型别)。你以-一个boolean template参数取得使用者所提供的信息:

tempiate <typename T, bool isPolymorphic>
class NiftyContainer
{
  void DoSomething ()
  {
    T* pSomeObj = .. .;
    if(1sPo1 ymorphic){
      *pNewObj = psomeObj->Clone () ;
      ... 
      polymorphic algorithm ...(多态算法)
    }
    else{
      *pNewObj = new T ( *pSome0bj ) ;//译注:copy构造函数
      ... non-polymorphic algorithm ... (非多态算法)
    }
  }
};

问题是,编译器不会让你侥幸成功。如果多态算法使用pobj->Clone ),那么面对任何一个未曾定义成员函数clone ()之型别,NiftyContainer: : DoSomething ()都无法编译成功。虽然编译期间很容易知道哪一条分支会被执行起来,但这和编译器无关,因为即使优化工具可以评估出哪一条分支不会被执行,编译器还是会勤劳地编译每个分支。如果你试着调用Niftycontainer<int,false>的 DoSomething ),编译器会停在p0bj->clone ( )处并说“嗨,不行哨”。

上述的non-polymorphic部分也有可能编译失败。如果T是个 polymorphic型别,而上述的non-polymorphic程序分支想做newT( *pObj)动作,这样也有可能编译失败。举个实例,如果T借着“把 copy构造函数置于private区域以产生隐藏效果”,就像一个有良好设计的polymorphicclass那样,那么便有可能出现上述的失败情况。

如果编译器不去理会那个不可能被执行的代码就好了,然而目前情况下是不可能的。甚么才是令人满意的解决方案呢?

事实证明有很多解法,而Int2Type提供了一个特别明确的方案。它可以把isPolymorphic这个型别的 true和 false 转换成两个可资区别的不同型别,然后在程序中便可以运用Int2Type进行函数重载。瞧,可不是吗!

template <typename T, bool 1sPo1ymorphicclass Ni ftyContainer
{
private:
  void Dosomething ( T* pobj, Int2Type<true>)
  {
    T*pNewObj = pObj->Clone ( );... polymorphic algorithm .
  }
  void Dosomething (T*pObj, int2Type<false>){
    T* pNewobj = new T ( *pobj ) ;... nonpo1 ymorphic algorithm .
  }
public:
  void Dosomething ( r* pObj){
    Dosomething (pobj, Int2Type<isPolymorphic> () );
  }
};

Int2Type是一个用来“将数值转换为型别”的方便手法。有了它,你便可以将该型别的一个暂时对象传给一个重载函数(overloaded function),后者实现必要的算法。

(译注:这种手法在STL中亦有大量实现,唯形式略有不同;详见STL源码,或《STL源码剖析》by 侯捷

这个小技巧之所以有效,最主要的原因是,编译器并不会去编译一个未被用到的template函数,只会对它做文法检查。至于此技巧之所以有用,则是因为在 template 代码中大部分情形下你需要在编译期做流程分派(dispatch)动作。

你会在Loki的数个地方看到Int2Type的运用,尤其是本书第11章:Muttimethods。

在那儿,template class是个 双分派(double-dispatch)引擎,运用bool template参数决定是否要支持对称性分派(symmetric dispatch)

2.5 类别对类别的映射

就如2.2节所说,并不存在template函数的偏特化。然而偶尔我们需要模拟出类似机制。

试想下面的程序:

template <class T, class U>
T* Create (const U& arg)
{
  return new T ( arg ) ;
}

create( )会将其参数传给构造函数,用以产生一个新对象。

现在假设你的程序有个规则: widget对象是你碰触不到的老旧代码,它需要两个引数才能构造出对象来,第二引数固定为-1。widget派生类则没有这个问题。

现在你该如何特化Create (),使它能够独特地处理widget呢?一个明显的方案是另写出一个CreateWidget()来专门处理。但这么一来你就没有一个统一的接口用来生成widgets和其派生对象。这会使得create ()在任何泛型程序中不再有用。

由于你无法偏特化一个函数,因此无法写出下面这样的代码:

//llegal code — don 't try this at home
template <class U>
Widget* Create<Widget, U> (const U& arg)
{
return new widget (arg,一1);
}

由于函数缺乏偏特化机制,因此(再一次地)你只有一样工具可用:

  • 多载化(重载)机制。

我们可以传入一个型别为T的暂时对象,并以此进行重载:

template <class T , class U>
T* Create (const U&arg, T /*dummy */)
{
  return new T ( arg) ;
}
template <class U>
Widget* Create (const U& arg,Widget /*dummy*/)
{
  return new Widget (arg,-1) ;
)

这种解法会轻意构造未被使用的复杂对象,造成额外开销。我们需要个轻量级机制来传递“型别T的信息”到Create()中。这正是Type2Type扮演的角色,它是一个型别代表物,一个可以让你传给重载函数的轻量级ID。Type2Type定义如下:

template <typename T>
struct Type2Type
{
  typedef T OriginalType;
};

它没有任何数值,但其不同型别却足以区分各个 Type2Type实体,这正是我们所要的。现在你可以这么写:

// An implementation of Create relying on overloadinglland Type2Type
template <class T, class U>
T* Create (const U&arg, Type2Type<T>)
{
  return new T (arg) ;
}
template <class U>
Widget* Create(const U& arg, Type2Type<Widget>)
{
  return new widget (arg,-1);
}
// Use Create ( )
String* pStr = Create("Hello",Type2Type<String> ());
Widget* pW - Create(100,Type2Type<Widget>());

Create()的第二参数只是用来选择适当的重载函数,现在你可以令各种 Type2Type实体对应于你的程序中的各种型别,并根据不同的 Type2Type实体来特化Create ( )。

2.6 类别的选择

有时候,泛型程序需要根据一个boolean变量来选择某个型别或另一型别。

在2.4节讨论的NiftyContainer例子中,你也许会以一个std::vector作为后端存储结构。很显然,面对 Polymorphic((多态〉型别,你不能存储其对象实体,必须存储其指针。但如果面对的是non-polymorphic(非多态)型别,你可以存储其实体,因为这样比较有效率,在你的class template 中:

template <typename T, bool isPolymorphic>
class Niftycontainer
{
  ...
}

你需要存放一个vector<T*>(如果 isPolymorphic 为true)或vector(如果isPolymorphic为 false)。根本而言,你需要根据isPolymorphic来决定将valueType定义为T*或T。你可以使用traits class template ( Alexandrescu 2000a)如下:

template <typename T, bool isPo1ymorphic>
struct NiftyContainerValueTraits
{
  typedef T* ValueType;
};
template <typename T>
struct NiftyContainerValueTraits<T, false>
{
  typedef T ValueType;
};
template <typename T, bool isPolymorphic>
class NiftyContainer
{
  ...
  typedef NiftyContainerValueTraits<T, isPolymorphic> Traits;
  typedef typename Traits::ValueType  valueType;
};

这样的做法其实笨拙难用,此外它也无法扩充:针对不同的型别的选择,你都必须定义出专属的 traits class template。

Loki提供的 Select Class template可使型别的选择立时可用。它采用偏特化机制(Partial Template Specialization) :

template <bool flag, typename T, typename u>struct select
{
typedef T Result;
) ;
template <typename r, typename u>struct select<false, T ,U>
{
typedef U Result;
} ;

2.7 便器期间侦测可转换性和继承性

实作 template functions 和 template classes时我常常发现一个问题:面对两个陌生的型别T和U,如何知道U是否继承自T呢? 在编译期间发掘这样的关系,实在是实作泛型程序库的一个优化关键。在泛型函数中,如果你确知某个class实作有某个接口,你便可以采用某个最佳算法。在编译期发现这样的关系,意味着不必使用dynamic_cast——它会耗损执行期效率。

发掘继承关系,靠的是一个用来侦测可转换性(convertibility)的更一般化机制。这里我们面临更一般化的问题:

如何测知任意型别T是否可以自动转换为型别U?

有个方案可以解决问题,并且只需依赖sizeof。

sizeof有着惊人的威力:你可以把sizeof用在任何表达式 (expression)身上,不论后者有多复杂。sizeof 会直接传回大小,不需拖到执行期才评估。这意味着sizeof 可以

  • 感知重载(overloading)、
  • 模板具现( template instantiation)、
  • 转换规则(conversion rules),

或任何可发生于C+表达式身上的机制。

事实上sizeof背后隐藏了一个“用以推导表达式型别”的完整设施。最终sizeof 会丢弃表达式并传回其大小1

“侦测转换能力”的想法是:合并运用sizeof 和重载函数。我们提供两个重载函数:其中一个接受U(U型别代表目前讨论中的转换目标),另一个接受“任何其他型别”。我们以型别T的暂时对象来调用这些重载函数,而“T是否可转换为U”正是我们想知道的。如果接受U的那个函数被调用,我们就知道T可转换为U;否则T便无法转换为U。

为了知道哪一个函数被调用,我们对这两个重载函数安排大小不同的返回型别,并以 sizeof来区分其大小。型别本身无关紧要,重要的是其大小必须不同。

让我们先建立两个不同大小的型别。很显然char和 long double的大小不同,不过C++标准规格书并未保证此事,所以我想到一个极其简单的做法:

typedef char Small;
class Big { char dummy [2]; } ;

2.8 type_info的一个外覆类

std::type_info class

标准C++提供了一个std::type_info class,使你能够于执行期间查询对象型别。

外覆类

2.9 NullType和EmptyType

Loki 定义了两个非常简单的型别: NullType和 EmptyType。你可以拿它们当做型别计算时的某种边界标记。

NullType

NullType是一个只有声明而无定义的class:

class NullType;// no definition

你不能生成一个NullType对象,它只被用来表示“我不是个令人感兴趣的型别”。2.10节把NullType用在有语法需求却无语义概念的地方(例如“int 指的是什么型别”)。第3章的 typelist以NullType标记typelist的末端,并用以传回“找不到型别”这一消息。

EmptyType

第二个辅助型别是EmptyType。和你想的一样,EmptyType定义如下:

struct EmptyType i } ;

这是一个可被继承的合法型别,而且.你可以传递EmptyType对象。你可以把这个轻量级型别视为template的缺省(可不理会的)参数型别。第3章的 typelist就是这样用它的。

2.10 Type Traits

前言

Traits是一种“可于编译期根据型别作判断”的泛型技术,很像你在执行期根据数值进行判断一样(Alexandrescu 2000a)。众所皆知,加上一个间接层便可解决很多工程问题,trait让你得以在“型别确立当时”以外的其他地点做出与型别相关的判断。这会让最终的代码变得比较干净,更具可读性,而且更好维护。

通常,当你的泛型程序需要时,你会写出自己的trait templates 和 trait classes。然而某些traits可应用于任何型别,它们可以帮助泛型程序员根据型别特性修改出适当的泛型代码。

template <typename InIt, typename OutIt >
OutIt Copy (InIt first, InIt last,OutIt result)
{
  for (; first != last ; ++first, ++result )
  *result * first ;
}

std::copy函数

2.10.1 实作出PointerTraits

2.10.2 侦测基本类别

2.10.3 优化的参数类别

2.10.4 卸除饰词

2.10.5运用TypeTraits

2.10.6 包装

2.11 摘要

总结一

模板几个作用:

  • 静态检测 2.1节
  • 静态重载,选择合适的分支 2.4节

总结二

函数虽然无法使用模板偏特化,但是可以使用重载 2.4节


  1. 目前正有一份提案,准备为C++语言加入typeof操作符,它会传回一个表达式的型别。有了这个typeof操作符,泛型程序将会更好写,也更易被了解。Gnu C++己实作出typeof作为语言扩充功能。很显然typeof和sizeof拥有共同的后端实作,因为 sizeof也需要判断型别
相关文章
|
1月前
|
Linux 数据处理 C++
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用(一)
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用
78 0
|
1月前
|
存储 Linux API
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用(三)
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用
31 1
|
1月前
|
消息中间件 Linux 数据处理
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用(二)
Linux系统编程 C/C++ 以及Qt 中的零拷贝技术: 从底层原理到高级应用
32 1
|
1月前
|
存储 编解码 vr&ar
用C++实现视频编码器:FFmpeg与SDL技术结合,轻松编写高效编解码器
用C++实现视频编码器:FFmpeg与SDL技术结合,轻松编写高效编解码器
73 0
|
3月前
|
存储 C++ 计算机视觉
【三维重建技术】C++医学影像处理系统源码
医学影像存储与传输系统是PACS和RIS系统完美结合在一起的综合应用系统。系统的硬件结构由高性能服务器、大容量存储设备、高速网络、各种信息采集设备、各种诊断及应用工作站组成;系统的基本结构由系统管理、图像采集传输与存贮和图像处理与辅助诊断应用三大部分构成。
46 10
|
1月前
|
存储 算法 编译器
C/C++编译器局部优化技术:局部优化是针对单个函数或基本块进行的优化
C/C++编译器局部优化技术:局部优化是针对单个函数或基本块进行的优化
36 0
|
1月前
|
算法 编译器 C++
【C++ 泛型编程 中级篇】C++ 编译时技术:探索 if constexpr 和 std::enable_if
【C++ 泛型编程 中级篇】C++ 编译时技术:探索 if constexpr 和 std::enable_if
40 0
|
1月前
|
存储 算法 安全
【C++ 泛型编程 高级篇】 C++编译时函数调用技术深度解析
【C++ 泛型编程 高级篇】 C++编译时函数调用技术深度解析
36 1
|
1月前
|
缓存 编译器 程序员
C/C++编译器链接优化技术:链接优化是在编译器和链接器之间进行的优化
C/C++编译器链接优化技术:链接优化是在编译器和链接器之间进行的优化
40 0
|
1月前
|
缓存 编译器 程序员
C/C++编译器并行优化技术:并行优化针对多核处理器和多线程环境进行优化,以提高程序的并行度
C/C++编译器并行优化技术:并行优化针对多核处理器和多线程环境进行优化,以提高程序的并行度
64 0

热门文章

最新文章