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

C++越来越好用啦

Posted by 大狗 on July 4, 2024

C++20 设计模式

这本书的代码如果自己写的有问题就直接去看下https://github.com/Apress/design-patterns-in-modern-cpp怎么写的,我发现某个实现者在github写的胡说八道的代码,然后自己标注有问题还上传。。。。

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

Template Metaprograming with C++

建议看看这个 https://www.bilibili.com/video/BV1JK4y1D7Yz/

1 基础模板概念

有下面几种概念,

  • 类模板
  • 函数模板
  • 区分类模板的成员函数
  • 区分类成员函数模板
  • 区分类模板的类成员函数模板

还有关于类型的一些概念

  • 类型模板参数
  • 非类型模板参数
  • 双重模板参数
    • 这两天我在写一个非常扭曲的代码,简单来说就是两个类型互相依赖,最后的解决方法实际上是使用双重模板参数。
  • 可变参数模板

关于参数包

  • 可变参数函数模板

    • 关于展开参数包有什么作用,这里注意区分argument和parameter的区别:

      • 模板参数列表(parameter),为模板指定参数

      • 模板参数(argument)列表,为模板指定参数

      • 函数参数(parameter)列表,为函数模板指定参数

      • 函数参数(argument)列表,

      • 圆括号初始化列表,展开包出现在直接初始化列表里面、函数样式强制转换或者,成员初始化里面

      • 大括号初始化表达式

      • 基类说明

      • 使用声明

      • Lambda表达式,用于捕获lambda表达式

      • 折叠表达式

      • sizeof···操作符

  • 可变参数类模板

    • 定义
      • 可变参数函数模板需要指定带有两个带有重载的递归模式,一个用于一般模式,另一个用于结束
      • 可变参数类模板也需要同样的方法。
      • 看代码感觉不太对,看看这个https://medium.com/@r.siddhesh96/lets-learn-recursive-templates-by-implementing-std-tuple-b490933206d0
    • 可变参数模板太麻烦,所以有了折叠表达式。
      • 一元左折叠
      • 一元右折叠
      • 二元左折叠
      • 二元右折叠
    • 可变参数别名模板
    • 可变参数变量模板

1.S 关于模板互相依赖的问题

可以直接看这几个stackoverflow的问题:

  • https://stackoverflow.com/questions/213761/what-are-some-uses-of-template-template-parameters/23930985#23930985
  • https://cplusplus.com/forum/general/115940/
  • https://stackoverflow.com/questions/34128529/breaking-template-circular-dependencies-by-using-template-template-parameters

简单来说,一般情况下,我们写代码都是单向依赖,即A依赖B,B依赖C。但是有的时候我们需要让B存储指向A的指针,这种情况下不算严格破坏依赖。如果写的是CC和.h分离的代码,那么具体实现依赖还没什么可说的,但是如果是模板文件,就引发一个问题,即依赖回出现循环

比方说写几个不同的类型,它们相互依赖。

  • template <typename S, typename TaskType> class TCPStream依赖类型S和类型TaskType。其中S需要存储在TCPStream,而TaskType它实际上只是在代码实现中用到了它的代码。
  • template <typename TCPStreamType> class Task 它又依赖类型TCPStream,这个类型里面存储了一个指向TCPStream的智能指针。注意,这里如果两者都互相存储了对端对象,那是不能相互引用的!会造成循环引用,因为必须都先知道具体的内存布局,参考CRTP的解释那部分
  • 最终出现了一个经典的问题,两个模板类互相依赖,实例化的时候开始报错。

这个具体的过程可以理解为

  1. 实例化TCPStream且未使用Handle的代码时,暂时不需要实例化TaskType,继续
  2. 某阶段需要用到Handle的代码,实例化TaskType,发现Tasktype实际上也是个模板类。此时去实例化TaskType
  3. 发现TaskType还需要参数,此时如果把TCPStream再传递进去就无穷无尽了。

可以简化为如下的代码

// TCPStream 需要使用Task,而Task又需要使用TCPStream
template <typename S, typename TaskType>
class TCPStream
  S socket_;
  public:
    S &GetSocket() {
      return socket_;
    }
    void Handle() {
      std::shared_ptr<TaskType> task = TaskType::Create(req, this);
      task->xxx();
      // do something with task
    }
};


