news 2025/12/28 6:53:05

CppCon 2024 学习: Design Patterns The Most Common Misconceptions (2 of N)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppCon 2024 学习: Design Patterns The Most Common Misconceptions (2 of N)

设计模式与虚函数

讨论集中在设计模式虚函数以及如何优化继承层次结构的开销。以下是对CRTP(Curiously Recurring Template Pattern)和std::variant的详细分析。

继承层次结构的开销

在面向对象编程中,继承层次结构的使用通常会引入虚函数,从而产生运行时多态性的开销。主要有以下几个问题:

  1. 动态调度的开销:每次调用虚函数时,程序必须查找正确的函数实现,这需要通过虚拟表(vtable)进行。这种动态查找会增加调用的开销。
  2. 内存使用的增加:每个具有虚函数的对象都需要存储指向虚拟表的指针,这会增加内存开销。
    为了优化这些问题,有时需要使用设计模式,如CRTP和**std::variant**。

CRTP 之前先了解这个

代码分析与输出解析

我们来看这两段代码和它们的输出,详细分析背后的原理。

第一段代码
#include<iostream>classBase{public://没有多态静态this类型voidprint1(){std::cout<<typeid(*this).name()<<std::endl;}};classDriver:publicBase{public:voidprint2(){std::cout<<typeid(*this).name()<<std::endl;}};intmain(){Driver d;d.print1();d.print2();}
输出:
4Base 6Driver

输出解释

在这段代码中,我们有一个Base类和一个继承自它的Driver类。

  1. d.print1()输出:
    • 这里,print1方法是Base类中的普通成员函数,没有使用虚函数机制。typeid(*this)的作用是获取当前对象的类型。
    • 由于print1是在Base类中调用的,this实际上是指向一个Driver类型的对象(因为dDriver类型的实例),但由于没有虚函数机制,typeid(*this)会返回Base类型的静态类型。
    • 这里的输出是4Base,数字4是编译器生成的Base类型的名字长度(具体数值依赖编译器)。
  2. d.print2()输出:
    • print2Driver类的成员函数,因此在print2中,this的类型已经明确是Driver类型。
    • 由于print2是在Driver类中调用的,typeid(*this)返回的是Driver类型的静态信息。
    • 这里的输出是6Driver,数字6是编译器生成的Driver类型的名字长度(具体数值依赖编译器)。

第二段代码

#include<iostream>classBase{public://多态动态类型voidprint1(){std::cout<<typeid(*this).name()<<std::endl;}virtualvoidprint3(){std::cout<<typeid(*this).name()<<std::endl;}};classDriver:publicBase{public:voidprint2(){std::cout<<typeid(*this).name()<<std::endl;}};intmain(){Driver d;d.print1();d.print2();d.print3();}
输出:
6Driver 6Driver 6Driver

输出解释

  1. d.print1()输出:
    • 这次print1仍然是Base类中的方法,但与第一段代码的不同之处在于Base类的print1方法是一个非虚方法。
    • 由于print1方法是非虚函数,在调用print1时,typeid(*this)依然会返回当前对象的动态类型,即Driver类型,因为d是一个Driver对象。
    • 所以,d.print1()输出的是6Driver
  2. d.print2()输出:
    • print2方法是Driver类中的方法,因此typeid(*this)会返回Driver类型。
    • 这里输出是6Driver,与print1相同。
  3. d.print3()输出:
    • 由于print3虚函数,它将触发动态类型识别,即使它是Base类中的方法。
    • 通过虚函数机制,typeid(*this)会根据运行时类型确定当前对象的实际类型。在这个例子中,d的实际类型是Driver,所以typeid(*this)返回的也是Driver类型。
    • 结果是6Driver

总结:

  • 第一段代码:
    • print1调用时,Base类的print1方法没有虚函数机制,因此它返回的是静态类型Base
    • print2调用时,Driver类的print2方法返回的是静态类型Driver
  • 第二段代码:
    • print1方法仍然是Base类的方法,但由于没有虚函数机制,print1返回的是动态类型Driver,因为dDriver类型。
    • print2print3都返回Driver类型,因为它们涉及到派生类Driver中的成员,且print3是虚函数,因此使用了动态绑定。

关键点:

  • 静态类型与动态类型
    • 没有虚函数时,typeid(*this)返回的是静态类型,即编译时确定的类型。
    • 使用虚函数时,typeid(*this)返回的是动态类型,即运行时确定的实际对象类型。
  • 虚函数的影响
    • 使用虚函数时,C++ 会进行动态类型识别(RTTI),即便函数在基类中定义,派生类的类型信息也能被正确识别。

CRTP - Curiously Recurring Template Pattern(奇异递归模板模式)

CRTP是一种通过模板实现静态多态的设计模式,目的是避免虚函数和动态调度的开销。该模式的基本思想是一个类模板通过继承自己的模板实例来达到静态多态化。CRTP可以在编译时解决多态问题,而不需要依赖虚函数。
例如,下面是一个使用CRTP的C++示例:

