2024-07-04-C++20设计模式

C++越来越好用啦

Posted by 大狗 on July 4, 2024

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是基类对子类的调用,一种反向调用

1.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.
1.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不会像函数模板一样子隐藏实现。

1.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';
}

1.3 如何简单使用CRTP

简单来说包括两种用法:

template <typename T>
struct crtp
{
    T& underlying() { return static_cast<T&>(*this); }
    T const& underlying() const { return static_cast<T const&>(*this); }
};

1.4 CRTP的误用

使用CRTP并不是无代价

2 MixIn inherence

Mixin Inherence实际上是在派生类里调用传递的类型T的某个方法,这种方法不会对基类T造成干扰,但是可能会造成拷贝成本。CRTP是给传入的类添加函数,而Mixin inherence是给传入的类生成派生类并进行调用。所以实际上Mixin inherence往往用来做拓展模式,说的感觉不像是人话,简单解释下:

  • CRTP是使用“包含派生类的typename的基类”给“派生类”(实际上是我们传递的typename)添加功能。我们最终获得的类,就是我们传入的类,也就是派生类。
  • MixIn Inherence则是“包含基类的typename“,派生出来真正要使用的新类别。我们最终获得的类,不是我们传入的类(我们传入的是基类),而是基类添加了新的信息的派生类。
template <typename T> Struct Mixin: T {

}

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 单例模式

4.1 单例模式的问题

单例模式不需要多说了,非常经典,单例模式的问题有两种

  • 如果需要使用其他static对象,那么初始化的顺序是不能控制的,可能会出现依赖未初始化导致程序崩溃的问题
  • 对基于单例模式的代码写UT会非常脆弱和dummy

针对第二种问题的解决方式很简单,简单来说就是让代码不再依赖具体的类,而是依赖plungable的组件,或者说单例真正包含内部的代码。比方说我现在有个database类型,拓展为单例,

class Database{
public:
	virtual intget_population(const string& name) = 0;
};

class SingletonDatabase : public Database{
  SingletonDatabase() { /* read data from database */ }
  map<string, int> capitals;
public:
  SingletonDatabase(SingletonDatabase const&) = delete;
  void operator=(SingletonDatabase const&) =delete;
  static SingletonDatabase& get() {
    staticSingletonDatabase db;
    return db; 
  }
  int get_population(const string& name) override {
    return capitals[name];
  }
};

那么我写一个工具函数来从Database里面拿数据,然后取出来的时候就会导致问题。因为它依赖单例,而单例里面的任何数据的改变都会导致它依赖项的变化。实际上这种做法相当于要去拿真正的数据库连接做操作。下面的代码就很糟糕。

struct SingletonRecordFinder {
  int total_population(vector<string> names){
    int result = 0;
    for (auto& name : names) 
      result +=SingletonDatabase::get().get_population(name); 
    returnresult;
  }
};

TEST(RecordFinderTests,SingletonTotalPopulationTest){
  SingletonRecordFinderrf;
  vector<string> names{ "Seoul", "Mexico City" };
  inttp = rf.total_population(names);
  EXPECT_EQ(17500000 + 17400000, tp);
}

所以应该怎么写呢?简单来说,写pluginable的东西。这个应该稍微有点经验的工程师都会,说实话。。。

struct ConfigurableRecordFinder{
  explicitConfigurableRecordFinder(Database& db) : db{db} {}
  int total_population(vector<string> names) {
    int result= 0;
    for (auto& name : names)
      result += db.get_population(name);
    return result;
  }
  Database& db;
};
          
class DummyDatabase : public Database{
  map<string,int> capitals;
  public:DummyDatabase(){
    capitals["alpha"] = 1;
    capitals["beta"] = 2;
    capitals["gamma"] = 3;
  }
  int get_population(const string& name) override {
    return capitals[name];
  }
};

TEST(RecordFinderTests, DummyTotalPopulationTest){
  DummyDatabase db{};
  ConfigurableRecordFinder rf{db };
  EXPECT_EQ(4, rf.total_population(vector<string>{"alpha", "gamma"}));
}

4.2 Per thread单例模式

实际上实现起来很简单,就是加个thread_local的标志。这里多说一句,static thread_local和thread_local是一样的。

template <typename T>
class SingletonCRTP {
 public:
  static T& GetInstance() {
    thread_local T instance;
    return instance;
  }
 // why must be protected? Derived Singleton will call implicited 
 protected:
  SingletonCRTP() = default;
  ~SingletonCRTP() = default;

  // 禁止拷贝构造和赋值操作
  SingletonCRTP(const SingletonCRTP&) = delete;
  SingletonCRTP& operator=(const SingletonCRTP&) = delete;
};

结构相关模式

6 适配器模式

这个就不多说了,适配器实际上就是要使用的函数接口只支持特定格式,需要转换一层

7 桥接模式

8 Composite(组合)模式

composite模式从名字来说是组合模式(Composite Pattern),我个人觉得实际上是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。但是这里有个问题,本质来说,组合模式组合的对象是功能一致的对象,还是说只是接口相同的对象呢?

按照查询到的资料来说,接口相同即可。但是功能相同放到一起实际上更符合DDD之类的概念,那么这里应该如何取舍呢?这个我也没想明白。