template <typename TCPStreamType>
class Task {
public:
  ...
  std::shared_ptr<TCPStreamType> GetConnection() {
    return connection_;
  }

private:
  std::shared_ptr<TCPStreamType> connection_= nullptr;
};

那么如何解决呢?双重模板参数或许是个好的选择。按照下面的方式进行代码更新

  1. Task的代码不变,还是存储指向TCPStreamType的类型

    template <typename TCPStreamType>
    class Task {
    public:
      ...
      std::shared_ptr<TCPStreamType> GetConnection() {
        return connection_;
      }
       
    private:
      std::shared_ptr<TCPStreamType> connection_= nullptr;
    };
    
  2. 修改TCPStream的类型,指明它的第二个参数是个模板类。简单来说就是,这是一个双重模板参数

    template <typename S, template<typename> class DetectionTaskType>
    class TCPStream
      S socket_;
      public:
        S &GetSocket() {
          return socket_;
        }
        void Handle() {
          std::shared_ptr<TaskType> task = TaskType::Create(req, this);
          task->xxx();
          // do something with task
        }
    };
    
  3. 注意,这里有个点,需要在类的内部定义DetectionTaskType包裹自己的情况。可以看到这里将DetectionTaskType包裹住了TCPStream

    // TCPStream 需要使用Task,而Task又需要使用TCPStream
    template <typename S, template<typename> class TaskType>
    class TCPStream {
      public:
      // 注意这里的TaskType<TCPStream>
      typedef TaskType<TCPStream> WrapperTask;
      ...
    };
    
  4. 到这里就出现了好玩的东西。接下来是显式实例化。让我们一句一句看。首先在用到两个类的地方使用using显式实例化TCPStream,定义为WrapperTCPStreamT类型。这里注意,首先

    1. 首先typename S,或者说Socket是TCPStream的类成员,它的实例化的时候,已经是一个完整的类型了。实际上除了TCPStream::Handle需要用Task类型,其它都不用
    2. 但是TCPStream::Handle函数还没使用到,换言之,这个时候就不会实例化TCPStream::Handle。所以这个时候对TCPStream的内存布局,类定义的实例化已经结束了。所以此时TCPStream的类定义可以认为完成了。
    using WrapperTCPStreamT = TCPStream<Socket, Task>;
    
  5. 接下来,再使用另一个using来显式实例化Task类型。这个时候,实际上我们也没有实例化刚才的TCPStream::Handle函数。时候我们拿到了的WrapperTaskT,实际上是TaskType<TCPStream>。而这个时候TCPStream已经完成了类定义。这个时候TaskType内部需要的TCPStreamType类型已经是具体的类了,它并不依赖Task类型。所以TaskType实例化完成。

    using WrapperTaskT = WrapperTCPStreamT::WrapperTask;
    

解释了上面的东西,我们来改一下代码,做点好玩的东西。假设TaskType内部存储的不是shared_ptr<TCPStreamType>,存储的就是TCPStreamType。那么这段代码还能实例化吗?首先使用理论分析TCPStream可以实例化,因为它的内存布局不需要依赖Task。然后Task依赖的TCPStream已经实例化,所以理论上应该可以实例化。

具体实现的代码就不写了。

2 模板的基本概念

模板实例化是理解模板能够工作的重点,需要重点理解

模板只是蓝图,编译器在遇到模板时,会根据模板创建实际代码。从模板声明中为函数、类或 变量创建定义的行为称为模板实例化。这可以是显式的 (告诉编译器何时应该生成定义时),也可以 是隐式的 (编译器根据需要生成新定义时)

2.1 隐式实例化

  • 对于函数模板,当用户代码在需要函数定义存在的上下文中引用函数时,就会发生隐式实例化。

  • 对于类模板,当用户代码在需要完整类型的上下文中引用模板时,或者当类型的完整性影响代码时, 也会隐式实例化。此类上下文是构造此类类型的对象,
  • 声明指向类模板的指针时就是另外一种 情况了

比方说

template <typename T>
struct foo
{
	void f() {}
  void g() {}
};

int main() {
	foo<int>* p; 
  foo<int> x; 
  foo<double>* q;
	x.f();
	q->g();
}