template<typenameDerived>classBase{public:voidfoo(){// 静态调用派生类的成员函数static_cast<Derived*>(this)->fooImpl();}};classDerived:publicBase<Derived>{public:voidfooImpl(){std::cout<<"Derived fooImpl called!"<<std::endl;}};

在这个例子中:

  • Base类是一个模板类,它接受派生类Derived作为模板参数。
  • Derived类继承自Base<Derived>,从而让Base类能够调用派生类的fooImpl函数。
  • foo()在编译时解析调用,而不是在运行时动态查找。
    CRTP的优点
  • 零开销抽象:CRTP避免了虚函数的开销,所有的调用都在编译时解析。
  • 没有虚拟表:由于没有运行时多态性,CRTP模式下的对象不需要存储指向虚拟表的指针,从而减少了内存开销。
    缺点
  • 代码复杂性:对于不熟悉该模式的开发者来说,代码可能显得较为复杂,理解起来较难。

std::variant

std::variant是C++17引入的一个类型安全的联合体,用于在同一对象中存储多个不同类型的数据。与传统的继承层次结构不同,std::variant避免了虚函数和继承的开销,它可以用来替代多态场景中的一些用法。
例如,下面是一个使用std::variant的简单示例:

#include<variant>#include<iostream>structA{voidprint(){std::cout<<"A"<<std::endl;}};structB{voidprint(){std::cout<<"B"<<std::endl;}};usingMyVariant=std::variant<A,B>;voidprint_variant(constMyVariant&v){std::visit([](auto&&arg){arg.print();},v);}intmain(){MyVariant v=A{};print_variant(v);// prints "A"v=B{};print_variant(v);// prints "B"}

在这个例子中:

  • MyVariant是一个std::variant类型,它可以存储AB类型的对象。
  • print_variant使用std::visit来访问variant中存储的对象,并调用相应的print()方法。
    std::variant的优点
  • 避免虚函数开销std::variant不会使用虚函数,它通过类型安全的方式进行操作。
  • 类型安全:每次只能持有一种类型,并且你可以通过std::visit安全地访问它。
    缺点
  • 灵活性不足:与继承层次结构相比,std::variant在某些情况下缺乏扩展性,特别是在需要多种行为变体时。
  • 代码复杂性:使用std::visit可能会使代码变得冗长,特别是在处理多种变体类型时。

总结

  1. 继承层次结构在传统的面向对象设计中使用虚函数来实现多态性,但这会带来运行时开销(动态调度)和内存开销(vtable)。
  2. CRTP通过静态多态性消除了虚函数的开销,减少了运行时开销,但会增加代码的复杂性。
  3. std::variant是一种类型安全的替代方案,它避免了虚函数和继承的开销,适用于一些特定场景,但它的灵活性较低。
    理解:
    CRTP(奇异递归模板模式)是一种通过模板实现静态多态的设计模式,避免了虚函数和动态调度的开销。这种方法通过编译时解析来实现多态。
    std::variant则是C++17引入的一个类型安全的联合体,它能够在同一个对象中存储不同类型的数据,通过std::visit提供多态行为,避免了继承层次结构中的虚函数和动态调度开销。

CRTP(Curiously Recurring Template Pattern)详解

Curiously Recurring Template Pattern(奇异递归模板模式,简称 CRTP)是一种静态多态的实现方法,它通过模板机制来消除继承层次结构中的虚函数开销。在 C++ 中,CRTP 使得派生类通过继承模板基类来进行多态操作,而不是依赖运行时的虚函数调用,从而提高性能。
下面我们逐步分析 CRTP 在 C++ 中的实现过程。

CRTP 示例代码分析

  1. 基本的类模板与继承
template<typenameDerived>classAnimal{// 基类};classSheep:publicAnimal<Sheep>{// 派生类};

在这段代码中:

  • Animal是一个模板类,它的模板参数是Derived(将被具体化为Sheep)。
  • Sheep类继承自Animal<Sheep>,这就是 CRTP 的核心:类Sheep通过模板继承类Animal<Sheep>来实现静态多态。
  1. 构造函数保护
template<typenameDerived>classAnimal{private:Animal()=default;// 防止错误的派生类实例化~Animal()=default;// 防止错误的派生类析构};classSheep:publicAnimal<Sheep>{// 可以继承 Animal<Sheep> 类,但无法直接构造 Animal 类};

在这段代码中,Animal类的构造函数和析构函数是私有的,不能直接在外部创建Animal类的实例。这样做的目的是防止直接创建基类对象,从而确保只有派生类才能被实例化。
3.使用friend关键字进行授权

template<typenameDerived>classAnimal{private:Animal()=default;~Animal()=default;friendDerived;// 让派生类可以访问私有成员};

通过friend Derived;,我们授予Derived(即Sheep)类访问Animal基类的私有成员的权限,这样可以控制访问权限。
4.定义成员函数

template<typenameDerived>classAnimal{public:voidmake_sound()const{static_cast<Derivedconst&>(*this).make_sound();// 通过静态转换调用派生类的 make_sound 函数}};classSheep:publicAnimal<Sheep>{public:voidmake_sound()const{std::cout<<"baa";}};

在这里,Animal类定义了一个成员函数make_sound,但它并没有提供具体实现。相反,它通过static_cast<Derived const&>(*this).make_sound()静态地调用派生类(Sheep)中的make_sound函数。这样做的好处是,make_sound方法在编译时就能够被解析,不会产生运行时的虚函数开销。
5.通用函数模板

template<typenameDerived>voidprint(Animal<Derived>const&animal){// ...}

这个模板函数print接受任意Animal类的对象(包括Sheep)并可以在不引发虚函数调用的情况下执行操作。

CRTP 的优点

  • 避免虚函数开销:CRTP 通过静态多态消除了虚函数的运行时开销。所有的函数调用都在编译时解析,而不是运行时。
  • 提高性能:由于没有虚函数表(vtable),CRTP 可以减少内存使用并避免额外的指针解引用,这使得它比传统的面向对象多态(通过虚函数)更加高效。
  • 代码复用:可以通过Animal类实现许多不同动物的行为,而不需要为每个派生类都定义虚函数。

CRTP 的缺点

  • 增加代码复杂性:CRTP 使得代码结构变得更加复杂,尤其是对于不熟悉模板编程的开发者来说,理解起来可能会有些困难。
  • 无法动态扩展:CRTP 的多态性是静态的,无法像传统的继承层次结构那样动态扩展。如果想要添加新的行为,可能需要修改模板的代码,而不如虚函数那样灵活。

数学公式与理解

CRTP 通过静态多态的实现方式取代了运行时多态。这种方式通过模板机制实现了编译时决策,从而避免了传统 OOP 中虚函数带来的开销。具体来说,类Animal是一个模板类,它通过接受具体的派生类作为模板参数,从而实现了类似虚函数的行为。通过static_castAnimal类中的make_sound函数可以调用派生类(如Sheep)中的同名函数,所有这些操作都发生在编译时,而非运行时。

  • 编译时,Animal<Derived>中的make_sound()方法和Derived类的make_sound()方法通过模板机制被确定为一个明确的调用路径。
  • 通过静态多态性实现的调用在编译时确定,避免了虚函数的动态查找过程。
    这个模式的关键在于静态类型推导,所有的多态行为在编译时就已经确定,不需要依赖于运行时的动态类型识别(如虚函数表)。

总结

CRTP(Curiously Recurring Template Pattern)是一种通过模板实现的静态多态模式,它通过让派生类作为模板参数传递给基类,避免了虚函数的开销。这种方式具有较高的性能和较低的内存开销,特别适用于需要高效、低开销的多态实现。但它也带来了代码复杂性和灵活性不足的挑战,因此需要在具体场景中权衡使用。

CRTP(Curiously Recurring Template Pattern)的局限性

尽管CRTP(Curiously Recurring Template Pattern)提供了许多优点,如消除虚函数的开销、提高性能等,但它也有一些局限性。以下是两个主要的局限性:

1. 限制:没有公共基类

在 CRTP 中,每个派生类都需要继承自基类模板,但由于模板类型参数的不同,所有派生类的基类并不共享同一个公共基类。
例如,考虑以下代码:

template<typenameT>classAnimal{// ...};classSheep:publicAnimal<Sheep>{// ...};classDog:publicAnimal<Dog>{// ...};classCat:publicAnimal<Cat>{// ...};

在这个例子中:

  • Sheep类继承自Animal<Sheep>
  • Dog类继承自Animal<Dog>
  • Cat类继承自Animal<Cat>
    虽然它们都继承自模板类Animal,但是每个类的基类模板类型参数是不同的。这就意味着SheepDogCat并没有一个共享的公共基类。它们之间没有通用的接口或继承关系,这在一些需要统一接口或通用基类的情况下会带来问题。
问题:
  • 如果你希望所有动物类(如SheepDogCat)有一个公共基类,这种方式就不适用了,因为 CRTP 的设计原则是每个派生类都继承自一个以派生类本身为模板参数的基类。
  • 这使得在处理需要统一操作或者通用接口的场景时,CRTP 模式显得不太合适。

2. 限制:一切都是模板

CRTP 的另一个限制是它会使得与 CRTP 相关的所有代码都变成模板代码。换句话说,任何涉及到 CRTP 的函数或者类都必须是模板,这会导致一些负面影响,特别是编译时间增加。
例如:

template<typenameDerived>classAnimal{// ...};classSheep:publicAnimal<Sheep>{// ...};template<typenameDerived>voidprint(Animal<Derived>const&animal){// ...}

在这个例子中,print函数是一个模板函数,它必须是模板函数才能处理所有类型的Animal(例如,SheepDog等)。因此,任何涉及 CRTP 的代码都需要变成模板,甚至包括所有与 CRTP 相关的函数和类。

问题:
  • 模板的传播:由于print函数必须是模板函数,它会导致所有与 CRTP 相关的函数、类和接口都必须是模板。比如,如果你需要在多个地方使用Animal或者Sheep类型,就必须传递模板类型参数。这种模式可能会让代码变得冗长且不易维护。
  • 增加编译时间:由于模板在编译时需要进行实例化,涉及到 CRTP 的代码会导致编译时间的增加。编译器需要处理大量模板实例化,从而增加了编译时间。
  • 代码复杂性:模板代码本身就很复杂,而 CRTP 使得一切都变成模板,进一步增加了代码的复杂度,尤其是对于不熟悉模板编程的开发者。

总结:CRTP 的局限性

  1. 没有公共基类:CRTP 无法提供一个通用的基类或接口,因此在需要统一操作的场景下无法使用 CRTP。每个派生类都需要单独继承并传递模板参数,导致缺乏统一的继承结构。
  2. 一切都是模板:CRTP 导致所有与之相关的代码(包括函数、类等)都必须是模板,这不仅增加了代码的复杂性,还会显著提高编译时间。特别是在大型项目中,这种问题可能会变得更加明显。

理解

1. 限制:没有公共基类
由于 CRTP 的设计方式,派生类必须传递自己作为模板参数,因此每个派生类的基类都是独立的,没有公共基类。这意味着,如果你需要一个统一的接口或者需要共享的基类,那么 CRTP 将无法满足这一需求。
2. 限制:一切都是模板
CRTP 的模式会使得所有涉及的类和函数都变成模板,这导致了代码的复杂性增加。所有触及 CRTP 的地方都必须是模板,这样不仅增加了代码的冗长度,还可能导致编译时间的显著增长。

CRTP 增加功能示例

在这个例子中,我们通过CRTP(Curiously Recurring Template Pattern)模式来增加功能,使得基类NumericalFunctions可以提供一个scale函数,这个函数会通过静态多态性调用派生类的具体方法来执行操作。

代码分析

template<typenameDerived>structNumericalFunctions{voidscale(doublemultiplicator){Derived&underlying=static_cast<Derived&>(*this);underlying.setValue(underlying.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctions<Sensitivity>{doublegetValue()const{returnvalue;}voidsetValue(doublev){value=v;}doublevalue;};intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());}

步骤解析

  1. NumericalFunctions类模板
    NumericalFunctions是一个模板类,它接受一个派生类Derived作为模板参数。该类定义了一个成员函数scale,这个函数接受一个乘数并调整派生类的值:
    voidscale(doublemultiplicator){Derived&underlying=static_cast<Derived&>(*this);underlying.setValue(underlying.getValue()*multiplicator);}
    • scale函数:该函数通过static_cast<Derived&>(*this)将当前对象(基类对象)转换为派生类对象,这样就可以直接访问派生类中的getValuesetValue函数。scale函数会修改派生类中的值,将其乘以给定的乘数multiplicator
    • static_cast:这是关键部分,它利用了 CRTP 的静态多态性,通过静态转换将基类指针转换为派生类类型,以便访问派生类的方法。
  2. Sensitivity
    Sensitivity类继承自NumericalFunctions<Sensitivity>,并提供了getValuesetValue函数:
    structSensitivity:publicNumericalFunctions<Sensitivity>{doublegetValue()const{returnvalue;}voidsetValue(doublev){value=v;}doublevalue;};
    • getValuesetValue:这两个成员函数是Sensitivity类特有的,分别用于获取和设置value属性。scale函数会使用这些函数来更新value的值。
  3. main函数
    main函数中,我们创建了一个Sensitivity类型的对象s,并使用scale函数来修改其value的值:
    Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());
    • Sensitivity s{ 1.2 };:创建一个Sensitivity对象s,并将其value初始化为1.2
    • s.scale(2.0);:调用scale函数,将value乘以2.0,即1.2 * 2.0 = 2.4
    • 最后,输出s.getValue(),显示2.4

CRTP 增加功能的工作原理

  • 静态多态性:通过 CRTP,基类NumericalFunctions不需要知道具体的派生类是什么,它只需要通过模板参数Derived来实现功能。scale函数在编译时就能够知道如何调用派生类的getValuesetValue函数,这使得 CRTP 能够在不使用虚函数的情况下提供多态行为。
  • 模板特化NumericalFunctions<Sensitivity>作为模板被具体化为Sensitivity类型,这样Sensitivity类可以继承并使用NumericalFunctions中的函数(如scale)。此过程是通过模板机制在编译时完成的。
  • 没有虚函数开销:由于是静态多态(通过模板),整个过程没有运行时虚函数的开销,所有的函数调用都在编译时解析。这是 CRTP 的一个重要优势:避免了传统继承中虚函数调用的动态开销。

理解

在这个示例中,我们使用了CRTP来为派生类增加功能。基类NumericalFunctions提供了一个scale函数,可以通过静态多态性来调用派生类的成员函数getValuesetValue,从而修改派生类的状态。

#include<iostream>// 用于输出#include<format>// 用于格式化输出 (需要 C++20 或更高版本)structNumericalFunctions{// `scale` 函数定义:通过 `this` 指针操作当前对象,修改它的值voidscale(thisauto&&self,doublemultiplicator){// 将当前对象的值乘以 multiplicator 并设置回对象self.setValue(self.getValue()*multiplicator);}};// 定义 Sensitivity 结构体,继承自 NumericalFunctionsstructSensitivity:publicNumericalFunctions{// 获取当前对象的值doublegetValue()const{returnvalue;}// 设置当前对象的值voidsetValue(doublev){value=v;}// 定义 value 成员变量,存储值doublevalue;};intmain(){// 创建 Sensitivity 类型对象,并初始化 value 为 1.2Sensitivity s{1.2};// 调用 scale 函数,传入 2.0 作为倍数,更新 value 的值s.scale(2.0);// 打印更新后的 value 值std::println(std::cout,"s.getValue() = {}",s.getValue());}

这段 C++23 代码展示了如何通过使用this auto&& self来实现一种新的编程技巧,即“显式对象参数”(或称为“推导 this”)。

代码解析:

  1. scale函数
    • scale函数是NumericalFunctions类的一部分。它采用一个this参数(即auto&& self),这个参数代表调用该函数的对象。
    • 函数体内,self.setValue(self.getValue() * multiplicator)通过调用self的成员函数来更新对象的值。
  2. Sensitivity结构体
    • Sensitivity继承自NumericalFunctions,并实现了getValue()setValue()方法,用来访问和修改成员变量value
    • main()函数中创建了一个Sensitivity类型的对象s,并初始化了value为 1.2。
  3. main函数
    • main()中的s.scale(2.0)调用了scale函数,传入 2.0 作为multiplicator,从而将s.value的值更新为 2.4。
    • 最后通过std::println输出s.getValue(),打印出更新后的值。

数学公式解析:

  • scale函数中,self.setValue(self.getValue() * multiplicator)相当于执行了如下数学操作:
    new value=old value×multiplicator \text{new value} = \text{old value} \times \text{multiplicator}new value=old value×multiplicator
    具体地,getValue()获取当前对象的值,multiplicator是传入的倍数,结果被赋值回value通过setValue()方法。

C++23 特性:

  • this auto&& self是 C++23 中的一个新特性,用于让成员函数的this指针自动推导类型。这意味着不再需要明确地声明this指针的类型,编译器能够推导出实际类型。
    这种写法带来了更简洁和灵活的代码设计,尤其是在链式调用或某些模板编程场景下,非常有用。

Curiously Recurring Template Pattern (CRTP) – 未来及其应用

Curiously Recurring Template Pattern (CRTP)是 C++ 中的一种强大技巧,它通过让一个类模板继承自另一个以派生类作为模板参数的类模板,提供了静态多态性。这种模式消除了虚函数调用的开销,通过让编译器在编译时确定具体类型,避免了运行时的动态分发。

代码中的问题分析

我们来看下你提供的代码:

classAnimal{public:template<typenameSelf>voidmake_sound(thisSelfconst&self){self.make_sound_impl();}};classSheep:publicAnimal{public:voidmake_sound_impl()const{std::cout<<"baa";}};intmain(){Sheep sheep;Animal&animal=sheep;sheep.make_sound();// works fineanimal.make_sound();// Compilation error!}

为什么会出现编译错误?

  • 模板推导问题:关键问题在于make_sound是一个模板方法,它期望调用它的类能够作为 “Self” 类型。在你调用animal.make_sound()时,animalAnimal&类型,而编译器无法从Animal&中推导出派生类(例如Sheep)的类型。
  • 当你调用animal.make_sound()时,animal的类型是Animal&,编译器无法知道Self应该是什么类型,因此无法解析make_sound_impl()方法。

CRTP 在日常代码中的作用

Curiously Recurring Template Pattern (CRTP)在日常编程中可以带来很多优势,尤其在以下几种情况下:

  1. 增加功能性
    • CRTP 允许你为多个类添加通用功能,而无需重复编写相同的代码。
    • 比如,你可以在Animal类中定义make_sound方法,任何实现了make_sound_impl方法的类(比如Sheep)都可以复用这一方法。
  2. 静态接口(编译时多态性)
    • CRTP 可以用来创建静态接口,编译器确保派生类实现了特定的功能。
    • 与运行时多态性不同,CRTP 使得接口和功能的检查发生在编译时,从而提高了性能和类型安全。
  3. 性能优化
    • 由于没有虚函数调用,CRTP 在某些场景下比传统的继承模型要更高效,尤其是基类功能是通用的,可以在多个派生类之间复用的情况下。
  4. 更细粒度的继承控制
    • CRTP 给了你更大的灵活性,可以自定义每个派生类的行为,而无需依赖虚函数。

CRTP 的两种形式

  1. 简单 CRTP:派生类直接使用基类模板。
  2. 私有构造函数 CRTP:基类模板通过私有构造函数强制派生类使用,防止传入错误的类类型。

改进代码示例

为了解决错误并使代码按预期工作,你可以使用动态多态性显式类型转换来确保派生类的类型被传递给基类。以下是一个使用动态类型转换的示例:

#include<iostream>classAnimal{public:template<typenameSelf>voidmake_sound(thisSelfconst&self){self.make_sound_impl();}};classSheep:publicAnimal{public:voidmake_sound_impl()const{std::cout<<"baa";}};intmain(){Sheep sheep;Animal&animal=sheep;sheep.make_sound();// works fine// animal.make_sound(); // 仍然报错,因为 Animal& 并不知道 Sheep 是什么类型// 修复:显式转换为派生类类型dynamic_cast<Sheep&>(animal).make_sound();// 现在可以正常工作}

通过显式将animal转换为Sheep&类型,编译器就知道animal实际上是Sheep类型,从而可以调用make_sound_impl()

结论:CRTP 的未来

  • 静态多态性将越来越流行:CRTP 在未来将继续在需要高性能且避免运行时开销的场景中得到广泛应用。
  • 更强大的模板机制:CRTP 可以帮助开发者编写更灵活和高效的模板代码,尤其在编译时就能确定具体类型的情况下。
  • 静态接口的优势:CRTP 能作为虚拟接口的替代品,利用编译时检查来减少运行时的开销。

CRTP 在日常 C++ 编程中的应用

实际上,CRTP 可以让你的代码变得灵活高效。通过正确使用 CRTP,你可以写出模块化、可重用且性能优化的代码。如果你经常需要处理需要类继承的设计,但又不想使用运行时多态性,CRTP 是一个非常值得考虑的模式!

使用 CRTP 添加功能性

在你的示例中,Curiously Recurring Template Pattern (CRTP)被用来实现一个通用的功能:对数据进行缩放(scale),并在派生类中提供具体的数据操作。通过使用 CRTP,基类NumericalFunctions可以在不依赖虚函数的情况下,为派生类提供通用的功能,同时允许派生类定义自己的行为。

代码解析

template<typenameDerived>structNumericalFunctions{voidscale(doublemultiplicator){Derived&underlying=static_cast<Derived&>(*this);underlying.setValue(underlying.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctions<Sensitivity>{doublegetValue()const{returnvalue;}voidsetValue(doublev){value=v;}doublevalue;};intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());}

详细分析

1.NumericalFunctions类模板

NumericalFunctions是一个模板类,它接受一个派生类类型作为模板参数(在这里是Sensitivity)。这个模板类包含了一个scale方法,它的作用是将当前对象的value缩放一个倍数。我们在scale方法中使用了static_cast<Derived&>(*this)将基类指针转为派生类类型,以便调用派生类的具体实现方法(如getValuesetValue)。

  • Derived& underlying = static_cast<Derived&>(*this);
    通过这种方式,NumericalFunctions可以访问Sensitivity类的方法,而不需要在基类中显式定义它们。这是 CRTP 的一个关键特性:基类可以调用派生类的方法,而不需要依赖运行时多态。
  • underlying.setValue(underlying.getValue() * multiplicator);
    该行代码通过派生类的getValue获取当前的值,再通过setValue更新这个值。
2.Sensitivity

Sensitivity类是NumericalFunctions<Sensitivity>的派生类,它实现了getValuesetValue方法,这两个方法用于读取和设置value

  • double getValue() const { return value; }
    该方法返回value,这实际上是我们操作的数据。
  • void setValue(double v) { value = v; }
    该方法设置value,它在scale方法中被调用来更新数据。
3.main函数

main函数中:

Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());
  • 创建了一个Sensitivity类型的对象s,并初始化其value1.2
  • 调用s.scale(2.0),将value缩放为原值的 2 倍,即1.2 * 2 = 2.4
  • 最后,通过s.getValue()输出更新后的值,应该输出2.4

CRTP 的优势

  1. 无虚函数调用:
    CRTP 通过编译时多态性消除了虚函数调用的开销,使得代码在性能上更具优势。在这里,NumericalFunctions提供了scale方法,派生类Sensitivity可以通过编译时类型推导实现特定行为,而无需依赖虚函数机制。
  2. 代码复用:
    NumericalFunctions类提供了通用功能,而具体的getValuesetValue由派生类实现。这样,可以在不同的派生类中重用NumericalFunctions的功能,而不需要重复编写scale方法。
  3. 类型安全:
    由于NumericalFunctions使用了静态类型推导,它可以确保调用的是正确类型的成员方法,并避免了运行时的错误。这种方法在编译时已经得到了类型检查,提高了类型安全性。
  4. 简化代码:
    CRTP 让你能把常见的功能放到基类中,而派生类只需要专注于实现具体的行为。这种结构使得代码更简洁,避免了重复的实现。

总结

通过 CRTP 模式,你可以在 C++ 中实现高效的静态多态性,减少运行时开销,并使得代码更加模块化可复用。在这个例子中,基类NumericalFunctions提供了一个通用的功能,而派生类Sensitivity提供了具体的实现,结合 CRTP 达到了一种高效且灵活的设计方式。

代码解析:使用 C++23 的this关键字和扩展的成员函数

在你提供的这段代码中,主要涉及的是C++23中引入的this关键字的扩展,它允许在成员函数中更加灵活地访问成员。具体来说,this关键字现在不仅限于指向当前对象的指针,还能用于构造成员函数的“通用引用”

代码详细分析

structNumericalFunctions{voidscale(thisauto&&self,doublemultiplicator){self.setValue(self.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctions{doublegetValue()const{returnvalue;}voidsetValue(doublev){value=v;}doublevalue;};intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());}

1.NumericalFunctions结构体

structNumericalFunctions{voidscale(thisauto&&self,doublemultiplicator){self.setValue(self.getValue()*multiplicator);}};
  • NumericalFunctions结构体定义了一个成员函数scale,它接受一个参数self,这是一个“通用引用”,也就是auto&&。在C++23中,this可以作为成员函数的参数来引用当前对象,而不仅仅是指针。self在这里代表当前对象本身,且它是完美转发的(通过auto&&)。
  • self.setValue(self.getValue() * multiplicator);
    • self.getValue()获取当前对象的值。
    • self.setValue()用来设置当前对象的值,getValuesetValue都是派生类(在本例中是Sensitivity类)所定义的方法。
    • self通过传递给scale函数,允许我们在NumericalFunctions类中调用派生类的方法,达到了代码复用的目的。

2.Sensitivity结构体

structSensitivity:publicNumericalFunctions{doublegetValue()const{returnvalue;}voidsetValue(doublev){value=v;}doublevalue;};
  • Sensitivity类继承了NumericalFunctions,并实现了getValuesetValue方法。
  • getValue返回成员变量valuesetValue设置成员变量value的值。value是一个double类型的成员变量。

3.main函数

intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());}
  • main函数中,首先创建了一个Sensitivity对象s,并初始化value1.2
  • 调用s.scale(2.0),将value缩放为原值的 2 倍,即1.2 * 2 = 2.4
  • 最后,通过s.getValue()输出更新后的值,应该输出2.4

4. C++23this关键字的扩展

  • 在 C++23 中,this关键字的用法扩展,允许在成员函数中作为参数来引用当前对象。原来this只是一个指向当前对象的指针,但通过this auto&& self,它被当作一个通用引用(完美转发)传递,这样self可以在函数体内作为一个普通对象使用。
  • 这种扩展的this语法允许我们在基类中定义操作派生类对象的函数,从而实现基类通用方法的功能,同时仍然保持派生类的特定实现(如getValuesetValue)。

5. 输出分析

s.scale(2.0);std::println(std::cout,"s.getValue() = {}",s.getValue());
  1. s.scale(2.0)
    调用NumericalFunctions中的scale方法,传入的multiplicator2.0,此时getValue返回的是1.2,然后将其乘以2.0,结果是2.4。接着,setValuevalue更新为2.4
  2. s.getValue()
    返回value的当前值,即2.4,所以最终输出:
    s.getValue() = 2.4

总结

  1. C++23 中this关键字的扩展:通过this auto&& self,我们可以在成员函数中使用self来表示当前对象,从而在基类中操作派生类的特定行为(如访问派生类的成员函数getValuesetValue)。
  2. 代码复用NumericalFunctions类通过通用的scale方法,让Sensitivity类(及其其他派生类)能够共享这一方法,简化了代码的实现。
  3. 完美转发auto&&作为通用引用,能够完美转发self,无论是左值还是右值,都可以正确地传递。
#include<iostream>// 定义一个通用的数值操作功能类模板,使用 CRTP(Curiously Recurring Template Pattern)// CRTP 的作用是允许派生类通过基类的成员函数来进行操作,从而实现更好的代码复用和类型安全。structNumericalFunctions{// 'scale' 函数可以让派生类对自身的值进行缩放。'this' 指针允许我们操作派生类对象。// 通过 'auto&& self' 来捕获派生类类型,使得代码更加通用。voidscale(thisauto&&self,doublemultiplicator){// 调用派生类对象的 getValue 和 setValue 方法来进行缩放操作。self.setValue(self.getValue()*multiplicator);}voidsetValue(doublev){std::cout<<"base setValue"<<v<<std::endl;}};// Sensitivity 类继承自 NumericalFunctions,表示具体的数值类型。structSensitivity:publicNumericalFunctions{Sensitivity(doublevalue):value(value){}// 获取当前的数值doublegetValue()const{returnvalue;}// 设置新的数值voidsetValue(doublev){value=v;}// 数据成员,用于存储值doublevalue;};intmain(){// 创建一个 Sensitivity 对象,初始值为 1.2Sensitivity s{1.2};// 使用 scale 函数将值缩放为原来的 2 倍s.scale(2.0);// 输出当前的值,期望为 2.4std::cout<<"s.getValue() = "<<s.getValue()<<std::endl;}

输出s.getValue() = 2.4

这段代码展示了静态接口CRTP(Curiously Recurring Template Pattern)的一个典型例子,利用 CRTP 实现了一种静态多态。下面是详细的解析:

代码解析

1.Animal类模板
template<typenameDerived>classAnimal{private:// ... 私有的默认构造函数和析构函数public:voidmake_sound()const{static_cast<Derivedconst&>(*this).make_sound_impl();}};
  • Animal是一个模板类,接收一个类型参数Derived,这是一个子类类型,通常会在继承时被指定。
  • make_sound()函数中,使用static_cast<Derived const&>(*this)将当前对象强制转换为派生类类型 (Derived)。
  • 然后调用派生类的make_sound_impl()方法。这是静态多态的关键,编译器在编译时会根据实际传入的Derived类型来解析具体调用的方法。
    这种方式避免了运行时的虚函数表查找(即避免了运行时多态),通过静态的类型推断实现了“静态接口”的效果。
2.Sheep
classSheep:publicAnimal<Sheep>{public:voidmake_sound_impl()const{std::cout<<"baa";}};
  • Sheep继承了Animal<Sheep>类,并实现了make_sound_impl()方法,这是派生类需要提供的接口实现。
  • make_sound_impl()方法打印出 “baa”,表示羊的叫声。
3.main函数
intmain(){Sheep sheep;Animal<Sheep>&animal=sheep;sheep.make_sound();animal.make_sound();}
  • main函数中,创建了一个Sheep对象sheep
  • 通过Animal<Sheep>& animal = sheep;,我们将Sheep类型的对象赋给了Animal<Sheep>类型的引用animal。注意,animal是一个父类类型引用,但它绑定的是派生类对象,因此仍然能调用Sheep中的make_sound_impl()方法。
  • sheep.make_sound()animal.make_sound()都会调用Sheep中的make_sound_impl()方法,从而打印出 “baa”。

总结

这段代码通过CRTP模式实现了静态多态,具体做法是:

  1. Animal类作为基类模板,接收一个派生类Derived
  2. make_sound()方法调用Derived类的实现,从而避免了运行时虚函数的开销。
  3. Sheep类实现了自己的声音方法,继承自Animal<Sheep>
    通过这种方式,Animal类并不需要知道具体的派生类实现,而是通过模板参数传递信息,使得派生类能够提供自己的实现,同时在编译时决定调用哪个方法,避免了运行时的开销。

数学公式

虽然这段代码并没有涉及具体的数学公式,但你可以按照下面的方式理解它:

  • Animal<Sheep>可以看作是一个通用模板,接受Sheep类型作为参数。
  • make_sound_impl()可以视为一个方法实现的“接口”,其中的static_cast<Derived const&>(*this)在编译时根据类型推断来决定具体调用哪个方法。
    如果需要将其转化为数学形式,类似于下面的表达方式:
    Animal(Sheep)=make_sound()→Sheep::make_sound_impl() \text{Animal}(Sheep) = { \text{make\_sound()} \to \text{Sheep::make\_sound\_impl()} }Animal(Sheep)=make_sound()Sheep::make_sound_impl()
    这种设计方式提供了一种更灵活、更高效的多态机制,同时保证了类型安全和性能优化。

在这段代码中,展示了 C++23 中的一种新方式,通过在成员函数中使用this指针来实现静态接口(Static Interface)。

代码解析

1.Animal类模板
classAnimal{public:template<typenameSelf>voidmake_sound(thisSelfconst&self){self.make_sound_impl();}};
  • 这个Animal类提供了一个成员函数make_sound(),该函数是一个模板函数,它接收一个类型参数Self
  • this Self const& self是一个 C++23 的新特性,意味着该成员函数是通过this指针来访问当前对象的。也就是让Self类型被自动推断为当前对象的类型。
  • 函数体中,self.make_sound_impl()会调用当前对象的make_sound_impl()方法。这是一个静态接口的实现方式,类似于 CRTP 模式,不过这里没有显式地依赖于继承关系,而是通过模板和this指针来实现。
2.Sheep
classSheep:publicAnimal{public:voidmake_sound_impl()const{std::cout<<"baa";}};
  • Sheep类继承自Animal类,并实现了make_sound_impl()方法。该方法打印出 “baa” 表示羊的叫声。
3.main函数
intmain(){Sheep sheep;Animal&animal=sheep;sheep.make_sound();animal.make_sound();}
  • main函数中,创建了一个Sheep类型的对象sheep
  • 然后,将sheep对象赋给Animal类的引用animal,虽然animal是一个基类类型,但它绑定的是Sheep对象。
  • 调用sheep.make_sound()animal.make_sound()都期望调用Sheep类中的make_sound_impl()方法。

编译错误原因

Cannot compile since the ‘Self’ type cannot be deduced to be the dynamic type
  • 问题1:这里的make_sound()使用了模板类型Self,但是由于 C++ 并没有提供直接的方式来推断Self的类型(尤其是当我们使用基类引用时),因此编译器无法正确地推断SelfSheep类型。
  • 问题2:当animal.make_sound()被调用时,animal是一个Animal&类型的引用,它并不知道Sheep的实际类型。编译器无法根据基类引用推断出具体的派生类Sheep类型,因此也无法推断出Self类型。这导致无法编译。

解决方案

为了让编译通过,可以使用虚函数或者明确指定模板类型来解决类型推断问题。以下是两种可能的解决方式:

1.使用虚函数(动态多态)
classAnimal{public:virtualvoidmake_sound()const=0;// 纯虚函数};classSheep:publicAnimal{public:voidmake_sound()constoverride{std::cout<<"baa";}};

通过在Animal类中声明一个纯虚函数make_sound(),然后在Sheep类中实现该函数,我们可以在运行时根据对象的实际类型来调用正确的make_sound()方法。

2.显式传递模板类型
template<typenameSelf>voidmake_sound(Self&self){self.make_sound_impl();}classSheep{public:voidmake_sound_impl()const{std::cout<<"baa";}};intmain(){Sheep sheep;make_sound(sheep);// 显式调用模板函数}

在这种情况下,我们明确指定make_sound()的模板类型,避免了基类和派生类之间的类型推断问题。

数学公式

虽然这段代码没有涉及直接的数学公式,但如果需要理解模板推断的过程,可以用一个类似数学公式的方式表示类型推导:
假设我们有以下的模板函数:
make_sound(Self)=Self→make_sound_impl() \text{make\_sound}(Self) = Self \to \text{make\_sound\_impl()}make_sound(Self)=Selfmake_sound_impl()
在调用时,Self应该被推导为具体的类型Sheep,从而调用make_sound_impl()方法。
不过,由于 C++ 并没有自动推断基类引用类型为派生类的能力,因此编译器无法正确推导出SelfSheep,导致编译错误。

总结

这段代码展示了通过this指针和模板函数来实现静态接口的想法,但由于类型推导的限制,在基类引用上无法成功推断出实际类型,导致编译错误。可以通过虚函数或显式传递模板类型来解决这个问题。

CRTP 的两种形式

1.CRTP 用于静态接口(Static Interface)
  • 功能:这种形式的 CRTP 提供了一个基类,用于一组相关的类型(或类型族)。它定义了一个共同的接口,并通过基类接口进行使用。
  • 设计模式:这种用法引入了抽象,属于设计模式的范畴。通过静态多态,基类不需要知道派生类的实现,派生类通过 CRTP 提供特定的功能。
  • 应该称之为:“静态接口”(Static Interface)。
    这类静态接口 CRTP 的示例代码如下:
template<typenameDerived>classAnimal{public:voidmake_sound()const{static_cast<constDerived*>(this)->make_sound_impl();}};classSheep:publicAnimal<Sheep>{public:voidmake_sound_impl()const{std::cout<<"baa";}};intmain(){Sheep sheep;sheep.make_sound();// "baa"}

在这个例子中,Animal类提供了一个静态接口,而Sheep类提供了具体的实现。make_sound()通过静态多态调用make_sound_impl(),这就是“静态接口”模式。

2.CRTP 用于功能添加(Mixin)
  • 功能:这种形式的 CRTP 主要提供派生类的实现细节,并不定义公共接口。通过 CRTP,基类可以提供一些默认的行为或功能,而派生类只需继承即可。
  • 非设计模式:这种用法没有引入抽象,也不属于设计模式,因为它只是为了复用代码,而没有设计出一个统一的接口。
  • 应该称之为:“Mixin”。
    在这种情况下,Mixin主要是为了给派生类添加功能,而不是为了定义一个公共接口。例如:
template<typenameDerived>classLogging{public:voidlog(conststd::string&message){static_cast<Derived*>(this)->log_impl(message);}};classMyClass:publicLogging<MyClass>{public:voidlog_impl(conststd::string&message){std::cout<<"Log: "<<message<<std::endl;}};intmain(){MyClass obj;obj.log("This is a log message");}

在这个例子中,Logging类并没有定义一个公共接口,而是作为一个MixinMyClass提供了日志功能。派生类只需要提供log_impl的实现即可。

对这两种形式的区分

  • 静态接口(Static Interface):这是一种设计模式,通常用于定义一组相关类型的公共接口,基类通过 CRTP 提供接口,而派生类提供具体实现。它通过静态多态在编译时确定调用的方法。
  • Mixin:这是一个功能性代码复用的方式,基类通过 CRTP 提供默认的实现或行为,但没有引入抽象接口。派生类仅继承该基类并补充特定的实现。

准则

  1. 静态接口:当你打算创建一个静态类型家族,并且希望定义一个公共的接口时,应该使用 “静态接口”(Static Interface)的术语。这种形式是设计模式的一部分,主要用于接口的统一定义。
  2. Mixin:如果你只是打算继承实现细节,并希望复用功能而不需要引入抽象接口,可以使用 “Mixin” 的术语。Mixin 更多的是代码复用而非设计模式。

数学公式类比

可以用数学公式来帮助理解这两种形式的区别:

  • 静态接口(Static Interface)可以表示为:
    Static Interface=Interface Definition,Implemented by Derived Types \text{Static Interface} = { \text{Interface Definition}, \text{Implemented by Derived Types} }Static Interface=Interface Definition,Implemented by Derived Types
    其中,接口定义在基类中,而具体实现由派生类提供,利用静态多态机制进行调用。
  • Mixin可以表示为:
    Mixin=Implementation Details,Inherited by Derived Types \text{Mixin} = { \text{Implementation Details}, \text{Inherited by Derived Types} }Mixin=Implementation Details,Inherited by Derived Types
    在这种形式中,基类提供的是实现细节,派生类继承并补充具体的实现,且没有引入接口层的抽象。

总结

  • “静态接口”(Static Interface)是一种设计模式,它通过 CRTP 定义了类型族的公共接口,依赖于静态多态。
  • “Mixin”是一种功能扩展的方式,它通过 CRTP 为派生类提供实现细节,而不引入抽象接口。
  • 当你想定义一组有共同接口的类型时,使用“静态接口”;当你希望继承实现细节而非接口时,使用“Mixin”
    这种区分有助于减少对 CRTP 术语的歧义,使得代码的意图更加明确。
1.C++20 Concepts

在 C++20 中,Concepts是一种新的机制,允许对模板参数进行约束,确保它们满足特定的条件。这样就可以在编译时进行类型检查,避免出现不匹配的类型。

template<typenameT>conceptAnimal=requires(T animal){animal.make_sound();};
  • 这里定义了一个Animalconcept,它要求类型T必须有一个make_sound()成员函数。具体来说,requires语句表示,类型T必须能够成功地调用animal.make_sound(),这就是对T类型进行约束的方式。
    这种方式的优势在于,通过Concepts可以清晰地表达类型的需求,减少模板类型参数错误的发生。
2.使用 Concept 的print函数
template<Animal T>voidprint(Tconst&animal){animal.make_sound();}
  • print()函数的模板参数T被约束为必须满足Animalconcept,即T类型必须拥有make_sound()方法。只有符合这一要求的类型才能被传入print()函数。
  • print()函数中,animal.make_sound()被调用,前提是传入的类型T确实定义了make_sound()方法。
3.Sheep
classSheep{public:voidmake_sound()const{std::cout<<"baa";}};
  • Sheep类实现了make_sound()方法,这使得Sheep类型符合Animalconcept 的要求。
  • 因此,Sheep可以被传递给print()函数,从而输出"baa"
4.main函数
intmain(){Sheep sheep;print(sheep);}
  • main()函数中,创建了一个Sheep对象,并将其传递给print()函数。由于Sheep类实现了make_sound()方法,因此它符合Animalconcept 的要求,print()函数成功执行,并输出"baa"

主要问题:这与静态接口不同!

  • 静态接口(Static Interfaces)依赖于CRTP,并且要求显式地通过继承来表明类型的“选择”。
    • 通过 CRTP,类型必须显式地继承并实现基类的接口。这是显式的 opt-in(选择加入)方式。
  • Concepts提供了一种隐式的 opt-in(选择加入)方式。任何符合Animalconcept 的类型都可以被传入print()函数,无需显式的继承或类型指定。它可以是任何类型,只要它实现了make_sound()方法。

关键区别

  • 静态接口(Static Interfaces)强调的是类型的显式选择,通过 CRTP 模式要求派生类显式地继承基类,并实现基类中的接口。例如,Animal<Sheep>就显式地要求Sheep类型来提供实现。
  • Concepts允许隐式选择,即任何实现了特定方法的类型都能满足 concept 的要求,不需要显式地声明继承关系。这使得Sheep仅仅通过满足Animalconcept 的要求就能够传入print()函数,而没有显式的父类关系。

数学公式类比

假设我们要定义一个接口或功能的约束:

  • 静态接口(Static Interface)可以表示为:
    Static Interface=Base Class,Derived Classes Explicitly Opt-in \text{Static Interface} = { \text{Base Class}, \text{Derived Classes Explicitly Opt-in} }Static Interface=Base Class,Derived Classes Explicitly Opt-in
    这里,基类作为接口,派生类显式继承并实现它,体现了显式选择
  • Concepts则可以表示为:
    Concepts=Type Constraint,Implicit Opt-in \text{Concepts} = { \text{Type Constraint}, \text{Implicit Opt-in} }Concepts=Type Constraint,Implicit Opt-in
    这里,类型只要满足特定的约束(例如拥有make_sound()方法)就能被接受,体现了隐式选择

总结

  1. 静态接口(Static Interface)通过 CRTP 强制类型显式选择加入,属于一种设计模式,强调接口的抽象和类型家族的构建。
  2. Concepts使得类型的约束变得更为灵活和简洁。它允许任何满足要求的类型都能够加入,而不需要显式的继承或基类的设计。
  3. CRTP更侧重于类型之间的关系和设计模式,适合在需要显式控制继承关系和接口时使用。而Concepts则提供了一种更松散、灵活的方式来约束类型,适用于那些没有强继承关系的场景。
1.AnimalTag标签类
classAnimalTag{};
  • 这里定义了一个空的类AnimalTag,它作为一个标记类(tag class)使用。它本身并不包含任何功能,只是为了作为类型的一部分,帮助区分哪些类型是可以作为 “Animal” 的类型。
  • 这种做法类似于 CRTP 中的 “Tag Dispatching”,通过特定的标签类来标识类型,进而让编译器知道该类型是否符合某些约束。
2.定义AnimalConcept
template<typenameT>conceptAnimal=requires(T animal){animal.make_sound();}&&std::derived_from<T,AnimalTag>;
  • Animal是一个Concept,它用于约束传入的类型T。要满足Animal的要求,类型T必须:
    • 拥有make_sound()方法,这通过requires子句来检查。
    • 继承自AnimalTag(使用std::derived_from<T, AnimalTag>)。这一点确保只有显式声明为动物的类型才会符合该概念。
      这里,std::derived_from<T, AnimalTag>是 C++20 中的一个标准库工具,它检查类型T是否是AnimalTag的派生类。
  • 通过这样的设计,只有显式继承了AnimalTag的类型才能满足Animal概念,因此能够保证类型明确地选择加入Animal的接口。
3.print函数
template<Animal T>voidprint(Tconst&animal){animal.make_sound();}
  • print()函数的模板参数T被约束为必须满足Animalconcept,即T必须有make_sound()方法并且继承自AnimalTag
  • print()函数中,我们调用了animal.make_sound(),因此只有符合Animal概念的类型才会被接受并执行此操作。
4.Sheep
classSheep:publicAnimalTag{public:voidmake_sound()const{std::cout<<"baa";}};
  • Sheep类继承自AnimalTag,并实现了make_sound()方法,这使得它符合Animal概念的要求。
  • 由于Sheep类显式继承了AnimalTag,它符合Animal概念,因此可以被传递给print()函数。
5.main函数
intmain(){Sheep sheep;print(sheep);}
  • main()函数中,创建了一个Sheep对象并将其传递给print()函数。
  • 由于Sheep类继承了AnimalTag并实现了make_sound()方法,它符合Animal概念,因而可以传入print()函数并调用make_sound()方法,输出"baa"

主要要点和总结

  1. 显式选择加入:与 CRTP 的不同之处在于,类型(如Sheep)显式地通过继承AnimalTag来表明自己是一个符合Animal概念的类型。这是一种显式的 opt-in,即明确声明自己符合某种约束。这与 CRTP 中通过继承基类来实现静态接口的方式有所不同。
  2. Animal概念Animal概念要求类型T必须实现make_sound()方法,并且继承自AnimalTag。这保证了只有显式选择加入的类型才能成为Animal,避免了误用或错误类型的传递。
  3. 静态接口实现:通过Concepts,我们可以更加灵活地定义静态接口,而不需要显式的基类继承关系。这使得代码更加简洁和易于理解,且避免了冗长的继承链。
  4. 与 CRTP 的区别:CRTP 是一种依赖于模板的静态多态,而Concepts使得我们可以灵活地定义类型约束,简化了类型检查的过程。Concepts不需要强制的继承关系,而是基于接口方法来判断类型是否符合要求。

数学公式类比

可以使用数学公式来帮助理解这种显式选择加入的方式:

  • 静态接口(Static Interface):通过 CRTP,类型明确地继承基类,并实现接口:
    Static Interface (CRTP)=Base Class,Derived Classes Explicitly Opt-in \text{Static Interface (CRTP)} = { \text{Base Class}, \text{Derived Classes Explicitly Opt-in} }Static Interface (CRTP)=Base Class,Derived Classes Explicitly Opt-in
  • Concepts:通过Concepts,类型只需满足约束条件,而不需要显式继承关系:
    Static Interface (Concepts)=Concept,Implicit or Explicit Opt-in via Constraints \text{Static Interface (Concepts)} = { \text{Concept}, \text{Implicit or Explicit Opt-in via Constraints} }Static Interface (Concepts)=Concept,Implicit or Explicit Opt-in via Constraints

结论

通过Concepts,C++20 提供了一种新的方式来实现静态接口,避免了传统的 CRTP 所带来的冗长继承链和强制的类型关系。通过Concepts,类型只需要符合约束条件(如实现make_sound()和继承AnimalTag)即可,简化了类型的选择过程。这种方法避免了强制的继承,并提供了更灵活、简洁的接口设计方式。

主要准则

1.“Static Interface” 用于表示静态类型族的创建
  • 解释:当我们打算创建一组相关的类型,并且希望它们共享一个公共接口时,应该使用“Static Interface”这个术语。这种方式通常通过CRTP(Curiously Recurring Template Pattern)实现。
  • 设计模式Static Interface是一种设计模式,通过静态多态实现类型家族的统一接口。在这种模式下,基类定义了接口,而具体的实现则由派生类提供。
  • 数学公式类比
    Static Interface=Base Class with Common Interface,Derived Classes Implementing the Interface \text{Static Interface} = { \text{Base Class with Common Interface}, \text{Derived Classes Implementing the Interface} }Static Interface=Base Class with Common Interface,Derived Classes Implementing the Interface
    这意味着Static Interface强调了类型族中基类的作用,即基类提供了接口,而派生类提供具体的实现。
2.Explicit Object Parameters 不是 Static Interface 的替代方案
  • 解释Explicit Object Parameters(显式对象参数)指的是在函数中显式传递对象作为参数。这通常是实现某些功能的一种方式,但它并不能替代Static Interface的设计模式。
  • 实现细节Static Interface是一个更高层次的设计模式,它通过模板和继承机制在编译时确保类型的多态性。显式对象参数是实现某些功能的手段,不能实现像Static Interface那样的静态接口设计。
3.“Mixin” 用于表示继承实现细节
  • 解释Mixin是一种设计方式,旨在通过继承实现细节来增强派生类的功能。Mixin 不提供接口,而是提供具体的实现方法和行为。这种方法主要用于代码重用和功能扩展,而不需要定义明确的接口。
  • 实现细节Mixin本质上是一种实现细节,通常用于提供辅助功能或扩展已有类的功能,而不是为了定义类型之间的关系。Mixin 可以通过 CRTP 或其他方式来实现,但它并不强制要求类型显式实现某个公共接口。
  • 数学公式类比
    Mixin=Base Class with Implementation Details,Derived Classes Inheriting Behavior \text{Mixin} = { \text{Base Class with Implementation Details}, \text{Derived Classes Inheriting Behavior} }Mixin=Base Class with Implementation Details,Derived Classes Inheriting Behavior
    Mixin强调了类型之间通过继承共享实现细节,而不是接口。
4.Mixin 不是设计模式,而是实现细节
  • 解释Mixin是一种代码复用机制,通常被认为是实现细节而非设计模式。它通过将特定的实现行为提取到基类中,使得多个派生类能够共享这些行为,而无需重新实现。
  • 显式对象参数作为替代方案Explicit Object Parameters可以用来代替 Mixin,因为它允许在函数调用时明确传递对象参数,从而避免继承的复杂性。显式传递对象可以在某些情况下取代 Mixin 的设计,尤其是在不需要复杂继承体系时。
  • 数学公式类比
    Explicit Object Parameters=Function with Object Passed as Parameter,No Inheritance Involved \text{Explicit Object Parameters} = { \text{Function with Object Passed as Parameter}, \text{No Inheritance Involved} }Explicit Object Parameters=Function with Object Passed as Parameter,No Inheritance Involved
    这表明,显式对象参数只是将对象作为参数传递,避免了继承和类型之间的依赖。

总结

  • “Static Interface”:当我们希望在代码中定义一组相关类型,并且希望它们共享一个公共接口时,应该使用Static Interface。这种方式是一个设计模式,通常通过CRTP来实现,强调基类提供接口,派生类提供实现。
  • “Mixin”:当我们希望通过继承共享实现细节时,使用Mixin。这种方式是一个实现细节,用于代码复用和功能扩展,而不是定义接口。它更关注的是将实现细节通过继承的方式复用,而不是定义明确的接口。
  • 显式对象参数的替代性:在某些情况下,显式对象参数可以替代Mixin。通过直接传递对象,而不是依赖继承,可以实现类似的功能扩展,特别是在不需要复杂继承关系时。

数学公式总结

  • Static Interface强调基类提供接口,派生类提供实现:
    Static Interface=Base Class with Common Interface,Derived Classes Implementing the Interface \text{Static Interface} = { \text{Base Class with Common Interface}, \text{Derived Classes Implementing the Interface} }Static Interface=Base Class with Common Interface,Derived Classes Implementing the Interface
  • Mixin强调基类提供实现细节,派生类共享行为:
    Mixin=Base Class with Implementation Details,Derived Classes Inheriting Behavior \text{Mixin} = { \text{Base Class with Implementation Details}, \text{Derived Classes Inheriting Behavior} }Mixin=Base Class with Implementation Details,Derived Classes Inheriting Behavior
  • 显式对象参数可以用来替代Mixin,避免继承的复杂性:
    Explicit Object Parameters=Function with Object Passed as Parameter,No Inheritance Involved \text{Explicit Object Parameters} = { \text{Function with Object Passed as Parameter}, \text{No Inheritance Involved} }Explicit Object Parameters=Function with Object Passed as Parameter,No Inheritance Involved
    通过这些准则和数学公式,我们能够更清晰地理解Static InterfaceMixin在设计中的不同角色,并能够根据需要选择合适的设计方式。

Mixin是一种常用于代码重用的技术,通常用于将某些功能通过继承的方式引入到类中,而不需要强制要求类继承某个共同的接口或基类。在 C++ 中,Mixin类通常是一个不定义接口的基类,而是提供一些功能性的实现细节。派生类可以选择继承这些实现,添加或覆盖某些功能。

下面是几个Mixin的示例,展示如何通过继承实现细节来增强类的功能。

1.日志功能 Mixin

假设我们需要为多个类添加日志功能,我们可以创建一个日志 Mixin 类,将日志功能的实现放在基类中,派生类只需要继承即可。

#include<iostream>#include<string>// Mixin 类:提供日志功能classLogging{public:voidlog(conststd::string&message)const{std::cout<<"Log: "<<message<<std::endl;}};// 一个具体的类,继承了 Logging MixinclassFileHandler:publicLogging{public:voidopenFile(conststd::string&filename){log("Opening file: "+filename);// 其他文件操作}};// 另一个具体的类,继承了 Logging MixinclassNetworkHandler:publicLogging{public:voidconnect(conststd::string&server){log("Connecting to server: "+server);// 其他网络操作}};intmain(){FileHandler fileHandler;fileHandler.openFile("data.txt");NetworkHandler networkHandler;networkHandler.connect("127.0.0.1");return0;}
解释:
  • Logging是一个Mixin类,它提供了一个log()方法,用于记录日志。
  • FileHandlerNetworkHandler继承了Logging,并分别实现了文件操作和网络操作的功能。
  • FileHandlerNetworkHandler通过继承Logging来复用日志功能,而不需要显式实现日志功能。

2.计时功能 Mixin

我们可以使用Mixin为类添加计时功能,记录操作的耗时。

#include<iostream>#include<chrono>#include<thread>// Mixin 类:提供计时功能classTimer{public:voidstartTimer(){start_time=std::chrono::high_resolution_clock::now();}voidstopTimer(){autoend_time=std::chrono::high_resolution_clock::now();std::chrono::duration<double>duration=end_time-start_time;std::cout<<"Elapsed time: "<<duration.count()<<" seconds"<<std::endl;}private:std::chrono::high_resolution_clock::time_point start_time;};// 具体的类:继承 Timer MixinclassDataProcessor:publicTimer{public:voidprocessData(){startTimer();// 模拟耗时操作std::this_thread::sleep_for(std::chrono::seconds(2));stopTimer();}};intmain(){DataProcessor processor;processor.processData();return0;}
解释:
  • Timer是一个Mixin类,提供了startTimer()stopTimer()方法来记录时间。
  • DataProcessor类继承了Timer并使用startTimer()stopTimer()来衡量数据处理过程的时间。
  • 通过继承TimerDataProcessor无需自己实现计时功能,直接复用了Timer提供的功能。

3.数学运算功能 Mixin

假设我们需要为多个类添加数学计算功能,可以通过Mixin来实现。

#include<iostream>#include<cmath>// Mixin 类:提供数学运算功能classMathOperations{public:doublesquare(doublevalue)const{returnvalue*value;}doublesquareRoot(doublevalue)const{returnstd::sqrt(value);}};// 具体的类:继承 MathOperations MixinclassCalculator:publicMathOperations{public:voidcalculate(){doublex=9;doubleresult1=square(x);doubleresult2=squareRoot(x);std::cout<<"Square of "<<x<<" is: "<<result1<<std::endl;std::cout<<"Square root of "<<x<<" is: "<<result2<<std::endl;}};intmain(){Calculator calc;calc.calculate();return0;}
解释:
  • MathOperations是一个Mixin类,提供了square()squareRoot()方法来执行数学运算。
  • Calculator类继承了MathOperations,并使用其提供的数学运算方法来进行计算。
  • Calculator无需自己实现这些数学运算,而是通过继承MathOperations来复用这些功能。

总结

  • Mixin类提供了实现细节(例如日志记录、计时、数学运算等),派生类可以选择继承这些实现,而无需关心基类的实现细节。
  • Mixin 类不定义接口,它只是提供功能的实现,通过继承将这些功能带入派生类。
  • Mixin 是代码重用的一种方式,可以避免重复编写相同的功能代码,并保持类的职责清晰。
    通过上述例子可以看到,Mixin为不同的类提供了复用功能,同时避免了不必要的继承链或接口设计。

文字描述了一个玩具问题(Toy Problem),具体是关于绘制形状的问题。该问题的需求是可扩展的,允许通过新增形状来扩展系统,并且系统的规模较大,代码量达到 1000 万行,开发人员超过 100 人。这种规模的问题常常涉及到如何设计一个灵活、可扩展的系统以应对复杂性和不断变化的需求。

关键需求解析

1.可扩展性(Extensible)
  • 需求:系统需要支持新增形状的功能。也就是说,我们可能会有不同的形状(如圆形、矩形、三角形等),并且未来可能会新增其他形状。
  • 挑战:如何设计一个灵活的系统,在不修改现有代码的情况下,能够轻松地添加新形状。这通常涉及到面向对象的设计模式,如工厂模式策略模式组合模式,或者更高级的抽象技术,如多态类型系统(例如,C++中的std::variant)。
2.10M+ 行代码(10M+ lines of code)
  • 需求:系统的规模非常大,代码量达到了 1000 万行。
  • 挑战:在如此庞大的代码库中维护可扩展性可维护性是一项挑战。通常,系统会采取分层设计和模块化方法,以确保不同部分的代码不相互影响。代码库的规模也意味着要考虑团队协作版本控制的有效管理。
3.100+ 开发人员(100+ developers)
  • 需求:有超过 100 名开发人员共同协作。
  • 挑战:大规模团队协作的挑战包括任务划分代码风格统一代码冲突解决接口设计的一致性等。尤其是在开发过程中,如何确保不同开发人员编写的代码能够很好地协同工作,避免重复实现相同功能。

使用std::variant的思路

std::variant是 C++17 引入的标准库模板,它允许我们定义一个可以存储多种类型的变量。这个特性在设计可扩展的系统时非常有用,尤其是在面临多种可能类型的情况下。比如,在绘制形状的问题中,我们可能有很多不同的形状(如圆形、矩形等),而使用std::variant可以帮助我们将这些形状统一表示。

如何使用std::variant解决问题?

假设我们需要绘制不同类型的形状,我们可以通过std::variant来表示形状的类型。以下是一个简化的示例:

#include<iostream>#include<variant>#include<vector>// 定义不同的形状structCircle{doubleradius;};structRectangle{doublewidth;doubleheight;};// 使用 std::variant 来表示一个形状(可以是 Circle 或 Rectangle)usingShape=std::variant<Circle,Rectangle>;// 绘制函数,根据不同的形状类型绘制voiddraw(constShape&shape){std::visit([](auto&&s){usingT=std::decay_t<decltype(s)>;ifconstexpr(std::is_same_v<T,Circle>){std::cout<<"Drawing a Circle with radius "<<s.radius<<std::endl;}elseifconstexpr(std::is_same_v<T,Rectangle>){std::cout<<"Drawing a Rectangle with width "<<s.width<<" and height "<<s.height<<std::endl;}},shape);}intmain(){// 创建不同的形状Circle circle{5.0};Rectangle rectangle{10.0,20.0};// 将形状存入 std::variantstd::vector<Shape>shapes={circle,rectangle};// 绘制所有形状for(constauto&shape:shapes){draw(shape);}return0;}
解析:
  1. 形状定义:我们定义了两种形状CircleRectangle
  2. std::variant使用:使用std::variant<Circle, Rectangle>来表示一个形状类型,Shape可以是CircleRectangle
  3. std::visitstd::visit用来访问std::variant中的具体类型,并根据类型执行不同的逻辑。在这个例子中,我们根据是Circle还是Rectangle来调用不同的绘制逻辑。
  4. 可扩展性:如果未来我们想增加新的形状(例如Triangle),只需在std::variant中添加新类型,并在std::visit中添加相应的处理逻辑。

数学公式类比

假设我们有一个形状类型,可以表示多种类型的形状。我们可以通过以下方式表示它:
Shape=Circle,Rectangle,Triangle,… \text{Shape} = { \text{Circle}, \text{Rectangle}, \text{Triangle}, \dots }Shape=Circle,Rectangle,Triangle,
在这个表示中,Shape可以是多种形状之一,通过std::variant来表示这种多态关系。

总结
  • 需求分析:在一个庞大、团队协作的系统中,设计一个可扩展的绘制系统非常重要。需要通过抽象和可扩展的方式管理不同类型的形状。
  • std::variant是一种处理多种类型的简洁方式,能够有效地支持不同形状类型的扩展。
  • 可扩展性:通过使用std::variantstd::visit,系统可以轻松地添加新类型,而不需要修改现有代码库中的大量逻辑。
    这种设计方式保证了系统的灵活性和可扩展性,并能够随着需求的变化轻松增加新功能。

展示了一个典型的面向对象(Object-Oriented)解决方案,用于实现不同形状的绘制。解决方案使用了策略模式(Strategy Pattern)多态性来实现灵活的绘制行为。接下来我会逐行解析这段代码及其设计思想。

1.DrawStrategy 类

template<typenameConcreteShape>classDrawStrategy{public:virtual~DrawStrategy()=default;virtualvoiddraw(ConcreteShapeconst&shape)const=0;};
  • 目的DrawStrategy是一个抽象策略类,它定义了一个接口draw(),这个接口将被不同的具体形状类实现。每个具体形状的绘制方式是不同的,因此我们把绘制的行为封装在一个策略类中。
  • 泛型设计DrawStrategy是一个模板类,ConcreteShape是其模板参数。ConcreteShape代表具体的形状类(比如CircleRectangle等),它将会被用来为每种形状提供具体的绘制实现。
  • draw()方法:纯虚函数draw(),所有派生类都必须实现该方法,来定义如何绘制具体的形状。

2.Shape 类

classShape{public:virtual~Shape()=default;virtualvoiddraw()const=0;// ... several other virtual functions};
  • 目的Shape类是一个抽象基类,它定义了所有形状类的共同接口,其他具体形状类(如Circle)将继承自Shape并实现自己的绘制逻辑。
  • draw()方法:纯虚函数,要求每个具体的形状类都提供自己的draw()实现。通过这种方式,可以利用多态性来在运行时决定绘制哪种形状。

3.Circle 类

classCircle:publicShape{public:Circle(doublerad,std::unique_ptr<DrawStrategy<Circle>>&&ds):radius{rad},drawer{std::move(ds)}{}doublegetRadius()const;// ... getCenter(), getRotation(), ...};
  • 继承自ShapeCircleShape的派生类,表示具体的圆形。它必须实现draw()方法(通过DrawStrategy来委托绘制逻辑)。
  • 构造函数Circle类通过构造函数接收半径 (rad) 和一个DrawStrategy<Circle>的独特指针 (std::unique_ptr<DrawStrategy<Circle>>&& ds)。drawer是指向绘制策略的指针,策略类负责提供draw()方法的具体实现。
  • drawer成员drawer成员保存了一个DrawStrategy<Circle>类型的策略对象,它决定了如何绘制圆形。std::move(ds)表示将外部传入的指针转移到Circle对象中,避免不必要的复制。
关键设计思想
  • 策略模式(Strategy Pattern)DrawStrategy类就是典型的策略模式。这个模式允许我们在运行时选择不同的绘制策略,并使得不同形状的绘制行为可以独立变化。比如,圆形可以使用不同的绘制方法(如通过 SVG 绘制、通过 OpenGL 绘制等),而不会影响其他形状的绘制。
  • 依赖注入(Dependency Injection):在构造Circle对象时,我们通过构造函数注入了一个具体的绘制策略。这样,Circle类并不需要直接知道绘制的细节,它只需要依赖于DrawStrategy提供的接口。

4.如何使用此设计模式

在这种设计中,绘制的实现与形状的定义是分离的,这使得代码更加灵活可扩展。如果将来想添加新的绘制策略(比如使用不同的渲染引擎),我们只需要创建新的DrawStrategy类,而不需要修改原有的ShapeCircle类。这种开闭原则(对扩展开放,对修改封闭)是面向对象设计中的一个核心原则。

5.绘制行为的多态性

由于Circle继承自Shape并实现了draw()方法,而draw()方法的实现由DrawStrategy<Circle>提供,我们可以通过以下方式来利用多态性绘制不同的形状:

std::unique_ptr<DrawStrategy<Circle>>circleDrawer=std::make_unique<CircleDrawer>();// CircleDrawer 是具体的绘制策略Circlecircle(10.0,std::move(circleDrawer));circle.draw();// 调用具体的绘制策略来绘制圆形

在上述代码中,circle的绘制行为由CircleDrawer类决定,这个策略类实现了DrawStrategy<Circle>接口。

6.如何扩展

这种设计非常容易扩展。假设我们想增加一个新的形状(例如,Rectangle),只需要:

  1. 创建一个DrawStrategy<Rectangle>类来实现矩形的绘制。
  2. 创建一个Rectangle类,继承自Shape,并在构造时注入一个DrawStrategy<Rectangle>对象。
  3. 使用相应的绘制策略来绘制矩形。
classRectangle:publicShape{public:Rectangle(doublew,doubleh,std::unique_ptr<DrawStrategy<Rectangle>>&&ds):width(w),height(h),drawer(std::move(ds)){}voiddraw()constoverride{drawer->draw(*this);// 使用相应的策略绘制矩形}private:doublewidth,height;std::unique_ptr<DrawStrategy<Rectangle>>drawer;};

数学公式类比

假设每个形状的绘制策略是一个函数映射,我们可以将不同形状的绘制方式表示为:
Shape(x)→DrawStrategy(Shape,x) \text{Shape}(x) \to \text{DrawStrategy}(\text{Shape}, x)Shape(x)DrawStrategy(Shape,x)
这里,xxx代表形状的数据(如圆的半径、矩形的宽高等),DrawStrategy代表每种形状的绘制策略。

总结

  • 面向对象的设计通过策略模式实现了灵活的绘制行为扩展。
  • Shape类作为所有形状的基类,定义了一个统一的接口,而DrawStrategy类则封装了具体的绘制行为。
  • 依赖注入使得每个形状的绘制策略可以独立设置和替换,从而增强了系统的可扩展性。
  • 这种设计易于扩展,新增形状只需创建新的DrawStrategy类,而不需要更改现有的代码。

面向对象解决方案:分析与解析

展示了一个经典的面向对象设计方案,旨在通过策略模式工厂模式解决不同形状绘制的问题,并提供了一个灵活的扩展机制。设计的核心思想是利用多态性依赖注入来确保系统在面对新的形状类型时,可以轻松扩展且无需修改现有代码。
以下是对每个部分的详细解析。

1.DrawStrategy 类

template<typenameConcreteShape>classDrawStrategy{public:virtual~DrawStrategy()=default;virtualvoiddraw(ConcreteShapeconst&shape)const=0;};
  • 目的DrawStrategy是一个抽象策略类,它为每个具体的形状提供绘制接口。它使用模板,使得每种具体的形状(例如CircleSquare)都有自己的绘制策略。
  • 实现:每个具体形状将实现该策略的draw()方法,具体的绘制细节将由DrawStrategy<ConcreteShape>提供。

2.Shape 类

classShape{public:virtual~Shape()=default;virtualvoiddraw()const=0;// ... several other virtual functions};
  • 目的Shape是所有具体形状类的基类。它定义了所有形状类的共同接口,特别是draw()方法,它是纯虚函数,要求派生类提供具体实现。
  • 可扩展性:通过继承Shape类,未来可以轻松地添加新的形状类型,而不需要更改现有的代码。

3.Circle 类

classCircle:publicShape{public:Circle(doublerad,std::unique_ptr<DrawStrategy<Circle>>&&ds):radius{rad},drawer{std::move(ds)}{}doublegetRadius()const;voiddraw()constoverride;// ... several other virtual functionsprivate:doubleradius;std::unique_ptr<DrawStrategy<Circle>>drawer;};
  • 目的Circle类继承自Shape,并提供了一个构造函数,该构造函数接受一个半径和一个DrawStrategy<Circle>类型的策略对象。
  • drawerdrawerDrawStrategy<Circle>类型的指针,它委托给该策略来实现圆形的绘制。通过使用std::unique_ptr,确保了策略对象的所有权由Circle管理。
  • draw()Circle类重写了Shape类的draw()方法,并将绘制的实际逻辑委托给drawer对象。

4.Square 类

classSquare:publicShape{public:Square(doubles,std::unique_ptr<DrawStrategy<Square>>&&ds):side{s},drawer{std::move(ds)}{}doublegetSide()const;voiddraw()constoverride;// ... several other virtual functionsprivate:doubleside;std::unique_ptr<DrawStrategy<Square>>drawer;};
  • 目的Square类与Circle类类似,也继承自Shape并实现了draw()方法。构造函数接受边长side和一个绘制策略对象。
  • 扩展性:如果未来需要增加更多形状,只需要继承Shape类并实现draw()方法即可。

5.ShapesFactory 类

classShapesFactory{public:virtual~ShapesFactory()=default;virtualShapescreate(std::string_view filename)const=0;};
  • 目的ShapesFactory是一个工厂类,负责根据文件中的数据动态创建形状实例。通过该类,可以将形状的创建与实际使用解耦。
  • 功能create()方法读取文件并返回一组形状对象,这样就实现了从外部输入数据到内部对象创建的映射。

6.绘制所有形状的函数

voiddrawAllShapes(Shapesconst&shapes){for(autoconst&s:shapes){s->draw();}}
  • 目的drawAllShapes()函数遍历所有形状对象,并调用它们的draw()方法。由于每个形状都有不同的绘制方式,draw()方法会根据形状的类型进行不同的处理。

7.具体工厂实现

classYourShapesFactory:publicShapesFactory{public:Shapescreate(std::string_view filename)constoverride{Shapes shapes{};std::string shape{};std::ifstream shape_file{filename};while(shape_file>>shape){if(shape=="circle"){doubleradius;shape_file>>radius;shapes.emplace_back(std::make_unique<Circle>(radius,std::make_unique<OpenGLDrawer>()));}elseif(shape=="square"){doubleside;shape_file>>side;shapes.emplace_back(std::make_unique<Square>(side,std::make_unique<OpenGLDrawer>()));}else{break;}}returnshapes;}};
  • 目的YourShapesFactory继承自ShapesFactory,实现了create()方法,根据文件内容动态创建不同类型的形状。该工厂读取一个文件,其中描述了每个形状的类型及其参数(如半径、边长等),然后返回一个形状对象列表。
  • 灵活性:如果以后需要支持更多形状,只需在create()方法中添加相应的判断和创建代码。

8.绘制所有形状并从文件创建

voidcreateAndDrawShapes(ShapesFactoryconst&factory,std::string_view filename){Shapes shapes=factory.create(filename);drawAllShapes(shapes);}
  • 目的createAndDrawShapes()函数通过工厂创建所有形状,并将其传递给drawAllShapes()进行绘制。此函数简化了创建和绘制形状的过程。

9.OpenGLDrawer 类

classOpenGLDrawer:publicDrawStrategy<Circle>,publicDrawStrategy<Square>{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voiddraw(Circleconst&circle)constoverride;voiddraw(Squareconst&square)constoverride;private:// ... Data members (color, texture, transparency, ...)};
  • 目的OpenGLDrawer是一个具体的绘制策略类,它分别为CircleSquare提供绘制实现。这是一个多重继承的例子,OpenGLDrawer同时继承了DrawStrategy<Circle>DrawStrategy<Square>,使得它能够为不同形状提供绘制策略。

总结

  1. 面向对象设计的优点:通过策略模式和工厂模式的结合,系统提供了非常高的灵活性和扩展性。新的形状可以通过继承Shape类,并通过DrawStrategy实现绘制逻辑。
  2. 工厂模式ShapesFactory负责形状对象的创建,这让我们能够通过外部文件动态配置需要绘制的形状类型,而无需在程序中硬编码。
  3. 策略模式DrawStrategy模式让我们能够为不同形状提供不同的绘制策略,保持了高度的可扩展性。例如,新增一个形状(如Triangle)时,只需继承Shape类并实现自己的绘制策略。
  4. 可扩展性:若未来想要添加更多的形状或绘制策略,只需要添加新的类和方法,不会破坏现有的代码结构。
  5. 实际应用:这种设计适合用于需要高灵活性和可扩展性的图形系统中,例如绘图软件、游戏引擎或可视化应用等。

经典面向对象解决方案分析

展示了一个典型的面向对象设计模式,结合了策略模式工厂模式多态性,来实现图形形状的绘制。它的目标是通过可扩展的设计来处理不同类型的形状,并且能够根据需求灵活地增加新的形状类型。以下是对代码各部分的详细分析:

1.drawAllShapes函数

voiddrawAllShapes(Shapesconst&shapes){for(autoconst&s:shapes){s->draw();}}
  • 目的drawAllShapes()函数遍历所有的形状对象并调用它们的draw()方法。由于每个形状都有自己独特的绘制方式,因此draw()方法会根据具体的形状类型调用对应的绘制策略。
  • 多态性:该函数展示了多态性,即不同类型的形状(例如CircleSquareRectangle)都可以通过基类指针 (Shape) 调用draw()方法,实际调用的是具体类型的draw()实现。

2.createAndDrawShapes函数

voidcreateAndDrawShapes(ShapesFactoryconst&factory,std::string_view filename){Shapes shapes=factory.create(filename);drawAllShapes(shapes);}
  • 目的:该函数通过工厂类ShapesFactory从文件中读取形状信息,创建形状对象的集合,并使用drawAllShapes()进行绘制。
  • 解耦:这个函数解耦了形状的创建和绘制,使得可以独立扩展形状类型和绘制方式。例如,如果需要改变形状的创建方式或增加新的绘制策略,可以轻松修改工厂或策略,而不需要修改其他部分的代码。

3.OpenGLDrawer

classOpenGLDrawer:publicDrawStrategy<Circle>,publicDrawStrategy<Square>,publicDrawStrategy<Rectangle>{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voiddraw(Circleconst&circle)constoverride;voiddraw(Squareconst&square)constoverride;voiddraw(Rectangleconst&rectangle)constoverride;private:// ... Data members (color, texture, transparency, ...)};
  • 目的OpenGLDrawer类是具体的绘制策略类,继承了DrawStrategy模板类,并为CircleSquareRectangle提供了不同的绘制实现。
  • 多重继承:通过多重继承,OpenGLDrawer实现了为多个形状提供绘制策略的功能,这是一种将具体实现与抽象绘制行为分离的方式。
  • 数据成员OpenGLDrawer还可以包含一些与绘制相关的额外数据(如颜色、纹理、透明度等),这些可以在绘制时使用。

4.Rectangle

classRectangle:publicShape{public:Rectangle(doublewidth,doubleheight,std::unique_ptr<DrawStrategy<Rectangle>>&&drawer):width_{width},height_{height},drawer_{std::move(drawer)}{}doublewidth()const{returnwidth_;}doubleheight()const{returnheight_;}voiddraw()constoverride{drawer_->draw(*this);}private:doublewidth_;doubleheight_;std::unique_ptr<DrawStrategy<Rectangle>>drawer_;};
  • 目的Rectangle类继承自Shape类,表示一个矩形形状。与CircleSquare类似,它也拥有自己的绘制策略(drawer_)。
  • 绘制策略:通过接受一个DrawStrategy<Rectangle>类型的对象,Rectangle将其绘制行为委托给该对象。draw()方法实际上调用了drawer_对象的draw()方法。
  • 扩展性:如果以后要添加更多的形状类型(例如Triangle),只需要继承Shape类并实现对应的draw()方法,不需要修改现有的绘制逻辑。

5.YourShapesFactory

classYourShapesFactory:publicShapesFactory{public:Shapescreate(std::string_view filename)constoverride{Shapes shapes{};std::string shape{};std::ifstream shape_file{filename};while(shape_file>>shape){if(shape=="circle"){// ... Creating a circle}elseif(shape=="square"){// ... Creating a square}elseif(shape=="rectangle"){doublewidth,height;shape_file>>width>>height;shapes.emplace_back(std::make_unique<Rectangle>(width,height,std::make_unique<OpenGLDrawer>()));}else{break;}}returnshapes;}};
  • 目的YourShapesFactory类继承自ShapesFactory,实现了create()方法。该方法根据输入文件中的形状描述动态创建不同类型的形状对象,并将它们存储在Shapes容器中返回。
  • 灵活性:如果需要支持新类型的形状,只需在create()方法中添加相应的判断并创建新的形状类实例。这种方式使得工厂类高度灵活且易于扩展。

6.main()函数

intmain(){YourShapesFactory factory{};createAndDrawShapes(factory,"shapes.txt");}
  • 目的:在main()函数中,使用YourShapesFactory创建形状对象并将它们绘制出来。shapes.txt文件描述了需要创建的形状类型和它们的属性(如半径、边长等)。
  • 高内聚低耦合:这个设计保持了良好的高内聚低耦合特性。每个模块(如Shape类、Drawer类、Factory类)都有清晰的职责,且模块之间通过接口解耦。

总结与讨论

优点:
  1. 高扩展性:通过使用策略模式和工厂模式,新的形状类型可以在不修改现有代码的情况下轻松地加入到系统中。
  2. 模块化设计:每个类都有清晰的职责,且通过接口和抽象类解耦,提高了系统的可维护性和可重用性。
  3. 灵活性:通过工厂类动态生成形状,并通过策略类管理绘制逻辑,极大地提升了系统的灵活性和可配置性。
缺点:
  1. 代码冗长:虽然面向对象设计提供了很好的扩展性,但同时也带来了较多的冗余代码。每新增一个形状类型,几乎每个相关类都需要进行修改(如ShapesFactoryDrawer)。
  2. 性能开销:多层的继承和虚函数调用可能导致性能损失,尤其是在性能敏感的环境中,可能需要使用更优化的设计。
    这种设计模式适合于大型、复杂的系统,例如图形软件、游戏引擎和可视化应用等,能够平衡扩展性和灵活性。在实际使用时,根据项目的规模和需求,可以进一步优化或简化设计。

《The Fallen Paradigm》:C++现代设计的转变与应用

这部分代码中,我们看到一种完全现代化的 C++ 设计方式,通过std::variant替代了传统的面向对象编程(OOP)和继承体系,彻底简化了代码结构,并避免了很多遗留问题。下面是对这种现代化设计方式的详细解读:

1.“Fallen Paradigm” 和 OOP 的反思

引用中的一位未知评论者提出了一个批判性观点:传统的面向对象编程,尤其是继承的使用,已经被高估。C++ 语言本身有强大的模板系统和std::variant,这使得大多数依赖继承的设计变得不再必要。

  • 模板std::variant提供了比传统的继承体系更灵活、更高效的设计方式。
  • 对象的多态性可以通过std::variant来替代,而不需要定义复杂的继承关系。

2.传统的面向对象解决方案与std::variant的替代

在传统的面向对象设计中,我们通常会使用继承来定义形状(如CircleSquare等),并通过一个基类(如Shape)来统一接口。而在现代 C++ 中,使用std::variant让设计变得更加简洁。

传统设计:使用继承和多态
classShape{public:virtualvoiddraw()const=0;};classCircle:publicShape{public:voiddraw()constoverride{/* circle-specific drawing logic */}};classSquare:publicShape{public:voiddraw()constoverride{/* square-specific drawing logic */}};
  • 缺点
    • 需要处理基类指针虚函数,这可能导致性能损失。
    • 每增加一个新的形状,都需要修改所有相关代码(如工厂类、绘制策略等)。
    • 内存管理复杂,尤其是在使用多态时需要关注动态内存的分配和释放。
现代 C++ 设计:使用std::variant
usingShape=std::variant<Circle,Square>;usingShapes=std::vector<Shape>;
  • 通过std::variant,我们可以将不同类型的形状(如CircleSquare)组合在一起,而不需要继承自同一个基类。
  • std::variant提供了一个类型安全的联合体,它可以存储多种类型,但同时只存储一个类型的值。
  • 无需继承,可以使用模板类型推导来实现类似的功能。

3.ShapesFactoryOpenGLDrawer:简化工厂和绘制策略

  • 使用std::variant函数对象(Functors),可以简化工厂类和绘制策略的实现。
工厂类:
classShapesFactory{public:Shapescreate(std::string_view filename){Shapes shapes{};std::string shape{};std::ifstream shape_file{filename};while(shape_file>>shape){if(shape=="circle"){doubleradius;shape_file>>radius;shapes.emplace_back(Circle{radius});}elseif(shape=="square"){doubleside;shape_file>>side;shapes.emplace_back(Square{side});}else{break;}}returnshapes;}};
  • 简化:不需要为每个形状定义一个工厂类或添加工厂方法来创建每个具体类型的实例。
  • 灵活性:只需通过std::variant存储多种类型,代码更加简洁,易于扩展。
绘制策略:
classOpenGLDrawer{public:explicitOpenGLDrawer(/* ... */){}voidoperator()(Circleconst&circle)const{/* OpenGL drawing logic for Circle */}voidoperator()(Squareconst&square)const{/* OpenGL drawing logic for Square */}};
  • 函数对象(Functors):通过operator(),可以在运行时动态调度各种形状的绘制逻辑。
  • 使用std::variant,不同类型的形状和对应的绘制逻辑可以通过std::visit来访问和调用。

4.drawAllShapescreateAndDrawShapes:现代绘制流程

voiddrawAllShapes(Shapesconst&shapes,Drawer drawer){for(autoconst&shape:shapes){std::visit([&](autod,autos){d(s);},drawer,shape);}}voidcreateAndDrawShapes(Factory factory,std::string_view filename,Drawer drawer){Shapes shapes=std::visit([&filename](autof){returnf.create(filename);},factory);drawAllShapes(shapes,drawer);}
  • std::visit:它是std::variant提供的一个工具,用来访问存储在variant中的具体类型并执行相应操作。
  • createAndDrawShapes中,使用std::visit从工厂创建形状,然后传递给drawAllShapes进行绘制。
  • 运行时派发:通过std::visit可以在运行时动态选择正确的类型,并执行相应的绘制操作,而不需要使用虚函数。

5.总结:std::variant替代继承的优势

  1. 简化代码:避免了继承链的复杂性,减少了基类和派生类之间的紧密耦合。
  2. 减少内存管理:不再需要手动管理指针的生命周期,std::variant提供了更简洁的内存管理方式。
  3. 提高灵活性:使用std::variant,我们可以灵活地添加新的形状类型,而不需要修改现有的代码结构。
  4. 类型安全std::variant保证了类型安全,在编译时捕获类型错误,而不像传统的多态方法那样容易出错。

现代 C++ 的优势与挑战

  • 优势
    • 通过std::variant函数对象,可以实现更简洁和高效的代码。
    • 对于需要频繁扩展功能的场景,现代 C++ 提供的模板和变体提供了更强的可扩展性。
  • 挑战
    • 对于初学者而言,std::variantstd::visit可能显得比较抽象,需要一定的学习曲线。
    • 在某些情况下,传统的继承体系可能会更直观,特别是当涉及到复杂的多态性和对象生命周期管理时。
      这种设计方式适合于现代 C++ 项目,尤其是当性能和扩展性成为关键需求时。

《Truly Modern C++ Solution: std::variant》:面向现代C++的设计转变

在这部分内容中,我们深入探讨了使用std::variant和函数式编程方法代替传统面向对象编程(OOP)的优势。以下是对这种现代C++设计方式的详细解析。

1.现代C++的优势

通过引入std::variant,我们不再需要继承,也不需要使用指针(包括智能指针),而是通过值语义来管理对象的生命周期。设计方式变得更加简洁高效

  • 无需继承:摒弃了传统的基类和派生类的设计。
  • 值语义:通过直接存储值而不是指针来管理对象,简化内存管理,避免了复杂的生命周期管理。
  • 自动内存管理:现代C++的RAII(资源获取即初始化)机制自动处理对象的生命周期,减少了出错的机会。
  • 简化的代码:不再需要编写大量的样板代码,如指针管理、虚函数等。
  • 性能提升:相较于传统的OOP方法,使用std::variant可以提高性能,减少不必要的开销。

2.性能对比

在性能测试中,通过使用6种不同形状(如圆形、方形、椭圆、矩形、六边形和五边形),对10000个随机生成的形状执行了25000次translate()操作
测试在以下两个编译器版本上进行了:

  • GCC 13.2.0
  • Clang 18.1.4
    以及在以下配置下:
  • Intel Core i78核,3.8 GHz
  • 64 GB 主内存
测试结果
  • 经典OOP方法vsstd::variantvsmpark::variant
    • 结果显示,std::variant在性能上显著优于传统的OOP方法。
    • 在执行相同数量的操作时,std::variant方法的性能表现明显更好。
      性能比较的结果图示
  • GCC 13.2.0Clang 18.1.4编译器中,std::variant的执行时间远低于经典的OOP方法。

3.核心代码解析

通过std::variantstd::visit的结合,我们能够简洁地处理形状和其绘制操作,而无需使用多层继承和虚函数。

形状类定义:
usingShape=std::variant<Circle,Square>;usingShapes=std::vector<Shape>;

通过std::variant,我们能够存储不同类型的形状(如圆形和方形),而无需为每种形状创建不同的类。

绘制类OpenGLDrawer
classOpenGLDrawer{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voidoperator()(Circleconst&circle)const;voidoperator()(Squareconst&square)const;private:// ... Data members (color, texture, transparency, ...)};
  • OpenGLDrawer是一个函数对象,负责执行不同形状的绘制操作。
  • operator()方法使其成为一个可调用对象,并且通过std::variant实现多种类型的图形绘制。
绘制操作:
voiddrawAllShapes(Shapesconst&shapes,Drawer drawer){for(autoconst&shape:shapes){std::visit([&](autod,autos){d(s);},drawer,shape);}}
  • std::visit:通过std::visit,我们可以访问存储在std::variant中的具体类型并调用相应的绘制函数。它提供了一个类型安全的机制,能够保证在运行时动态选择正确的类型。
工厂和绘制流程:
voidcreateAndDrawShapes(Factory factory,std::string_view filename,Drawer drawer){Shapes shapes=std::visit([&filename](autof){returnf.create(filename);},factory);drawAllShapes(shapes,drawer);}
  • std::visit被用来根据传入的工厂类型(如ShapesFactory)创建形状对象。
  • 创建后的形状被传递给drawAllShapes,使用指定的绘制对象(如OpenGLDrawer)进行渲染。

4.现代C++设计的优点

  • 无需继承:使用std::variant可以避免传统面向对象编程中的复杂继承结构。每个形状都是一个独立的类型,而不是继承自同一个基类。
  • 简洁代码:通过将所有形状存储在同一个std::variant容器中,避免了对每个具体类型的显式依赖,减少了冗余代码的编写。
  • 高效性能:相比于使用继承和虚函数,std::variant提供了更快的类型选择和运行时派发,显著提高了性能。
  • 更简化的生命周期管理:不再需要显式管理对象的生命周期,因为std::variant会自动处理内存管理。

5.总结与展望

现代C++的设计思路(例如使用std::variant)提供了一种更加灵活且高效的替代方案,尤其是在需要管理多种类型对象时。它通过:

  • 无继承的设计
  • 值语义而非指针
  • 自动化的内存管理
    有效地简化了代码,并提高了运行时性能。虽然对于一些简单的场景传统的OOP方法仍然适用,但对于复杂的应用,现代C++提供的这些工具可以显著提升代码的简洁性和执行效率。
    这种设计方式对于面向高性能高扩展性的项目尤其有吸引力,在未来的C++开发中将变得更加流行。

模板与设计决策:使用std::variant替代传统 OOP

这一部分探讨了模板在现代C++中的应用,特别是std::variant在替代传统面向对象编程(OOP)时的优势和挑战。以下是对这一设计模式的详细分析,尤其是其在代码复杂度和维护性上的影响。

1.模板的应用

模板作为C++的一大特性,允许代码在编译时生成不同类型的实例。在这里,我们通过将绘制形状创建形状的操作提取为函数模板,极大地减少了代码重复,并提供了更加灵活的接口。

绘制形状模板:
template<typenameShapes,typenameDrawer>voiddrawAllShapes(Shapesconst&shapes,Drawer drawer){for(autoconst&shape:shapes){std::visit(drawer,shape);}}
  • std::visit:通过std::visit,我们能够在不同类型的shape上调用相应的绘制方法,而无需显式地检查每种类型。
  • Drawer:这个模板参数允许我们传递不同类型的绘制对象,如OpenGLDrawer,从而支持多种绘制方式。
创建并绘制形状模板:
template<typenameFactory,typenameDrawer>voidcreateAndDrawShapes(Factory factory,std::string_view filename,Drawer drawer){autoshapes=factory.create(filename);drawAllShapes(shapes,drawer);}
  • Factory:模板化工厂对象,能够从指定文件中读取形状数据并创建形状。
  • Drawer:负责渲染形状的绘制器对象,支持不同的渲染策略。
    通过使用模板,这段代码实现了依赖倒转,即createAndDrawShapes函数不再直接依赖于特定类型的形状或绘制方式,而是通过模板化参数使得实现更为通用和灵活。

2.std::variant与面向对象编程(OOP)

虽然现代C++中的模板和std::variant提供了许多优势,但在大规模的代码库中,std::variant的使用也有其局限性。

优势:
  • 无继承:通过std::variant,我们可以将不同类型的对象(如圆形、方形、矩形等)存储在同一个容器中,而不需要定义复杂的继承层次。
  • 简化代码:不再需要多态和虚函数的支持,代码变得更加简洁和易于理解。
  • 类型安全:使用std::variant,类型在编译时就可以得到检查,避免了运行时的错误。
挑战:
  • 不适用于超大规模项目:在一个代码量达到千万行的大型项目中,使用模板和std::variant的方式可能导致过多的模板实例化,增加了编译时间,并且使得代码的可维护性变差。每当需要添加新的形状或绘制方法时,都需要修改相关模板参数,导致代码的耦合度上升。

3.std::variant的限制:并非万无一失的解决方案

尽管std::variant在性能和代码简化上具有优势,但它并非适用于所有场景。以下是一些潜在的限制:

  • 编译时间:模板在大规模项目中可能引入长时间的编译延迟,尤其是当模板参数较多时,模板的实例化会导致编译器需要生成大量的代码。
  • 复杂性增加:当需要处理更多种类的类型时,模板和std::variant的组合会变得复杂,增加了理解和维护的难度,特别是在团队协作中。
  • 缺乏灵活性:对于一些复杂的逻辑,模板和std::variant的静态类型检查可能限制了灵活性。特别是当项目需要动态调整时,面向对象的动态多态性显得更为方便。

4.模板和std::variant的应用场景

尽管std::variant和模板在大型项目中的使用可能带来一些问题,但它们在小型项目性能要求高的系统中仍然非常有用,尤其是以下几个场景:

  • 简单图形系统:如上述的图形绘制系统,使用模板和std::variant来处理不同形状的图形非常有效,能够显著减少代码的重复。
  • 类型安全的回调机制:通过std::variant,可以在一个容器中存储不同类型的回调,并在运行时动态选择合适的回调函数。
  • 小型库的设计:在编写不依赖于复杂继承结构的库时,使用std::variant可以减少代码的冗余,并提高效率。

5.总结:是否采用std::variant

  • 小规模项目:对于小型项目或性能要求较高的系统,使用std::variant和模板的组合能够带来简洁且高效的设计,减少代码重复并提高性能。
  • 大规模项目:在代码量达到千万行以上时,std::variant可能会带来过多的模板实例化,增加编译时间,并且代码的维护性和可扩展性可能会受到影响。在这种情况下,传统的面向对象设计可能更为适合。
    总的来说,std::variant和模板是现代C++设计中的有力工具,但它们并非解决所有问题的“银弹”。选择是否使用它们,应该根据具体的项目需求和规模来决定。

std::variant与虚函数的比较

在现代 C++ 中,std::variant和虚函数是两种不同的设计方法,它们分别代表了两种不同的编程范式:功能编程面向对象编程。虽然两者有些相似之处,但它们并不是可以互相替代的,特别是在架构设计层面。

1.std::variantvs 虚函数

虚函数
  • 动态多态性:虚函数通过继承和多态提供了一种动态的运行时方法选择机制。基类指针可以指向不同类型的派生类对象,并根据对象的实际类型调用相应的函数。
  • 固定类型集合:虚函数依赖于类的继承层次,类型集合是固定的。新的类型需要通过继承或修改现有的类体系来实现。
  • 封闭的操作集合:在虚函数中,操作的集合是封闭的,也就是说操作(函数)一旦定义,就不太容易扩展。
std::variant
  • 功能编程std::variant实现了Visitor设计模式,允许通过std::visit调用不同类型上的函数。它并不依赖于继承结构,而是通过类型安全的方式处理多种类型的组合。
  • 开放的类型集合std::variant允许类型在编译时定义,而不像虚函数那样依赖固定的继承结构。这使得可以在编译时确定类型,但缺乏虚函数的动态特性。
  • 开放的操作集合:与虚函数不同,std::variant允许我们向不同的类型添加新的操作,而不需要修改继承层次。每次添加新的类型或功能时,我们只需要修改std::variantstd::visit的相关部分。

2. 设计模式与架构选择

设计模式的作用

设计模式不仅仅是代码中的解决方案,它们还代表了依赖结构。不同的设计模式具有不同的架构特性。理解每种设计模式背后的架构思维,可以帮助我们在不同的情况下做出最佳选择。

  • CRTP (Curiously Recurring Template Pattern)std::variant都有其各自的优势,但它们并不能直接替代虚函数。
  • CRTP通过模板参数和编译时类型推导避免了虚函数的运行时开销,但它并不适用于所有情境,尤其是在需要动态多态的地方。
  • std::variant提供了另一种处理多类型数据的方法,但它缺乏虚函数所提供的真正的动态分派能力,尤其是在处理需要运行时决定的多态行为时。
架构决策的正确顺序
  1. 首先考虑架构设计,而不是实现细节。架构决定了系统如何组织和扩展,因此应优先考虑。
  2. 根据需求选择合适的设计模式/抽象。设计模式应根据需求进行选择,而不是为了性能或简单性而盲目使用某种模式。
  3. 避免性能驱动的设计。设计模式的选择不应该仅仅为了性能优化,而是应基于系统的实际需求和可维护性。

3. 总结:虚函数、std::variant与架构选择

  • 虚函数是处理动态多态性和运行时选择操作的标准方法,特别适合需要对象之间交互和动态扩展的场景。
  • std::variant提供了一种替代继承体系的方式,通过类型安全的std::visit使得对不同类型的操作变得更加灵活,但它不具备虚函数的动态多态能力。
  • 架构设计应该是第一步,设计模式和抽象应根据系统的需求来选定,而不是盲目追求某种技术或性能的优化。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/11 20:53:01

USBIPD-WIN技术指南:Windows与WSL 2的USB设备无缝共享解决方案

USBIPD-WIN技术指南&#xff1a;Windows与WSL 2的USB设备无缝共享解决方案 【免费下载链接】usbipd-win Windows software for sharing locally connected USB devices to other machines, including Hyper-V guests and WSL 2. 项目地址: https://gitcode.com/gh_mirrors/us…

作者头像 李华
网站建设 2025/12/11 20:52:50

Xray实时协作功能终极指南:多人编程的完美解决方案

想要和团队成员一起实时编辑代码&#xff0c;享受无缝的协同编程体验吗&#xff1f;&#x1f680; Xray作为一款实验性的下一代基于Electron的文本编辑器&#xff0c;其强大的实时协作功能让多人编程变得前所未有的简单高效。本文将为你完整揭秘Xray的协同编辑机制&#xff0c;…

作者头像 李华
网站建设 2025/12/18 12:09:49

Rebel终极AppKit优化框架:告别Cocoa开发痛点

Rebel终极AppKit优化框架&#xff1a;告别Cocoa开发痛点 【免费下载链接】Rebel Cocoa framework for improving AppKit 项目地址: https://gitcode.com/gh_mirrors/reb/Rebel 在macOS应用开发中&#xff0c;AppKit框架虽然功能强大&#xff0c;但常常伴随着繁琐的API和…

作者头像 李华
网站建设 2025/12/11 20:49:50

Android bugreportz 源码分析

源码分析 Android.bp // bugreportz // package {// 许可证配置&#xff1a;继承 frameworks_native 项目的 Apache 2.0 许可证// 相关说明&#xff1a;http://go/android-license-faq// 大规模变更添加了 default_applicable_licenses 以导入所有许可证类型default_applicabl…

作者头像 李华
网站建设 2025/12/22 20:49:48

GC5035 CSP CMOS图像传感器终极指南:高性能移动摄像头解决方案

GC5035 CSP CMOS图像传感器终极指南&#xff1a;高性能移动摄像头解决方案 【免费下载链接】GC5035CSP图像传感器数据手册 GC5035 是一款高质量的 500 万像素 CMOS 图像传感器&#xff0c;专为移动电话摄像头应用和数码相机产品设计。GC5035 集成了一个 2592H x 1944V 像素阵列…

作者头像 李华
网站建设 2025/12/25 10:24:23

OpCore Simplify:告别复杂配置,一键生成完美黑苹果EFI

OpCore Simplify&#xff1a;告别复杂配置&#xff0c;一键生成完美黑苹果EFI 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 还在为繁琐的黑苹果配置…

作者头像 李华