使用设计技术

使用设计技术

没想到有一天我还会翻开我大一时候课程买的巨厚的书《C++高级编程》,由于以后要用 C++ 就挑里面我感兴趣的仔细看看吧。

设计技术

和设计模式很类似,设计技术介绍的是完成日常任务较好的编程方式,法则。

受限于篇幅,设计模式会在另外的一篇里面讲。

RAII

RAII(Resource Acquisition Is Initialization, 资源获得即初始化),它用于在 RAII 实例离开作用域的时候自动释放以获取的资源,基本上,新的 RAII 实例的构造函数获取特定资源的所有权,并使用资源初始化实例,当销毁的时候,析构函数自动释放所获取的资源。

整体来说,分为四个阶段:

  1. 设计一个类来封装某类资源
  2. 在类的构造函数中初始化
  3. 在析构函数中执行销毁操作
  4. 使用时声明一个该对象的类

由于在函数内部的局部变量在函数退出时就会被销毁,这一切都是自动发生了,所以但我们设计一个好的类,那么就不需要程序员显示的去调用释放资源的操作了。C++ 的 unique_lock 就是遵循这样规范的类。

书上给出的例子很好的说明了这一规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
class File final {
public:
File(std::FILE* file);
~File();
// Prevent copy construction and copy assigment
File(const File &src) = delete;
File& operator=(const File &src) = delete;

// Allow move construction and move assignment
File(File &&src) noexcept = default;
File& operator=(File &&rhs) noexcept = default;
std::FILE* get() const noexcept;
std::FILE* release() noexcept;
void reset(std::FILE* file = nullptr) noexcept;

private:
std::FILE* mFile;
};

File::File(std::FILE *file) :mFile(file) {}
File::~File() {
reset();
}
std::FILE * File::get() const noexcept {
return mFile;
}
std::FILE * File::release() noexcept {
std::FILE* file = mFile;
mFile = nullptr;
return file;
}
void File::reset(std::FILE *file) noexcept {
if (mFile) {
fclose(mFile);
}
mFile = file;
}
int main() {
File f(fopen("input.txt", "r"));
return 0;
}
  • 永远不要为 RAII 类增加默认的构造函数或者显示删除默认构造函数,防止在不需要使用变量名的时候忘记而导致行为不符合我们的预期。

    • 如: unique_lock<mutex> lock(mMutex) 是正确的初始化方法,它会创建一个本地对象 lock,该对象将会锁定 mMutex 的数据成员,并在方法结束的时候自动释放互斥体
    • 但如果不小心写成了 unique_lock<mutex>(mMutex),它实际上在函数中会声明一个本地变量 mMutex 并且调用默认构造函数来对 unique_lock 初始化,并没有锁上 mMutex 的成员变量,这不是我们想看到的结果。
  • noexcept 该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化

ScopeGuard

当然,也有人指出(参考最后的链接 2)我们不希望对于一个简单的变量大张旗鼓的写类来完成 RAII 的包装。他给出了更好的解决方法,通过将析构函数传递给一个上下文管理器类似的变量来完成这一系列工作。

1
2
3
4
// 调用模板
ScopeGuard onFailureRollback([&] { /* rollback */ });
... // do something that could fail
onFailureRollback.Dismiss();

只要发生异常就会 rollback,完成正常的析构(显示调用会存在由于异常而不调用析构的问题),当函数正常完成的时候,会解除这个操作。

ScopeGuard 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> onExitScope)
: onExitScope_(onExitScope), dismissed_(false){ }
~ScopeGuard(){
if(!dismissed_){
onExitScope_();
}
}
void Dismiss(){
dismissed_ = true;
}
private:
std::function<void()> onExitScope_;
bool dismissed_;
private: // noncopyable
ScopeGuard(ScopeGuard const&);
ScopeGuard& operator=(ScopeGuard const&);
};
// 具体例子
// HANDLE h = CreateFile(...);
// ScopeGuard onExit([&] { CloseHandle(h); });

