C++20 设计模式
0 基础概念
1 (CRTP)奇异递归模板
如何理解CRTP,CRTP我觉得最大的问题是理解难度:一个类怎么能继承自己(是派生类参数)呢?阅读这个链接https://stackoverflow.com/questions/49708984/why-curiously-recurring-template-pattern-crtp-works
什么是CRTP,如何理解CRTP呢?
- 继承自模板类
- 派生类将自身作为参数传递给模板类
如何理解CRTP的继承关系?一般来说继承的语义是说派生类是一种基类,将对基类的调用定向到派生类上。
然而CRTP是完全不同的,派生类并不是基类,它实际上是通过继承手段拓展了基类来增加更多函数性(With the CRTP the situation is radically different. The derived class does not express the fact it “is a” base class. Rather, it expands its interface by inherting from the base class, in order to add more functionality.)。
这种情况下,直接使用派生类才有意义,绝不要使用基类,当然只适合CRTP的前几种用法,不适合静态接口情况 (which is true for this usage of the CRTP, but not the one described below on static interfaces).
所以实际上基类不是接口,派生类也不是实现。恰恰相反,基类使用派生类的接口函数,是派生类给基类提供接口,这种CRTP的继承和传统继承关系不是相反的( In this regard, the derived class offers an interface to the base class. This illustrates again the fact that inheritance in the context of the CRTP can express quite a different thing from classical inheritance)
CRTP的目的是什么?
- 可以使用static_cast静态绑定将基类转换成派生类使用,而普通基类转派生类用的是dynamic_cast动态绑定。避免了虚函数的消耗
- 创建静态接口
换言之CRTP是基类对子类的调用,一种反向调用
2.1 CRTP的用处
CRTP的用法简单来说有以下几种:
- Some classes provide generic functionality, that can be re-used by many other classes.
- The second usage of the CRTP is, as described in this answer on Stack Overflow, to create static interfaces. In this case, the base class does represent the interface and the derived one does represent the implementation, as usual with polymorphism.
2.1.1 添加通用函数
CRTP的作用之一是添加通用函数,比方说下面的代码
class Sensitivity
{
public:
double getValue() const;
void setValue(double value);
// rest of the sensitivity's rich interface...
};
如果我要添加几个函数,那么需要改成这样子
class Sensitivity
{
public:
double getValue() const;
void setValue(double value);
void scale(double multiplicator)
{
setValue(getValue() * multiplicator);
}
void square()
{
setValue(getValue() * getValue());
}
void setToOpposite()
{
scale(-1);
};
// rest of the sensitivity's rich interface...
};
那么我可以直接使用CRTP写一个通用函数类然后让每个其它类别继承基础类
template <typename T>
struct NumericalFunctions
{
void scale(double multiplicator)
{
T& underlying = static_cast<T&>(*this);
underlying.setValue(underlying.getValue() * multiplicator);
}
void square()
{
T& underlying = static_cast<T&>(*this);
underlying.setValue(underlying.getValue() * underlying.getValue());
}
void setToOpposite()
{
scale(-1);
};
};
换言之,只要让每个类别成为一个CRTP的类别,就可以实现通用代码了,不需要再手动给每个类别添加类了。
class Sensitivity : public NumericalFunctions<Sensitivity>
{
public:
double getValue() const;
void setValue(double value);
// rest of the sensitivity's rich interface...
};
当然这带来一个问题,为啥不使用函数模板而使用CRTP呢?很简单,因为CRTP不会像函数模板一样子隐藏实现。
2.2.2 static interface
静态多态,没有虚函数表参与。
比方说一个基础类型为
template <typename T>
class Amount
{
public:
double getValue() const
{
return static_cast<T const&>(*this).getValue();
}
};
然后两个类别可以分别继承
class Constant42 : public Amount<Constant42>
{
public:
double getValue() const {return 42;}
};
class Variable : public Amount<Variable>
{
public:
explicit Variable(int value) : value_(value) {}
double getValue() const {return value_;}
private:
int value_;
};
这两个类别就变成了兼容两种Amount了,虽然这里有多态参与,但是并没有运行时多态,反而为编译时多态
template<typename T>
void print(Amount<T> const& amount)
{
std::cout << amount.getValue() << '\n';
}
2.3 如何简单使用CRTP
简单来说包括两种用法:
template <typename T>
struct crtp
{
T& underlying() { return static_cast<T&>(*this); }
T const& underlying() const { return static_cast<T const&>(*this); }
};
2.4 CRTP的误用
使用CRTP并不是无代价
1 构造模式
构造模式基本是最简单的模式通信模式了,基本上稍微复杂点的类别都会调用“构造模式”(虽然可能并没有这个含义)
1.1 Fluent Builder
By returning a reference to the builder itself, the builder calls can now be chained. This is what’s called a fluent interface:
HtmlBuilder* add_child(string child_name, string child_text)
{
root.elements.emplace_back(child_name, child_text);
return this;
}
1.2 强制使用构造模式提供的函数
怎样强制用户使用我们提供的创建新模式的函数,而不是直接使用构造函数呢?很简单,隐藏构造函数,暴露static的函数出来
struct HtmlElement
{
string name;
string text;
vector<HtmlElement> elements;
const size_t indent_size = 2;
static unique_ptr<HtmlBuilder> create(const string& root_name)
{
return make_unique<HtmlBuilder>(root_name);
}
protected: // hide all constructors
HtmlElement() {}
HtmlElement(const string& name, const string& text)
: name{name}, text{text}
{
}
};
1.3 Groovy-Style Builder
将构造函数改成Protected,通过让派生类的构造函数调用特定的构造函数来简化API使用难度
struct Tag
{
...
protected:
Tag(const string& name, const string& text)
: name{name}, text{text} {}
Tag(const string& name, const vector<Tag>& children)
: name{name}, children{children} {}
};
struct P : Tag
{
explicit P(const string& text)
: Tag{"p", text} {}
P(initializer_list<Tag> children)
: Tag("p", children) {}
};
struct IMG : Tag
{
explicit IMG(const string& url)
: Tag{"img", ""}
{
attributes.emplace_back({"src", url});
}
};
1.4 Composite Builder
Composite Builder的目的是为了简化各个模块组合起来构造对象的难度,There are two aspects to Person: their address and employment information. What if we want to have separate builders for each – how can we provide the most convenient API?
class PersonBuilderBase
{
protected:
Person& person;
explicit PersonBuilderBase(Person& person)
: person{person} {}
public:
operator Person()
{
return move(person);
}
// builder facets
PersonAddressBuilder lives() const;
PersonJobBuilder works() const;
};
This is much more complicated than our simple Builder earlier, so let’s discuss each member in turn:
- person is a reference to the object that’s being built. This may seem rather strange, but it’s done deliberately for the sub-builders. Note that the physical storage of Person is not present in this class. This is critical! The root class only holds a reference, not the constructed object.
- The reference-assigning constructor is protected so that only the inheritors (PersonAddressBuilder and PersonJobBuilder) can use it.
- operator Person is a trick that we’ve done before. I’m making the assumption that Person has a properly defined move constructor – it’s easy to generate one in an IDE.
- lives() and works() are functions returning builder facets: those sub-builders that initialize the address and employment information separately.
class PersonBuilder : public PersonBuilderBase
{
Person p; // object being built
public:
PersonBuilder() : PersonBuilderBase{p} {}
};
class PersonAddressBuilder : public PersonBuilderBase
{
typedef PersonAddressBuilder self;
public:
explicit PersonAddressBuilder(Person& person)
: PersonBuilderBase{ person } {}
self& at(string street_address)
{
person.street_address = street_address;
return *this;
}
self& with_postcode(string post_code) { ... }
self& in(string city) { ... }
};
Person p = Person::create()
.lives().at("123 London Road")
.with_postcode("SW1 1GB")
.in("London")
.works().at("PragmaSoft")
.as_a("Consultant")
.earning(10e6);
1.5 Builder Inheritance
是否可以继承Fluent Builder?这个有一些难度,因为类型不同很难继承,比方说下面的代码
class PersonBuilder
{
protected:
Person person;
public:
[[nodiscard]] Person build() const {
return person;
}
};
class PersonInfoBuilder : public PersonBuilder
{
public:
PersonInfoBuilder& called(const string& name)
{
person.name = name;
return *this;
}
};
class PersonJobBuilder : public PersonInfoBuilder
{
public:
PersonJobBuilder& works_as(const string& position)
{
person.position = position;
return *this;
}
};
// Why won’t the preceding code compile? It’s simple: called() returns *this, which is of type PersonInfoBuilder&; this simply doesn’t have the works_as() method!
PersonJobBuilder pb;
auto person =
pb.called("Dmitri")
.works_as("Programmer") // will not compile
.build();
这个之所以有问题是因为,called函数不是虚函数,返回的this还是自己的类型
解决办法也非常简单,调用CRTP即可,简单来说,一层一层的CRTP
template <typename TSelf>
class PersonInfoBuilder : public PersonBuilder
{
public:
TSelf& called(const string& name)
{
person.name = name;
return static_cast<TSelf&>(*this);
// alternatively, *static_cast<TSelf*>(this)
}
};
template <typename TSelf>
class PersonJobBuilder :
public PersonInfoBuilder<PersonJobBuilder<TSelf>>
{
public:
TSelf& works_as(const string& position)
{
this->person.position = position;
return static_cast<TSelf&>(*this);
}
};
2 工厂模式
为什么需要工厂模式?答案很简单,因为很多时候构造出来的东西需要和其它的类型产生关联,可能会造成影响,因此需要调用一个统一的工厂入口
3 原型模式
The fact is the Prototype pattern is all about copying objects. And, of course, we do not have a universal way of actually copying an object, but there are options, and we’ll choose some of them.
简单来说就是涉及到对象拷贝的时候,到底如何满足拷贝的要求(比方说重复拷贝,比方说深度拷贝啥的应该如何实现),这个之所以复杂是因为涉及到了继承,派生什么的奇特情况。所以一种见的解决方法就是虚函数!
class ExtendedAddress : public Address
{
public:
string country, postcode;
ExtendedAddress(const string &street, const string &city,
const int suite, const string &country,
const string &postcode)
: Address(street, city, suite)
, country{country}, postcode{postcode} {}
};
ExtendedAddress ea = ...;
Address& a = ea;
// how do you deep-copy `a`?
解决办法
virtual Address clone()
{
return Address{street, city, suite};
}
virtual Address* clone()
{
return new Address{street, city, suite};
}
ExtendedAddress* clone() override {
return new ExtendedAddress(street, city, suite,country, postcode);
}
ExtendedAddress ea{"123 West Dr", "London", 123, "UK", "SW101EG"};
Address& a = ea; // upcast
auto cloned = a.clone();
另外一个方面,如果希望信息可以多类型共享,那么直接建一个工厂模式,存储这些不同的地址,避免拷贝+避免重复。
组合模式
4 装饰器和Proxy模式
19 观察者模式
观察者模式有很多种不同的类型,一种是属性观察,就是跟踪某个属性的变化。方法很简单,收敛对属性的写操作,每次写操作发生做通知
通用的实现类型如下
// 实现方法1.0,这种方法的问题是field_name可能会发生变化
template<typename T> struct Observer
{
virtual void field_changed(T& source,
const string& field_name) = 0;
};
有观察者,必然就有被观察者。被观察者需要实现通知,订阅功能
template <typename T> struct Observable
{
void notify(T& source, const string& name) { ... }
void subscribe(Observer<T>* f) { observers.push_back(f); }
void unsubscribe(Observer<T>* f) { ... }
private:
vector<Observer<T>*> observers;
};
// CRTP丢给Persion
struct Person : Observable<Person>
{
void set_age(const int age)
{
if (this->age == age) return;
this->age = age;
notify(*this, "age");
}
private:
int age;
};
将观察者和被观察者结合到一起
struct ConsolePersonObserver : Observer<Person>
{
void field_changed(Person& source,
const string& field_name) override
{
cout << "Person's " << field_name << " has changed to "
<< source.get_age() << ".\n";
}
};
对于观察者模式,有个非常经典的依赖问题。对于由一个属性变化引起的观察,可以直接在设置函数的时候做变化,但如果是需要依赖两个乃至多个属性呢?甚至如果一个属性的变化会引起多个依赖更新,那么这函数岂不是天天修改?
Reentrancy问题
简化观察者内核,使用Decrator来包裹观察者模式简化使用修改代码的难度。参看下面的内容,PersionView是真正的被观察者
struct Person
{
string name;
};
struct PersonView : Observable<Person>
{
explicit PersonView(const Person& person)
: person(person) {}
string& get_name()
{
return person.name;
}
void set_name(const string& value)
{
if (value != person.name) return;
person.name = value;
property_changed(person, "name");
}
protected:
Person& person;
};
结尾
唉,尴尬