C++ 程序员如何用 D 编程
每个有经验的 C++ 程序员都积累了一系列的习惯和技术,这几乎成了第二天性。有时候,当学习一门新语言时,这些习惯会因为太令人舒适而使人看不到新语言中等价的方法。所以下面收集了一些常用的 C++ 技术,以及如何在 D 中完成同样的任务。另见:C 程序员如何用 D 编程
- 定义构造函数
- 基类初始化
- 比较结构
- 创造新的 typedef 型别
- 友元
- 运算符重载
- 名字空间 using 声明
- RAII (资源获得即初始化)
- 属性
- 递归模板
定义构造函数
C++ 的方式
构造函数同类同名:class Foo { Foo(int x); };
D 的方式
构造函数用 this 关键字定义:class Foo { this(int x) { } }
基类初始化
C++ 的方式
基类构造函数通过参数初始化列表语法调用。class A { A() {... } }; class B : A { B(int x) : A() // 调用基类构造函数 { ... } };
D 的方式
基类构造函数通过 super 关键字调用:class A { this() { ... } } class B : A { this(int x) { ... super(); // 调用基类构造函数 ... } }D 的方式优于 C++ 的地方在于可以灵活的在派生类的构造函数中的任何地方调用基类构造函数。D 还可以让一个构造函数调用另一个构造函数:
class A { int a; int b; this() { a = 7; b = foo(); } this(int x) { this(); a = x; } }也可以在调用构造函数之前初始化成员,所以上面的例子等价于:
class A { int a = 7; int b; this() { b = foo(); } this(int x) { this(); a = x; } }
比较结构
C++ 的方式
尽管 C++ 用简单、便捷的方式定义了结构之间的赋值:struct A x, y; ... x = y;但这不适用于结构之间的比较。因此,如果要比较两个结构实例之间的等价性的话:
#include <string.h> struct A x, y; inline bool operator==(const A& x, const A& y) { return (memcmp(&x, &y, sizeof(struct A)) == 0); } ... if (x == y) ...注意对于每个需要比较的结构来说,都要进行运算符重载,并且对运算符的重载会抛弃所有的语言提供的型别检查。C++ 的方式还有另一个问题,它不会检查 (x == y) 真正会发生什么,你不得不察看每一个被重载的 operator==() 以确定它们都做了些什么。
如果在 operator==() 中使用 memcmp() 还回造成潜在而丑陋的 bug 。由于对齐的缘故,结构的内存分布不一定是连续的,其中可能会有“洞”。C++ 并不保证这些用于对齐的洞中的值是确定的,所以两个结构实例可能拥有完全相同的结构成员,但是却因为洞中含有不同的垃圾而不相等。
为了解决这个问题,operator==() 可以实现为按成员(memberwise)比较。不幸的是,这是不可靠的,因为 (1) 如果一个成员被加入到结构定义中,程序员可能会忘记同时把它加到 operator==() 中,(2) 对于浮点数的 nan 值来说,就算它们按位比较相等,比较的结果也是不等。
在 C++ 中没有健壮的解决方案。
D 的方式
D 的方式明显而直接:A x, y; ... if (x == y) ...
创造新的 typedef 型别
C++ 的方式
Typedef 在 C++ 中是弱的,就是说,它们不会真正引入一个新的型别。编译器并不区分 typedef 和它底层的型别。#define HANDLE_INIT ((Handle)(-1)) typedef void *Handle; void foo(void *); void bar(Handle); Handle h = HANDLE_INIT; foo(h); // 未被捕获的编码错误 bar(h); // okC++ 的解决方案是创建一个傀儡(dummy)结构,这个结构的唯一的目的就是获得真正的新型别所具有的型别检查和重载能力。
#define HANDLE_INIT ((void *)(-1)) struct Handle { void *ptr; Handle() { ptr = HANDLE_INIT; } // default initializer Handle(int i) { ptr = (void *)i; } operator void*() { return ptr; } // conversion to underlying type }; void bar(Handle); Handle h; bar(h); h = func(); if (h != HANDLE_INIT) ...
D 的方式
不需要上面那种惯用的构造。只需要这样写:typedef void *Handle = cast(void *)-1; void bar(Handle); Handle h; bar(h); h = func(); if (h != Handle.init) ...注意,可以给 typedef 提供一个默认的初始值作为新型别的初始值。
友元
C++ 的方式
有时两个类关系很紧密,它们之间不是继承关系,但是它们需要互相访问对方的私有成员。在 C++ 中这样用到 friend 声明:class A { private: int a; public: int foo(B *j); friend class B; friend int abc(A *); }; class B { private: int b; public: int bar(A *j); friend class A; }; int A::foo(B *j) { return j->b; } int B::bar(A *j) { return j->a; } int abc(A *p) { return p->a; }
D 的方式
在 D 中,位于同一个模块的类隐式地具有友元访问权限。这样做是有道理的,因为关系紧密地类应该位于同一个模块中,所以隐式地赋予位于同一个模块中的其他类友元访问权限是优雅的:module X; class A { private: static int a; public: int foo(B j) { return j.b; } } class B { private: static int b; public: int bar(A j) { return j.a; } } int abc(A p) { return p.a; }private 特征禁止从其他模块中访问成员。
运算符重载
C++ 的方式
假设有一个结构代表了一种新的算术类型,将其的运算符重载以使其可以和整数比较是很方便的:struct A { int operator < (int i); int operator <= (int i); int operator > (int i); int operator >= (int i); }; int operator < (int i, A &a) { return a > i; } int operator <= (int i, A &a) { return a >= i; } int operator > (int i, A &a) { return a < i; } int operator >= (int i, A &a) { return a <= i; }所有的 8 个函数缺一不可。
D 的方式
D 认识到比较运算符在根本上互相之间是有联系的。所以只用一个函数是必需的:struct A { int opCmp(int i); }编译器依照 opCmp 函数自动解释 <、<=、> 和 >= 运算符,并处理左操作数不是对象引用的情况。
类似这样的明智的规则也适用于其他的运算符重载,这就使得 D 中的运算符重载不像在 C++ 中那样繁琐且易于出错。只需要少得多的代码,就可以达到相同的效果。
名字空间 using 声明
C++ 的方式
C++ 中的 using 声明 用来从一个名字空间作用域将名字引入当前的作用域:namespace Foo { int x; } using Foo::x;
D 的方式
D 用模块来代替名字空间和 #include 文件,用别名声明来代替 using 声明:---- Module Foo.d ------ module Foo; int x; ---- Another module ---- import Foo; alias Foo.x x;别名比简单的 using 声明灵活得多。别名可以用来重命名符号,引用模板成员,引用嵌套类型别等。
RAII(资源获得即初始化)
C++ 的方式
在 C++ 中,资源如内存等,都需要显式的处理。因为当退出当前作用域时会自动调用析构函数,RAII 可以通过将资源释放代码放进析构函数中实现:class File { Handle *h; ~File() { h->release(); } };
D 的方式
大多数的资源释放问题都是简单的跟踪并释放内存。在 D 中这是由垃圾收集程序自动完成的。除了内存外,用得最普遍的资源要数信号量和锁了,在 D 中可用 synchronized 声明和语句自动管理。其余少见的情况可用 auto 类处理。Auto 类退出其作用域时,会调用它们的析构函数。
auto class File { Handle h; ~this() { h.release(); } } void test() { if (...) { auto File f = new File(); ... } // f.~this() 在反大括号处开始运行,即使是因为抛出一个异常才退出该作用域的 }
属性
C++ 的方式
人们常常会定义一个域,同时为它提供面向对象的 get 和 set 函数:class Abc { public: void setProperty(int newproperty) { property = newproperty; } int getProperty() { return property; } private: int property; }; Abc a; a.setProperty(3); int x = a.getProperty();所有这些都不过是增加了击键的次数而已,并且还会使代码变得不易阅读,因为其中充满了 getProperty() 和 setProperty() 调用。
D 的方式
属性可以使用正常的域语法 get 和 set,然后 get 和 set 会被编译器用方法调用取代。class Abc { void property(int newproperty) { myprop = newproperty; } // set int property() { return myprop; } // get private: int myprop; }使用时:
Abc a; a.property = 3; // 等价于 a.property(3) int x = a.property; // 等价于 int x = a.property()因此,在 D 中属性可以被看作一个简单的域名。开始时,属性可以只是一个简单的域名,但是如果后来需要将读取和设置行为改变为函数调用,只需要改动类的定义就够了。这样就避免了定义 get 和 set 时敲入冗长的代码,仅仅是为了‘谨防’日后派生类有可能会不得不重载它们。这也是一种定义接口类的方法,这些类没有数据域,只在语法上表现得好像它们作了实际工作。
递归模板
C++ 的方式
一种使用模板的高级方式是递归的扩展它们,依靠特化来终止递归。用来计算阶乘的模板可能会是这样:template<int n> class factorial { public: enum { result = n * factorial<n - 1>::result }; }; template<> class factorial<1> { public: enum { result = 1 }; }; void test() { printf("%d/n", factorial<4>::result); // 打印出 24 }
D 的方式
D 的版本与之相似,但是简单一点,利用了将单一模板成员提升到外围的名字空间的能力:template factorial(int n) { enum { factorial = n* .factorial!(n-1) } } template factorial(int n : 1) { enum { factorial = 1 } } void test() { printf("%d/n", factorial!(4)); // 打印出 24 }