[笔记]读书笔记 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也需要判断型别
相关文章
|
3月前
|
存储 算法 C++
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
文章详细探讨了C++中的泛型编程与STL技术,重点讲解了如何使用模板来创建通用的函数和类,以及模板在提高代码复用性和灵活性方面的作用。
63 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
|
6月前
|
存储 分布式数据库 API
技术好文:VisualC++查看文件被哪个进程占用
技术好文:VisualC++查看文件被哪个进程占用
|
6月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
|
6月前
|
编译器 数据安全/隐私保护 C++
c++primer plus 6 读书笔记 第十三章 类继承
c++primer plus 6 读书笔记 第十三章 类继承
|
4月前
|
人工智能 Anolis
聚焦C++20 最新标准!技术 Workshop 精彩亮点一览 | 2024 龙蜥大会
多场技术 Workshop、多位领域专家亲自授课,分享独家洞察与宝贵经验。
|
4月前
|
C++ 容器
【C/C++笔记】迭代器
【C/C++笔记】迭代器
29 1
|
4月前
|
算法 C# 开发工具
《黑神话:悟空》背后的编程语言揭秘——超越C++的多元技术融合
【8月更文挑战第27天】在游戏开发领域,一款游戏的成功往往离不开其背后强大的技术支持和编程语言的精妙运用。《黑神话:悟空》作为备受瞩目的国产单机动作游戏,其开发过程不仅涉及了多种编程语言,更是一次技术创新的集中展现。然而,当我们深入探讨其开发语言时,会发现它并非仅依赖于单一的C++,而是融合了多种编程语言的优势,共同铸就了这款游戏的辉煌。
291 0
|
4月前
|
存储 安全 程序员
【C/C++笔记】迭代器范围
【C/C++笔记】迭代器范围
73 0
|
5月前
|
C++ Windows
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
在Windows上使用Visual Studio 2022进行FFmpeg和SDL2集成开发,首先安装FFmpeg至E:\msys64\usr\local\ffmpeg,然后新建C++控制台项目。在项目属性中,添加FFmpeg和SDL2的头文件及库文件目录。接着配置链接器的附加依赖项,包括多个FFmpeg及SDL2的lib文件。在代码中引入FFmpeg的`av_log`函数输出"Hello World",编译并运行,若看到"Hello World",即表示集成成功。详细步骤可参考《FFmpeg开发实战:从零基础到短视频上线》。
230 0
FFmpeg开发笔记(三十九)给Visual Studio的C++工程集成FFmpeg
|
6月前
|
C++
c++primer plus 6 读书笔记 第十四章 C++中的代码重用
c++primer plus 6 读书笔记 第十四章 C++中的代码重用