话说回来,C++怎么实现Composite模式呢?追求能够将不同的组件组合到一起,这里(实际上也有我个人的理解)有三种方式:

  • 方法1 : 利用传统的C++ 多态的方式,composite的组合 & 整体部分都使用整体的代码。代码如下:

    lass Graphic {
    public:
        virtual void draw() const = 0;
        virtual ~Graphic() = default;
    };
      
    // 叶子节点,具体图形:圆
    class Circle : public Graphic {
    public:
        void draw() const override {
            std::cout << "Drawing a Circle" << std::endl;
        }
    };
      
    // 叶子节点,具体图形:正方形
    class Square : public Graphic {
    public:
        void draw() const override {
            std::cout << "Drawing a Square" << std::endl;
        }
    };
      
    // 组合节点,图形组合
    class CompositeGraphic : public Graphic {
    public:
        void add(std::shared_ptr<Graphic> graphic) {
            children.push_back(graphic);
        }
      
        void draw() const override {
            for (const auto& child : children) {
                child->draw();
            }
        }
      
    private:
        std::vector<std::shared_ptr<Graphic>> children;
    };
      
    /* 具体用法就得靠调用包裹Graphic的add方法来做
    std::shared_ptr<CompositeGraphic> compositeGraphic = std::make_shared<CompositeGraphic>();
    compositeGraphic->add(circle1);
    compositeGraphic->add(circle2);
    compositeGraphic->add(square);
    */
      
    
  • 方法2: 使用类似CRTP的方式,这种方法就适合实现静态的composite模式。好处是,坏处是需要多实现一些东西,比方说begin & end。这里实际上已经可以使用C++ 17 的Fold Expression了

      
    #include <iostream>
    #include <string>
    #include <vector>
      
    class Component;
      
    template <typename Self>
    struct SomeComponents {
        template<typename T> void ConnectTo(T &other) {
            for (Component &from: *static_cast<Self*>(this)) {
                for (Component &to: other) {
                    from.out.push_back(&to);
                    to.in.push_back(&from);
                }
            }
        }
      
        virtual Component* begin() = 0;
        virtual Component* end() = 0;
      
    };
      
    struct Component : SomeComponents<Component>{
        vector<Component*> in, out;
        unsigned int id;
          
        //默认的构造函数,提供了
        Component() {
          // do something
        }
      
        Component *begin() override {
            return this;
        }
      
        Component *end() override {
            return this + 1;
        }
    };
      
    struct ComponentLayer : vector<Component>, SomeComponents<ComponentLayer> {
        ComponentLayer(int count) {
            while (count-- > 0) {
                emplace_back(Component{});
            }
        }
    };
      
      
    int main() {
        Component com, com2;
        ComponentLayer layer, layer2;
        com.connect_to(com2);
        com.connect_to(layer);
        layer.connect_to(com);
        layer.connect_to(layer2);
        return 0;
    }
    
  • 方法3: 让我们使用微软提供的Proxy库+facade模式来实现这个蛋疼的问题。这里假设我们实现的多种模式

9 装饰器模式

装饰器模式主要解决什么问题?装饰器模式(Decorator Pattern)是一种结构型设计模式,主要用于动态地给对象添加新的功能,而不改变其结构。这一模式通过创建一个装饰对象,也就是包装原始对象,实现了功能的扩展。

怎么样?是不是听起来就和Mixin Inherence非常的对应?写起来也确实如此。这里注意我们没有使用继承基类,里面包一个基类的引用来实现Dynamic Decorator,而是使用Mixin Inherence来实现Static Decorator

这里给出两种Static Decorator,一种就是拓展某个对象,另一种是拓展某个函数或者功能

  • 拓展对象,这里假设我们的基础对象是Shape,我们要拓展出来ColorShape和TransparentShape,那么使用Mixin inherence就可以拓展。这里请注意,使用了std::forward,即完美转发,来实现调用基类的方法。这里实际上还有一个问题,就是我们需要限制T必须是继承自最基础的Shape类型,方法有两个

    • static_assert(is_base_of_v<Shape, T>
    • 用Concept,这里我使用的是concept
    struct Shape {
    	virtual string str() const = 0;
    };
      
    template <typename T> struct TransparentShape : T {
    	uint8_t transparency;
    	template<typename...Args>
    	TransparentShape(const uint8_t transparency, Args...args)
        	: T(std::forward<Args>(args)...)
        	, transparency{ transparency } {}
      ...
    };
    
  • 拓展函数/功能,这里注意,暂存了result这个,来实现及时原先的函数直接返回,也可以插入进入和出来的代码。

    template <typename R, typename... Args> struct Logger3<R(Args...)>
    {
    	Logger3(function<R(Args...)> func, const string& name) : func{func}, name{name} {}
    	R operator() (Args ...args) {
    		cout << "Entering " << name << endl;
        R result = func(args...);
    		cout << "Exiting " << name << endl;
        return result;
    	}
        
      function<R(Args ...)> func;
      string name;
    };
    

10 Facade模式

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;
};

结尾

唉,尴尬

狗头的赞赏码.jpg