通过这些更改,编译器需要实例化以下内容:

  • 当声明 x 变量时,实例化 foo<int>
  • 当 x.f() 调用发生时,实例化 foo<int>::f()
  • 当 q->g() 调用发生时,实例化 foo<double> 和 foo<double>::g()。

另外,当声明指针 p 时,编译器不需要实例化 foo<int>;当声明指针 q 时,也不需要实例化 foo<double>

简单总结就是,隐式实例化声明指针时不会实例化,声明对象时只实例化内存部分。只有实例化函数调用才会显示实例化对应的函数(和内存布局,如果原先没实例化内存布局)

2.2 显式实例化

可以显式地告诉编译器实例化类模板或函数模板,这称为显式实例化。它有两种形式: 显式实例化定义显式实例化声明

2.2.1 显式实例化定义

显式实例化定义实际上是传入类模板参数,定义出来类型。语法如下(如果包含namepsace,那么显示实例化定义需要包含namespace之类的信息,也叫做完全限定)。其中[1]和[2]是显式的定义。函数定义同样如此,

// 类定义显式实例化
template class-key template-name <argument-list>;

// 函数定义显式实例化
template return-type name<argument-list>(parameter-list);
template return-type name(parameter-list);

namespace ns {
template <typename T> struct wrapper
{
	T value;
};

template struct wrapper<int>; // [1]

template struct ns::wrapper<double>; // [2]
int main() {}

2.2.2 显式实例化声明

4 高级模板概念

4.1 名称绑定和依赖名称

首先需要理解名称绑定和依赖名称

  • 名称绑定和依赖名称:

    • 依赖名称和非依赖名称的定义

      • 依赖名称:依赖模板参数的类型或值的名称,可以是类型参数、非类型形参或模板参数。•依赖名称,在模板实例化时执行。
      • 非依赖名称:不依赖于模板参数的名称称为非依赖名称。• 非依赖名称,则在模板定义时执行。
    • 依赖名称和非依赖名称的绑定

      • For a non-dependent name used in a template definition, unqualified name lookup takes place when the template definition is examined. The binding to the declarations made at that point is not affected by declarations visible at the point of instantiation. For a dependent name used in a template definition, the lookup is postponed until the template arguments are known, at which time ADL examines function declarations with external linkage(until C++11) that are visible from the template definition context as well as in the template instantiation context, while non-ADL lookup only examines function declarations with external linkage(until C++11) that are visible from the template definition context (in other words, adding a new function declaration after template definition does not make it visible except via ADL). The behavior is undefined if there is a better match with external linkage in the namespaces examined by the ADL lookup, declared in some other translation unit, or if the lookup would have been ambiguous if those translation units were examined. In any case, if a base class depends on a template parameter, its scope is not examined by unqualified name lookup (neither at the point of definition nor at the point of instantiation).

        看下这个解释:https://stackoverflow.com/questions/63392144/when-is-adl-lookup-is-considered-for-unqualified-dependent-name

理解两种名称之后,就需要知道名称查找的流程

4.2 两阶段名称查找

模板的实例化会分为两个阶段:

  • 第一个阶段发生在定义时,检查模板语法并将名称分类为依赖或非依赖。
  • 第二个阶段发生在实例化时,此时模板实参替换为模板参数。依赖名称的绑定这时发生。

这个分为两步的过程称为两阶段名称查找

4.3 如果依赖名称是一个类型?

需要显示指定这个类型是谁的类型。

4.4 依赖模板的名称?

某些情况下,依赖名称是模板,例如函数模板或类模板。但编译器的默认行为是将依赖项名称解释为非类型

4.5 模板特化