双分派(double dispatch)

根据对象的类型而对方法进行的选择,就是分派(Dispatch)

  • 一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。
  • 根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
  • C++ 和 Java 就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。

但我们在多态的时候往往会遇到这样的一个问题:不同的派生类对于其他的派生类有不同的表现的时候,如优化下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <typeinfo>
class Animal {
public:
virtual bool eats(const Animal& prey) const = 0;
};

class Fish: Animal {
};

class Dinosaur: Animal {
};

class Bear: Animal {
bool eats(const Animal &prey) const override {
if (typeid(prey) == typeid(Bear)) {
return false;
}
else if (typeid(prey) == typeid(Fish)) {
return true;
}
else if (typeid(prey) == typeid(Dinosaur)) {
return false;
}
else {
return false;
}
};
};

看起来没有什么问题,但对于每个动物都要编写长长的 if 语句,更严重的问题在,如果新加入一种捕食关系,这意味着要新改动很多的类。这显然不是好的多态,也没有实现多态的目的。

双分派的关键在于基于参数上的方法调用来确定结果,我们不妨给 Animal 加入一个 eaten_by 方法,该方法将不同的 Animal 作为参数,这时编写 eat 函数时就可以简洁化了,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// declarations
class Fish;
class Bear;
class Dinosaur;

class Animal {
public:
virtual bool eats(const Animal& prey) const = 0;
virtual bool eaten_by(const Bear&) const = 0;
virtual bool eaten_by(const Fish&) const = 0;
virtual bool eaten_by(const Dinosaur&) const = 0;

};
class Bear: Animal {
bool eats(const Animal &prey) const override{
return prey.eaten_by(*this);
};
bool eaten_by(const Bear &) const override {
return false;
};
bool eaten_by(const Dinosaur &) const override {
return false;
}
bool eaten_by(const Fish &) const override {
return true;
}
};
class Fish: Animal {
};

class Dinosaur: Animal {
};

这里发生了两次多态确定类型:调用 eatseats 调用 eaten_by。比较重要的想到一个被动的关系,让被调用者实现以调用为参数的函数来完成不用显示确定类型的多态函数。

混入类

基于多继承的一种实现,除了从主层次结构的父类派生外,还混入类派生。

如果在有些 (Java)GUI 的自定义组件中,我们往往会继承一些函数(或者说是 Java 的接口),如 Clickable,Playable 等。他们就是一种混入类,所有包含这个功能的函数都应该继承并实现里面的虚函数。

又如战棋类游戏,不同的棋子有不同的移动方式,还有些棋子无法移动。这时我们就设计一个 Moveable 类,将需要移动的棋子全部继承它就好。

1
2
3
4
5
6
7
8
class Chess {};
class Moveable {
virtual void move();
};
class Defender: Chess {};
class Attacker: Chess {};
class Barrier : Defender{};
class Knight: Attacker, Moveable{};

MVC 模型

MVC(Model-View-Controller),其中视图是模型的特定可视化形式,并有控制器来操纵视图。

他们的关系大致像下面这样:

举书上的例子:简易的赛车模拟器

1
2
3
4
5
6
7
8
9
10
11
12
class RaceCar {
public:
RaceCar();
virtual ~RaceCar() = default;
virtual double getSpeed() const;
virtual void setSpeed(double speed);
virtual double getDamageLevel() const;
virtual void setDamageLevel(double damage);
private:
double mSpeed;
double mDamageLevel;
};

在简易的情况下,我们考虑赛车的两个视图,图形视图,和随着时间推移的受损程度。当赛车遇到混凝土护栏时,控制器函数要设置赛车的速度和受损级别,比如说要大幅提高受损程度,大幅降低速度等等。

参考链接

Author

Ctwo

Posted on

2021-07-29

Updated on

2021-07-29

Licensed under

Comments