  • 转发引用
  • decltype:这个下面是简单理解的方式,想要精确理解看https://en.cppreference.com/w/cpp/language/decltype。注意delcltype只是查询操作数的类型,如果操作数是表达式,并不会真的执行对应的表达式
    1. 解析过程
      1. 若表达式是标识符或类成员访问,则结果是由表达式命名的实体类型。若实体不存在,或者是具有重载集的函数(存在多个同名函数),编译器将报错。
      2. 若表达式是函数调用或重载操作符函数,则结果为函数白的返回类型。若重载的操作符括在括号中,则忽略这些操作符。
      3. 若表达式是左值,则结果类型是对表达式类型的左值引用。
      4. 若表达式为其他类型,则结果类型为表达式的类型。
      5. decltype表达式中使用的对象的const或volatile说明符不构成推导的类型。
      6. 对象或指针表达式是左值还是右值并不影响推导的类型。
      7. 若数据成员访问表达式括号括起来,例如decltype((expression),则前两条规则不适用。对象的const或volatile限定符确实会影响推导的类型,包括对象的值。这个说白了就是左值右值会影响表达式的类型,现在可能出现decltype是右值的情况,原先的操作数包上了()就变成了表达式
    2. 注意事项,参考https://cplusplus.com/forum/general/285738/,注意decltype(auto) 和auto区分的情况。这里使用decltype隐藏着传递引用回来,而如果是auto实际上是隐藏copy
  • declval
  • 模板的友情:如果希望严格控制模板友元的使用,参考律师客户模式
  • 模板实例化

5 类型特征和条件编译

  • 类型特征

  • SFINAE:

    • SFINAE表示替换失败而不是错误。给一个SFINAE的例子,这里第二个参数是char(*)[1]或者是char(*)[0],char(*)[0]对应SFINAE错误

      template <typename T, size t N>
      void handle(T(&arr) [N], char(*) [N % 2 == 0] = 0)
      {
        std::cout << "handle even array\n"; 
      }
          
      template <typename T, size t N>
      void handle(T(&arr) [N], char(*) [N % 2 == 1]= 0)
      {
        std::cout << "handle odd array\n";
      }
          
      int arr1[]{ 1,2,3,4,5 };
      handle (arr1);
      int arr2[]{ 1,2,3,4 };
      handle (arr2);
      
    • SFINAE只适用用模板声明(模板参数列表,函数返回类型,函数参数列表)

  • enable_if:只有定义出来的东西为真才能使用对应type的东西。这里需要注意template < typename T = void >的含义,参考https://stackoverflow.com/questions/34459640/what-does-typename-enable-void-mean。这里参考https://en.cppreference.com/w/cpp/types/enable_if也可。这里给一个经典的错误

    #include <iostream>
    #include <type_traits>
      
    template <typename T,
    	typename std::enable_if<std::is_integral_v<T>>::type* = nullptr>
    void output(T const &value){
      std::cout << "value is " << value << std::endl;
    }
      
    void f() {
      std::cout << "hahahaha" << std::endl;
    }
      
    int main() {
      int a = 100;
      output<int>(a);
      output<int, nullptr>(a);
      // 这里注意,对函数取地址是void(*)(),即函数指针
      // void*是通用指针
      output<int, (void*)(&f)>(a);
      static_assert(std::same_as<decltype(&f), void(*)()>, "void(*)()");
      return 0;
    }
    
    • 用途

      • 定义具有默认参数的模板参数。参考

      • 定义具有默认参数的函数参数。参考

      • 指定函数的返回类型。

        //std::enable_if_t,是
        
  • constexpr if

  • 使用模板来构建范型函数,std::void_t 参考https://en.cppreference.com/w/cpp/types/void_t。这里有个很好玩的点,参考

    template <typename, typename... Ts>
       struct has_common_type : std::false_type {};
      
    template <typename... Ts>
       struct has_common_type<std::void_t<std::common_type_t<Ts...>>, Ts...>
          : std::true_type {};
      
    template <typename... Ts>
       constexpr bool has_common_type_v =
          sizeof...(Ts) < 2 ||
          has_common_type<void, Ts...>::value;
    // 如果std::common_type_t<Ts...>有结果,
    // 为什么这里的has_common_type<void, Ts...>不会直接解析到
    // 上面的template <typename, typename... Ts> struct has_common_type : std::false_type {};
    

    解释如下:

    • template <typename, typename... Ts> struct has_common_type : std::false_type {};
      
      • 默认情况下,has_common_type继承自std::false_type,意味着如果没有找到特定的特化版本,它将返回false
    • template <typename... Ts> struct has_common_type<std::void_t<std::common_type_t<Ts...>>, Ts...> : std::true_type {};
      
      • 这个特化版本用来检测std::common_type_t<Ts...>是否有效。std::void_t<...>是一种典型的SFINAE技巧,它将类型转换为void,以便进行特化匹配。

        如果std::common_type_t<Ts...>是一个有效类型,那么std::void_t<std::common_type_t<Ts...>>也是一个有效类型,从而使得该特化匹配,并has_common_type继承std::true_type。这里注意,在std::common_type_t<Ts...>是否有效时,这个特化的例子实际上是被变成了。

        template <typename... Ts> struct has_common_type<void,  Ts...> : std::true_type {};
        

        所以命中的时候会优先命中这种代码

    • has_common_type<void, Ts...>::value需要判断std::common_type_t<Ts...>的可用性:

      • 如果std::common_type_t<Ts...>可以成功计算,那么在积分该特化时,std::void_t<std::common_type_t<Ts...>>是有效类型。因此has_common_type的这一特化将被选择为最匹配的特化,并导致has_common_type<void, Ts...>::valuetrue

      • 如果std::common_type_t<Ts...>不能成功计算,那么std::void_t<std::common_type_t<Ts...>>将导致类型替换失败(但由于SFINAE,这不是一个编译错误)。在这种情况下,特化匹配失败,编译器将退回至基本模板,导致has_common_type<void, Ts...>::valuefalse

在看has_common_type<void, Ts…>的时候,我实际上犯了一个错误,也就是我认为是直接把void, Ts…带入到has_common_type的第二个表达式里面,变成has_common_type<std::void_t<std::common_type_t<void, Ts…», void, Ts…>。实际上我后来又看了下https://stackoverflow.com/questions/27687389/how-do-we-use-void-t-for-sfinae 才反应过来哪里错了。因为特化本质是比较因为很有意思,所以另起一段:

  1. 在将类型实例化之后,对于默认的类型template <typename, typename... Ts> struct has_common_type : std::false_type {};是必然吻合的,因为此时第一个类型是占位符。所以主模板必然匹配。

  2. 接下来进行特化模板的匹配,这个时候注意并不是将直接把void, Ts…带入到has_common_type的第二个表达式里面,变成has_common_type<std::void_t<std::common_type_t<void, Ts…», void, Ts…>。恰恰相反,这个时候匹配的是将所谓的

    has_common_type<
        // 这个Ts...是下面那个Ts...
        void, Ts...
    >
    

    // 上面的代码块的Ts不和这个跟在template <typename...之后的Ts>对应
    template <typename... Ts>
    has_common_type<
        // 上面的代码块的Ts...和下面的这个Ts...
        std::void_t<std::common_type_t<Ts...>>, Ts...
    >
    

    也就是说这个时候执行的是template argument deduction.。这里的重点是理解下面代码块的Ts…和上面代码块的Ts…对应,而不是和这个地方认为是作为模板参数的,即跟在template <typename...之后的Ts>对应。

  3. 编译器这个时候开始推测了,因为编译器并不能确定void和std::void_t<std::common_type_t<Ts...>>对应,所以这个时候,实际上编译器只能deduce出来Ts…是后面的Ts…

  4. 知道Ts…和后面匹配后,放到std::void_t<std::common_type_t<Ts...>>里面看看能不能匹配,这个时候执行替换发现,欸嘿,刚好匹配。

  5. 最后一步,是选择匹配的模板,确定和哪个一致

这里用个例子来说明,假设我现在要判断<uint32_t, int, double>的情况,自然就有

has_common_type_v<uint32_t, int, double> = has_common_type<void, Ts...>::value; = has_common_type<void, uint32_t, int, double>::value;
  • 首先,has_common_type<void,uint32_t, int, double>和主模板必然匹配

  • 接下来判断能不能命中部分类型

    • 接下来判断has_common_type<void, uint32_t, int, double>能不能特化。先试试看是匹配

      template <typename uint32_t, int, double>
      has_common_type<
          std::void_t<std::common_type_t<uint32_t, int, double>>, uint32_t, int, double
      >
      

      发现std::void_t<std::common_type_t<uint32_t, int, double»,就是void。所以如果Ts…当成uint32_t, int, double,是可以命中特化的。

    • 接下来判断has_common_type<void,uint32_t, int, double>是不是和下面这个东西匹配,也就是Ts…能不能是除了uint32_t, int, double,之外的任何东西,比方说int, double。肯定不可能,因为要匹配必须数量一致,而且类型一致(前提是能有common type,否则就回退到别的地方去了)

    template <typename int, double>
    has_common_type<
        std::void_t<std::common_type_t<int, double>>, int, double
    >
      
    // 也就是这个东西,绝对不可能
    has_common_type <
    	void, int , double
    >
    
  • 现在可以确定几个模板里面,特化模板可以确定,就选它了。

可以看看这个https://gist.github.com/jefftrull/ff6083e2e92fdabb62f6

6 概念和约束

  • requires
    • 定义
      • requires子句
      • requires表达式
    • requires内的内容
      • 简单需求:
      • 类型需求:
  • concept: 这里注意有个问题。就是Concept和CRTP冲突了。比方说下面的代码

    template<typename T>
    concept JobImpl = 
      requires(T a, std::shared_ptr<std::string> b, size_t s) {
        { a.GetJobDone() } -> std::convertible_to<size_t>;
        { a.OutputName(b, s) } -> std::convertible_to<size_t>;
      };
      
    template <JobImpl T>
    class Job {
      public:
        template <typename... Args>
        static T CreateJob(Args&&... args) {
          return T(std::forward<Args>(args)...);
        }
      
        void Done() {
          static_cast<T*>(this)->GetJobDone
        }
    };
    

    执行构建的话,会报错:

    note: because 'a.GetJobDone()' would be invalid: member access into incomplete type 'XXX'
    

    什么原因呢?解释起来,原因就是在派生类继承基类时,其类型尚未完整,导致直接约束基类模板参数时概念检查失败。

    那么怎么解决呢?很简单,因为类的定义incomplete,所以把检查滞后就可以了。这里改成检查在函数滞后就可以了

    template<typename T>
    concept JobImpl = 
      requires(T a, std::shared_ptr<std::string> b, size_t s) {
        { a.GetJobDone() } -> std::convertible_to<size_t>;
        { a.OutputName(b, s) } -> std::convertible_to<size_t>;
      };
      
    // 注意这里
    template <typename T>
    class Job {
      public:
        template <typename... Args>
        static T CreateJob(Args&&... args) {
          return T(std::forward<Args>(args)...);
        }
        //注意这里
        void Done() requires JobImpl<T> {
          static_cast<T*>(this)->GetJobDone
        }
    };
    

7 模式和习语

  • MIXIN Inheritence

    template<typename T>
    struct MyT : T
    {
      ...
    };
      
    MyT<int> int_my_t;
    
  • CRTP

    template <typename T>
    class Amount
    {
    public:
        double getValue() const
        {
            return static_cast<T const&>(*this).realGetValue();
        }
    };
      
    class MyAmount : Amount<MyAmount> {
      public:
      int realGetValue() {
        return 100;
      }
    }
    
    • 关于CRTP和Mixin Inherience有两个经典的问题,即:
      • 为什么CRTP传递进去的派生类是不完整的,它可以被派生类继承呢?简单解释就是C++对于模板的实例化是分阶段的,它只需要知道内存布局,而函数的实例化是在真正调用的位置才触发的。参考上面CRTP的例子来说,它的实例化流程如下,因此CRTP成立。但是如果Amount里面需要存储传递进去的T,它需要知道内存布局的时候,就不行了
        1. Amount将MyAmount作为模板参数的时候,它并不需要知道MyAmount的内存布局,因为它不是继承,只是一个类型,它只有函数,不需要存储MyAmount的内存。
        2. MyAmount提供了Amount函数实现里面的realGetValue。
        3. Amount拿到了MyAmount的realGetValue函数,其getValue完整了。
        4. MyAmount此时继承Amount的时候,它知道自己的内存布局,也拿到了Amount包裹之后的函数
      • 为什么Mixin Inherience的派生类,就必须是完整定义的呢?因为MyT继承自T,它必须知道T的内存布局,所以T必须是一个完整定义的类型
  • 类型擦除

  • 标记分派

  • 表达式模板

8 范围和算法

结尾

唉,尴尬

狗头的赞赏码.jpg