无锡网站维护,网页微信登陆登录入口,北京市朝阳区最新消息,wordpress如何做产品展示设计模式与虚函数
讨论集中在设计模式、虚函数以及如何优化继承层次结构的开销。以下是对CRTP#xff08;Curiously Recurring Template Pattern#xff09;和std::variant的详细分析。
继承层次结构的开销
在面向对象编程中#xff0c;继承层次结构的使用通常会引入虚函数Curiously Recurring Template Pattern和std::variant的详细分析。继承层次结构的开销在面向对象编程中继承层次结构的使用通常会引入虚函数从而产生运行时多态性的开销。主要有以下几个问题动态调度的开销每次调用虚函数时程序必须查找正确的函数实现这需要通过虚拟表vtable进行。这种动态查找会增加调用的开销。内存使用的增加每个具有虚函数的对象都需要存储指向虚拟表的指针这会增加内存开销。为了优化这些问题有时需要使用设计模式如CRTP和**std::variant**。CRTP 之前先了解这个代码分析与输出解析我们来看这两段代码和它们的输出详细分析背后的原理。第一段代码#includeiostreamclassBase{public://没有多态静态this类型voidprint1(){std::couttypeid(*this).name()std::endl;}};classDriver:publicBase{public:voidprint2(){std::couttypeid(*this).name()std::endl;}};intmain(){Driver d;d.print1();d.print2();}输出4Base 6Driver输出解释在这段代码中我们有一个Base类和一个继承自它的Driver类。d.print1()输出:这里print1方法是Base类中的普通成员函数没有使用虚函数机制。typeid(*this)的作用是获取当前对象的类型。由于print1是在Base类中调用的this实际上是指向一个Driver类型的对象因为d是Driver类型的实例但由于没有虚函数机制typeid(*this)会返回Base类型的静态类型。这里的输出是4Base数字4是编译器生成的Base类型的名字长度具体数值依赖编译器。d.print2()输出:print2是Driver类的成员函数因此在print2中this的类型已经明确是Driver类型。由于print2是在Driver类中调用的typeid(*this)返回的是Driver类型的静态信息。这里的输出是6Driver数字6是编译器生成的Driver类型的名字长度具体数值依赖编译器。第二段代码#includeiostreamclassBase{public://多态动态类型voidprint1(){std::couttypeid(*this).name()std::endl;}virtualvoidprint3(){std::couttypeid(*this).name()std::endl;}};classDriver:publicBase{public:voidprint2(){std::couttypeid(*this).name()std::endl;}};intmain(){Driver d;d.print1();d.print2();d.print3();}输出6Driver 6Driver 6Driver输出解释d.print1()输出:这次print1仍然是Base类中的方法但与第一段代码的不同之处在于Base类的print1方法是一个非虚方法。由于print1方法是非虚函数在调用print1时typeid(*this)依然会返回当前对象的动态类型即Driver类型因为d是一个Driver对象。所以d.print1()输出的是6Driver。d.print2()输出:print2方法是Driver类中的方法因此typeid(*this)会返回Driver类型。这里输出是6Driver与print1相同。d.print3()输出:由于print3是虚函数它将触发动态类型识别即使它是Base类中的方法。通过虚函数机制typeid(*this)会根据运行时类型确定当前对象的实际类型。在这个例子中d的实际类型是Driver所以typeid(*this)返回的也是Driver类型。结果是6Driver。总结第一段代码print1调用时Base类的print1方法没有虚函数机制因此它返回的是静态类型Base。print2调用时Driver类的print2方法返回的是静态类型Driver。第二段代码print1方法仍然是Base类的方法但由于没有虚函数机制print1返回的是动态类型Driver因为d是Driver类型。print2和print3都返回Driver类型因为它们涉及到派生类Driver中的成员且print3是虚函数因此使用了动态绑定。关键点静态类型与动态类型没有虚函数时typeid(*this)返回的是静态类型即编译时确定的类型。使用虚函数时typeid(*this)返回的是动态类型即运行时确定的实际对象类型。虚函数的影响使用虚函数时C 会进行动态类型识别RTTI即便函数在基类中定义派生类的类型信息也能被正确识别。CRTP - Curiously Recurring Template Pattern奇异递归模板模式CRTP是一种通过模板实现静态多态的设计模式目的是避免虚函数和动态调度的开销。该模式的基本思想是一个类模板通过继承自己的模板实例来达到静态多态化。CRTP可以在编译时解决多态问题而不需要依赖虚函数。例如下面是一个使用CRTP的C示例templatetypenameDerivedclassBase{public:voidfoo(){// 静态调用派生类的成员函数static_castDerived*(this)-fooImpl();}};classDerived:publicBaseDerived{public:voidfooImpl(){std::coutDerived fooImpl called!std::endl;}};在这个例子中Base类是一个模板类它接受派生类Derived作为模板参数。Derived类继承自BaseDerived从而让Base类能够调用派生类的fooImpl函数。foo()在编译时解析调用而不是在运行时动态查找。CRTP的优点零开销抽象CRTP避免了虚函数的开销所有的调用都在编译时解析。没有虚拟表由于没有运行时多态性CRTP模式下的对象不需要存储指向虚拟表的指针从而减少了内存开销。缺点代码复杂性对于不熟悉该模式的开发者来说代码可能显得较为复杂理解起来较难。std::variantstd::variant是C17引入的一个类型安全的联合体用于在同一对象中存储多个不同类型的数据。与传统的继承层次结构不同std::variant避免了虚函数和继承的开销它可以用来替代多态场景中的一些用法。例如下面是一个使用std::variant的简单示例#includevariant#includeiostreamstructA{voidprint(){std::coutAstd::endl;}};structB{voidprint(){std::coutBstd::endl;}};usingMyVariantstd::variantA,B;voidprint_variant(constMyVariantv){std::visit([](autoarg){arg.print();},v);}intmain(){MyVariant vA{};print_variant(v);// prints AvB{};print_variant(v);// prints B}在这个例子中MyVariant是一个std::variant类型它可以存储A或B类型的对象。print_variant使用std::visit来访问variant中存储的对象并调用相应的print()方法。std::variant的优点避免虚函数开销std::variant不会使用虚函数它通过类型安全的方式进行操作。类型安全每次只能持有一种类型并且你可以通过std::visit安全地访问它。缺点灵活性不足与继承层次结构相比std::variant在某些情况下缺乏扩展性特别是在需要多种行为变体时。代码复杂性使用std::visit可能会使代码变得冗长特别是在处理多种变体类型时。总结继承层次结构在传统的面向对象设计中使用虚函数来实现多态性但这会带来运行时开销动态调度和内存开销vtable。CRTP通过静态多态性消除了虚函数的开销减少了运行时开销但会增加代码的复杂性。std::variant是一种类型安全的替代方案它避免了虚函数和继承的开销适用于一些特定场景但它的灵活性较低。理解CRTP奇异递归模板模式是一种通过模板实现静态多态的设计模式避免了虚函数和动态调度的开销。这种方法通过编译时解析来实现多态。std::variant则是C17引入的一个类型安全的联合体它能够在同一个对象中存储不同类型的数据通过std::visit提供多态行为避免了继承层次结构中的虚函数和动态调度开销。CRTPCuriously Recurring Template Pattern详解Curiously Recurring Template Pattern奇异递归模板模式简称 CRTP是一种静态多态的实现方法它通过模板机制来消除继承层次结构中的虚函数开销。在 C 中CRTP 使得派生类通过继承模板基类来进行多态操作而不是依赖运行时的虚函数调用从而提高性能。下面我们逐步分析 CRTP 在 C 中的实现过程。CRTP 示例代码分析基本的类模板与继承templatetypenameDerivedclassAnimal{// 基类};classSheep:publicAnimalSheep{// 派生类};在这段代码中Animal是一个模板类它的模板参数是Derived将被具体化为Sheep。Sheep类继承自AnimalSheep这就是 CRTP 的核心类Sheep通过模板继承类AnimalSheep来实现静态多态。构造函数保护templatetypenameDerivedclassAnimal{private:Animal()default;// 防止错误的派生类实例化~Animal()default;// 防止错误的派生类析构};classSheep:publicAnimalSheep{// 可以继承 AnimalSheep 类但无法直接构造 Animal 类};在这段代码中Animal类的构造函数和析构函数是私有的不能直接在外部创建Animal类的实例。这样做的目的是防止直接创建基类对象从而确保只有派生类才能被实例化。3.使用friend关键字进行授权templatetypenameDerivedclassAnimal{private:Animal()default;~Animal()default;friendDerived;// 让派生类可以访问私有成员};通过friend Derived;我们授予Derived即Sheep类访问Animal基类的私有成员的权限这样可以控制访问权限。4.定义成员函数templatetypenameDerivedclassAnimal{public:voidmake_sound()const{static_castDerivedconst(*this).make_sound();// 通过静态转换调用派生类的 make_sound 函数}};classSheep:publicAnimalSheep{public:voidmake_sound()const{std::coutbaa;}};在这里Animal类定义了一个成员函数make_sound但它并没有提供具体实现。相反它通过static_castDerived const(*this).make_sound()静态地调用派生类Sheep中的make_sound函数。这样做的好处是make_sound方法在编译时就能够被解析不会产生运行时的虚函数开销。5.通用函数模板templatetypenameDerivedvoidprint(AnimalDerivedconstanimal){// ...}这个模板函数print接受任意Animal类的对象包括Sheep并可以在不引发虚函数调用的情况下执行操作。CRTP 的优点避免虚函数开销CRTP 通过静态多态消除了虚函数的运行时开销。所有的函数调用都在编译时解析而不是运行时。提高性能由于没有虚函数表vtableCRTP 可以减少内存使用并避免额外的指针解引用这使得它比传统的面向对象多态通过虚函数更加高效。代码复用可以通过Animal类实现许多不同动物的行为而不需要为每个派生类都定义虚函数。CRTP 的缺点增加代码复杂性CRTP 使得代码结构变得更加复杂尤其是对于不熟悉模板编程的开发者来说理解起来可能会有些困难。无法动态扩展CRTP 的多态性是静态的无法像传统的继承层次结构那样动态扩展。如果想要添加新的行为可能需要修改模板的代码而不如虚函数那样灵活。数学公式与理解CRTP 通过静态多态的实现方式取代了运行时多态。这种方式通过模板机制实现了编译时决策从而避免了传统 OOP 中虚函数带来的开销。具体来说类Animal是一个模板类它通过接受具体的派生类作为模板参数从而实现了类似虚函数的行为。通过static_castAnimal类中的make_sound函数可以调用派生类如Sheep中的同名函数所有这些操作都发生在编译时而非运行时。编译时AnimalDerived中的make_sound()方法和Derived类的make_sound()方法通过模板机制被确定为一个明确的调用路径。通过静态多态性实现的调用在编译时确定避免了虚函数的动态查找过程。这个模式的关键在于静态类型推导所有的多态行为在编译时就已经确定不需要依赖于运行时的动态类型识别如虚函数表。总结CRTPCuriously Recurring Template Pattern是一种通过模板实现的静态多态模式它通过让派生类作为模板参数传递给基类避免了虚函数的开销。这种方式具有较高的性能和较低的内存开销特别适用于需要高效、低开销的多态实现。但它也带来了代码复杂性和灵活性不足的挑战因此需要在具体场景中权衡使用。CRTPCuriously Recurring Template Pattern的局限性尽管CRTPCuriously Recurring Template Pattern提供了许多优点如消除虚函数的开销、提高性能等但它也有一些局限性。以下是两个主要的局限性1. 限制没有公共基类在 CRTP 中每个派生类都需要继承自基类模板但由于模板类型参数的不同所有派生类的基类并不共享同一个公共基类。例如考虑以下代码templatetypenameTclassAnimal{// ...};classSheep:publicAnimalSheep{// ...};classDog:publicAnimalDog{// ...};classCat:publicAnimalCat{// ...};在这个例子中Sheep类继承自AnimalSheep。Dog类继承自AnimalDog。Cat类继承自AnimalCat。虽然它们都继承自模板类Animal但是每个类的基类模板类型参数是不同的。这就意味着Sheep、Dog和Cat并没有一个共享的公共基类。它们之间没有通用的接口或继承关系这在一些需要统一接口或通用基类的情况下会带来问题。问题如果你希望所有动物类如Sheep、Dog、Cat有一个公共基类这种方式就不适用了因为 CRTP 的设计原则是每个派生类都继承自一个以派生类本身为模板参数的基类。这使得在处理需要统一操作或者通用接口的场景时CRTP 模式显得不太合适。2. 限制一切都是模板CRTP 的另一个限制是它会使得与 CRTP 相关的所有代码都变成模板代码。换句话说任何涉及到 CRTP 的函数或者类都必须是模板这会导致一些负面影响特别是编译时间增加。例如templatetypenameDerivedclassAnimal{// ...};classSheep:publicAnimalSheep{// ...};templatetypenameDerivedvoidprint(AnimalDerivedconstanimal){// ...}在这个例子中print函数是一个模板函数它必须是模板函数才能处理所有类型的Animal例如Sheep、Dog等。因此任何涉及 CRTP 的代码都需要变成模板甚至包括所有与 CRTP 相关的函数和类。问题模板的传播由于print函数必须是模板函数它会导致所有与 CRTP 相关的函数、类和接口都必须是模板。比如如果你需要在多个地方使用Animal或者Sheep类型就必须传递模板类型参数。这种模式可能会让代码变得冗长且不易维护。增加编译时间由于模板在编译时需要进行实例化涉及到 CRTP 的代码会导致编译时间的增加。编译器需要处理大量模板实例化从而增加了编译时间。代码复杂性模板代码本身就很复杂而 CRTP 使得一切都变成模板进一步增加了代码的复杂度尤其是对于不熟悉模板编程的开发者。总结CRTP 的局限性没有公共基类CRTP 无法提供一个通用的基类或接口因此在需要统一操作的场景下无法使用 CRTP。每个派生类都需要单独继承并传递模板参数导致缺乏统一的继承结构。一切都是模板CRTP 导致所有与之相关的代码包括函数、类等都必须是模板这不仅增加了代码的复杂性还会显著提高编译时间。特别是在大型项目中这种问题可能会变得更加明显。理解1. 限制没有公共基类由于 CRTP 的设计方式派生类必须传递自己作为模板参数因此每个派生类的基类都是独立的没有公共基类。这意味着如果你需要一个统一的接口或者需要共享的基类那么 CRTP 将无法满足这一需求。2. 限制一切都是模板CRTP 的模式会使得所有涉及的类和函数都变成模板这导致了代码的复杂性增加。所有触及 CRTP 的地方都必须是模板这样不仅增加了代码的冗长度还可能导致编译时间的显著增长。CRTP 增加功能示例在这个例子中我们通过CRTPCuriously Recurring Template Pattern模式来增加功能使得基类NumericalFunctions可以提供一个scale函数这个函数会通过静态多态性调用派生类的具体方法来执行操作。代码分析templatetypenameDerivedstructNumericalFunctions{voidscale(doublemultiplicator){Derivedunderlyingstatic_castDerived(*this);underlying.setValue(underlying.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctionsSensitivity{doublegetValue()const{returnvalue;}voidsetValue(doublev){valuev;}doublevalue;};intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,s.getValue() {},s.getValue());}步骤解析NumericalFunctions类模板NumericalFunctions是一个模板类它接受一个派生类Derived作为模板参数。该类定义了一个成员函数scale这个函数接受一个乘数并调整派生类的值voidscale(doublemultiplicator){Derivedunderlyingstatic_castDerived(*this);underlying.setValue(underlying.getValue()*multiplicator);}scale函数该函数通过static_castDerived(*this)将当前对象基类对象转换为派生类对象这样就可以直接访问派生类中的getValue和setValue函数。scale函数会修改派生类中的值将其乘以给定的乘数multiplicator。static_cast这是关键部分它利用了 CRTP 的静态多态性通过静态转换将基类指针转换为派生类类型以便访问派生类的方法。Sensitivity类Sensitivity类继承自NumericalFunctionsSensitivity并提供了getValue和setValue函数structSensitivity:publicNumericalFunctionsSensitivity{doublegetValue()const{returnvalue;}voidsetValue(doublev){valuev;}doublevalue;};getValue和setValue这两个成员函数是Sensitivity类特有的分别用于获取和设置value属性。scale函数会使用这些函数来更新value的值。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函数在编译时就能够知道如何调用派生类的getValue和setValue函数这使得 CRTP 能够在不使用虚函数的情况下提供多态行为。模板特化NumericalFunctionsSensitivity作为模板被具体化为Sensitivity类型这样Sensitivity类可以继承并使用NumericalFunctions中的函数如scale。此过程是通过模板机制在编译时完成的。没有虚函数开销由于是静态多态通过模板整个过程没有运行时虚函数的开销所有的函数调用都在编译时解析。这是 CRTP 的一个重要优势避免了传统继承中虚函数调用的动态开销。理解在这个示例中我们使用了CRTP来为派生类增加功能。基类NumericalFunctions提供了一个scale函数可以通过静态多态性来调用派生类的成员函数getValue和setValue从而修改派生类的状态。#includeiostream// 用于输出#includeformat// 用于格式化输出 (需要 C20 或更高版本)structNumericalFunctions{// scale 函数定义通过 this 指针操作当前对象修改它的值voidscale(thisautoself,doublemultiplicator){// 将当前对象的值乘以 multiplicator 并设置回对象self.setValue(self.getValue()*multiplicator);}};// 定义 Sensitivity 结构体继承自 NumericalFunctionsstructSensitivity:publicNumericalFunctions{// 获取当前对象的值doublegetValue()const{returnvalue;}// 设置当前对象的值voidsetValue(doublev){valuev;}// 定义 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());}这段 C23 代码展示了如何通过使用this auto self来实现一种新的编程技巧即“显式对象参数”或称为“推导 this”。代码解析scale函数scale函数是NumericalFunctions类的一部分。它采用一个this参数即auto self这个参数代表调用该函数的对象。函数体内self.setValue(self.getValue() * multiplicator)通过调用self的成员函数来更新对象的值。Sensitivity结构体Sensitivity继承自NumericalFunctions并实现了getValue()和setValue()方法用来访问和修改成员变量value。在main()函数中创建了一个Sensitivity类型的对象s并初始化了value为 1.2。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 valueold value×multiplicator \text{new value} \text{old value} \times \text{multiplicator}new valueold value×multiplicator具体地getValue()获取当前对象的值multiplicator是传入的倍数结果被赋值回value通过setValue()方法。C23 特性this auto self是 C23 中的一个新特性用于让成员函数的this指针自动推导类型。这意味着不再需要明确地声明this指针的类型编译器能够推导出实际类型。这种写法带来了更简洁和灵活的代码设计尤其是在链式调用或某些模板编程场景下非常有用。Curiously Recurring Template Pattern (CRTP) – 未来及其应用Curiously Recurring Template Pattern (CRTP)是 C 中的一种强大技巧它通过让一个类模板继承自另一个以派生类作为模板参数的类模板提供了静态多态性。这种模式消除了虚函数调用的开销通过让编译器在编译时确定具体类型避免了运行时的动态分发。代码中的问题分析我们来看下你提供的代码classAnimal{public:templatetypenameSelfvoidmake_sound(thisSelfconstself){self.make_sound_impl();}};classSheep:publicAnimal{public:voidmake_sound_impl()const{std::coutbaa;}};intmain(){Sheep sheep;Animalanimalsheep;sheep.make_sound();// works fineanimal.make_sound();// Compilation error!}为什么会出现编译错误模板推导问题关键问题在于make_sound是一个模板方法它期望调用它的类能够作为 “Self” 类型。在你调用animal.make_sound()时animal是Animal类型而编译器无法从Animal中推导出派生类例如Sheep的类型。当你调用animal.make_sound()时animal的类型是Animal编译器无法知道Self应该是什么类型因此无法解析make_sound_impl()方法。CRTP 在日常代码中的作用Curiously Recurring Template Pattern (CRTP)在日常编程中可以带来很多优势尤其在以下几种情况下增加功能性CRTP 允许你为多个类添加通用功能而无需重复编写相同的代码。比如你可以在Animal类中定义make_sound方法任何实现了make_sound_impl方法的类比如Sheep都可以复用这一方法。静态接口编译时多态性CRTP 可以用来创建静态接口编译器确保派生类实现了特定的功能。与运行时多态性不同CRTP 使得接口和功能的检查发生在编译时从而提高了性能和类型安全。性能优化由于没有虚函数调用CRTP 在某些场景下比传统的继承模型要更高效尤其是基类功能是通用的可以在多个派生类之间复用的情况下。更细粒度的继承控制CRTP 给了你更大的灵活性可以自定义每个派生类的行为而无需依赖虚函数。CRTP 的两种形式简单 CRTP:派生类直接使用基类模板。私有构造函数 CRTP:基类模板通过私有构造函数强制派生类使用防止传入错误的类类型。改进代码示例为了解决错误并使代码按预期工作你可以使用动态多态性或显式类型转换来确保派生类的类型被传递给基类。以下是一个使用动态类型转换的示例#includeiostreamclassAnimal{public:templatetypenameSelfvoidmake_sound(thisSelfconstself){self.make_sound_impl();}};classSheep:publicAnimal{public:voidmake_sound_impl()const{std::coutbaa;}};intmain(){Sheep sheep;Animalanimalsheep;sheep.make_sound();// works fine// animal.make_sound(); // 仍然报错因为 Animal 并不知道 Sheep 是什么类型// 修复显式转换为派生类类型dynamic_castSheep(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可以在不依赖虚函数的情况下为派生类提供通用的功能同时允许派生类定义自己的行为。代码解析templatetypenameDerivedstructNumericalFunctions{voidscale(doublemultiplicator){Derivedunderlyingstatic_castDerived(*this);underlying.setValue(underlying.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctionsSensitivity{doublegetValue()const{returnvalue;}voidsetValue(doublev){valuev;}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_castDerived(*this)将基类指针转为派生类类型以便调用派生类的具体实现方法如getValue和setValue。Derived underlying static_castDerived(*this);通过这种方式NumericalFunctions可以访问Sensitivity类的方法而不需要在基类中显式定义它们。这是 CRTP 的一个关键特性基类可以调用派生类的方法而不需要依赖运行时多态。underlying.setValue(underlying.getValue() * multiplicator);该行代码通过派生类的getValue获取当前的值再通过setValue更新这个值。2.Sensitivity类Sensitivity类是NumericalFunctionsSensitivity的派生类它实现了getValue和setValue方法这两个方法用于读取和设置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并初始化其value为1.2。调用s.scale(2.0)将value缩放为原值的 2 倍即1.2 * 2 2.4。最后通过s.getValue()输出更新后的值应该输出2.4。CRTP 的优势无虚函数调用CRTP 通过编译时多态性消除了虚函数调用的开销使得代码在性能上更具优势。在这里NumericalFunctions提供了scale方法派生类Sensitivity可以通过编译时类型推导实现特定行为而无需依赖虚函数机制。代码复用NumericalFunctions类提供了通用功能而具体的getValue和setValue由派生类实现。这样可以在不同的派生类中重用NumericalFunctions的功能而不需要重复编写scale方法。类型安全由于NumericalFunctions使用了静态类型推导它可以确保调用的是正确类型的成员方法并避免了运行时的错误。这种方法在编译时已经得到了类型检查提高了类型安全性。简化代码CRTP 让你能把常见的功能放到基类中而派生类只需要专注于实现具体的行为。这种结构使得代码更简洁避免了重复的实现。总结通过 CRTP 模式你可以在 C 中实现高效的静态多态性减少运行时开销并使得代码更加模块化和可复用。在这个例子中基类NumericalFunctions提供了一个通用的功能而派生类Sensitivity提供了具体的实现结合 CRTP 达到了一种高效且灵活的设计方式。代码解析使用 C23 的this关键字和扩展的成员函数在你提供的这段代码中主要涉及的是C23中引入的this关键字的扩展它允许在成员函数中更加灵活地访问成员。具体来说this关键字现在不仅限于指向当前对象的指针还能用于构造成员函数的“通用引用”。代码详细分析structNumericalFunctions{voidscale(thisautoself,doublemultiplicator){self.setValue(self.getValue()*multiplicator);}};structSensitivity:publicNumericalFunctions{doublegetValue()const{returnvalue;}voidsetValue(doublev){valuev;}doublevalue;};intmain(){Sensitivity s{1.2};s.scale(2.0);std::println(std::cout,s.getValue() {},s.getValue());}1.NumericalFunctions结构体structNumericalFunctions{voidscale(thisautoself,doublemultiplicator){self.setValue(self.getValue()*multiplicator);}};NumericalFunctions结构体定义了一个成员函数scale它接受一个参数self这是一个“通用引用”也就是auto。在C23中this可以作为成员函数的参数来引用当前对象而不仅仅是指针。self在这里代表当前对象本身且它是完美转发的通过auto。self.setValue(self.getValue() * multiplicator);self.getValue()获取当前对象的值。self.setValue()用来设置当前对象的值getValue和setValue都是派生类在本例中是Sensitivity类所定义的方法。self通过传递给scale函数允许我们在NumericalFunctions类中调用派生类的方法达到了代码复用的目的。2.Sensitivity结构体structSensitivity:publicNumericalFunctions{doublegetValue()const{returnvalue;}voidsetValue(doublev){valuev;}doublevalue;};Sensitivity类继承了NumericalFunctions并实现了getValue和setValue方法。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并初始化value为1.2。调用s.scale(2.0)将value缩放为原值的 2 倍即1.2 * 2 2.4。最后通过s.getValue()输出更新后的值应该输出2.4。4. C23this关键字的扩展在 C23 中this关键字的用法扩展允许在成员函数中作为参数来引用当前对象。原来this只是一个指向当前对象的指针但通过this auto self它被当作一个通用引用完美转发传递这样self可以在函数体内作为一个普通对象使用。这种扩展的this语法允许我们在基类中定义操作派生类对象的函数从而实现基类通用方法的功能同时仍然保持派生类的特定实现如getValue和setValue。5. 输出分析s.scale(2.0);std::println(std::cout,s.getValue() {},s.getValue());s.scale(2.0)调用NumericalFunctions中的scale方法传入的multiplicator是2.0此时getValue返回的是1.2然后将其乘以2.0结果是2.4。接着setValue将value更新为2.4。s.getValue()返回value的当前值即2.4所以最终输出s.getValue() 2.4总结C23 中this关键字的扩展通过this auto self我们可以在成员函数中使用self来表示当前对象从而在基类中操作派生类的特定行为如访问派生类的成员函数getValue和setValue。代码复用NumericalFunctions类通过通用的scale方法让Sensitivity类及其其他派生类能够共享这一方法简化了代码的实现。完美转发auto作为通用引用能够完美转发self无论是左值还是右值都可以正确地传递。#includeiostream// 定义一个通用的数值操作功能类模板使用 CRTPCuriously Recurring Template Pattern// CRTP 的作用是允许派生类通过基类的成员函数来进行操作从而实现更好的代码复用和类型安全。structNumericalFunctions{// scale 函数可以让派生类对自身的值进行缩放。this 指针允许我们操作派生类对象。// 通过 auto self 来捕获派生类类型使得代码更加通用。voidscale(thisautoself,doublemultiplicator){// 调用派生类对象的 getValue 和 setValue 方法来进行缩放操作。self.setValue(self.getValue()*multiplicator);}voidsetValue(doublev){std::coutbase setValuevstd::endl;}};// Sensitivity 类继承自 NumericalFunctions表示具体的数值类型。structSensitivity:publicNumericalFunctions{Sensitivity(doublevalue):value(value){}// 获取当前的数值doublegetValue()const{returnvalue;}// 设置新的数值voidsetValue(doublev){valuev;}// 数据成员用于存储值doublevalue;};intmain(){// 创建一个 Sensitivity 对象初始值为 1.2Sensitivity s{1.2};// 使用 scale 函数将值缩放为原来的 2 倍s.scale(2.0);// 输出当前的值期望为 2.4std::couts.getValue() s.getValue()std::endl;}输出s.getValue() 2.4这段代码展示了静态接口和CRTPCuriously Recurring Template Pattern的一个典型例子利用 CRTP 实现了一种静态多态。下面是详细的解析代码解析1.Animal类模板templatetypenameDerivedclassAnimal{private:// ... 私有的默认构造函数和析构函数public:voidmake_sound()const{static_castDerivedconst(*this).make_sound_impl();}};Animal是一个模板类接收一个类型参数Derived这是一个子类类型通常会在继承时被指定。在make_sound()函数中使用static_castDerived const(*this)将当前对象强制转换为派生类类型 (Derived)。然后调用派生类的make_sound_impl()方法。这是静态多态的关键编译器在编译时会根据实际传入的Derived类型来解析具体调用的方法。这种方式避免了运行时的虚函数表查找即避免了运行时多态通过静态的类型推断实现了“静态接口”的效果。2.Sheep类classSheep:publicAnimalSheep{public:voidmake_sound_impl()const{std::coutbaa;}};Sheep继承了AnimalSheep类并实现了make_sound_impl()方法这是派生类需要提供的接口实现。make_sound_impl()方法打印出 “baa”表示羊的叫声。3.main函数intmain(){Sheep sheep;AnimalSheepanimalsheep;sheep.make_sound();animal.make_sound();}在main函数中创建了一个Sheep对象sheep。通过AnimalSheep animal sheep;我们将Sheep类型的对象赋给了AnimalSheep类型的引用animal。注意animal是一个父类类型引用但它绑定的是派生类对象因此仍然能调用Sheep中的make_sound_impl()方法。sheep.make_sound()和animal.make_sound()都会调用Sheep中的make_sound_impl()方法从而打印出 “baa”。总结这段代码通过CRTP模式实现了静态多态具体做法是Animal类作为基类模板接收一个派生类Derived。make_sound()方法调用Derived类的实现从而避免了运行时虚函数的开销。Sheep类实现了自己的声音方法继承自AnimalSheep。通过这种方式Animal类并不需要知道具体的派生类实现而是通过模板参数传递信息使得派生类能够提供自己的实现同时在编译时决定调用哪个方法避免了运行时的开销。数学公式虽然这段代码并没有涉及具体的数学公式但你可以按照下面的方式理解它AnimalSheep可以看作是一个通用模板接受Sheep类型作为参数。make_sound_impl()可以视为一个方法实现的“接口”其中的static_castDerived 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()这种设计方式提供了一种更灵活、更高效的多态机制同时保证了类型安全和性能优化。在这段代码中展示了 C23 中的一种新方式通过在成员函数中使用this指针来实现静态接口Static Interface。代码解析1.Animal类模板classAnimal{public:templatetypenameSelfvoidmake_sound(thisSelfconstself){self.make_sound_impl();}};这个Animal类提供了一个成员函数make_sound()该函数是一个模板函数它接收一个类型参数Self。this Self const self是一个 C23 的新特性意味着该成员函数是通过this指针来访问当前对象的。也就是让Self类型被自动推断为当前对象的类型。函数体中self.make_sound_impl()会调用当前对象的make_sound_impl()方法。这是一个静态接口的实现方式类似于 CRTP 模式不过这里没有显式地依赖于继承关系而是通过模板和this指针来实现。2.Sheep类classSheep:publicAnimal{public:voidmake_sound_impl()const{std::coutbaa;}};Sheep类继承自Animal类并实现了make_sound_impl()方法。该方法打印出 “baa” 表示羊的叫声。3.main函数intmain(){Sheep sheep;Animalanimalsheep;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的类型尤其是当我们使用基类引用时因此编译器无法正确地推断Self为Sheep类型。问题2当animal.make_sound()被调用时animal是一个Animal类型的引用它并不知道Sheep的实际类型。编译器无法根据基类引用推断出具体的派生类Sheep类型因此也无法推断出Self类型。这导致无法编译。解决方案为了让编译通过可以使用虚函数或者明确指定模板类型来解决类型推断问题。以下是两种可能的解决方式1.使用虚函数动态多态classAnimal{public:virtualvoidmake_sound()const0;// 纯虚函数};classSheep:publicAnimal{public:voidmake_sound()constoverride{std::coutbaa;}};通过在Animal类中声明一个纯虚函数make_sound()然后在Sheep类中实现该函数我们可以在运行时根据对象的实际类型来调用正确的make_sound()方法。2.显式传递模板类型templatetypenameSelfvoidmake_sound(Selfself){self.make_sound_impl();}classSheep{public:voidmake_sound_impl()const{std::coutbaa;}};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)Self→make_sound_impl()在调用时Self应该被推导为具体的类型Sheep从而调用make_sound_impl()方法。不过由于 C 并没有自动推断基类引用类型为派生类的能力因此编译器无法正确推导出Self为Sheep导致编译错误。总结这段代码展示了通过this指针和模板函数来实现静态接口的想法但由于类型推导的限制在基类引用上无法成功推断出实际类型导致编译错误。可以通过虚函数或显式传递模板类型来解决这个问题。CRTP 的两种形式1.CRTP 用于静态接口Static Interface功能这种形式的 CRTP 提供了一个基类用于一组相关的类型或类型族。它定义了一个共同的接口并通过基类接口进行使用。设计模式这种用法引入了抽象属于设计模式的范畴。通过静态多态基类不需要知道派生类的实现派生类通过 CRTP 提供特定的功能。应该称之为“静态接口”Static Interface。这类静态接口 CRTP 的示例代码如下templatetypenameDerivedclassAnimal{public:voidmake_sound()const{static_castconstDerived*(this)-make_sound_impl();}};classSheep:publicAnimalSheep{public:voidmake_sound_impl()const{std::coutbaa;}};intmain(){Sheep sheep;sheep.make_sound();// baa}在这个例子中Animal类提供了一个静态接口而Sheep类提供了具体的实现。make_sound()通过静态多态调用make_sound_impl()这就是“静态接口”模式。2.CRTP 用于功能添加Mixin功能这种形式的 CRTP 主要提供派生类的实现细节并不定义公共接口。通过 CRTP基类可以提供一些默认的行为或功能而派生类只需继承即可。非设计模式这种用法没有引入抽象也不属于设计模式因为它只是为了复用代码而没有设计出一个统一的接口。应该称之为“Mixin”。在这种情况下Mixin主要是为了给派生类添加功能而不是为了定义一个公共接口。例如templatetypenameDerivedclassLogging{public:voidlog(conststd::stringmessage){static_castDerived*(this)-log_impl(message);}};classMyClass:publicLoggingMyClass{public:voidlog_impl(conststd::stringmessage){std::coutLog: messagestd::endl;}};intmain(){MyClass obj;obj.log(This is a log message);}在这个例子中Logging类并没有定义一个公共接口而是作为一个Mixin给MyClass提供了日志功能。派生类只需要提供log_impl的实现即可。对这两种形式的区分静态接口Static Interface这是一种设计模式通常用于定义一组相关类型的公共接口基类通过 CRTP 提供接口而派生类提供具体实现。它通过静态多态在编译时确定调用的方法。Mixin这是一个功能性代码复用的方式基类通过 CRTP 提供默认的实现或行为但没有引入抽象接口。派生类仅继承该基类并补充特定的实现。准则静态接口当你打算创建一个静态类型家族并且希望定义一个公共的接口时应该使用 “静态接口”Static Interface的术语。这种形式是设计模式的一部分主要用于接口的统一定义。Mixin如果你只是打算继承实现细节并希望复用功能而不需要引入抽象接口可以使用 “Mixin” 的术语。Mixin 更多的是代码复用而非设计模式。数学公式类比可以用数学公式来帮助理解这两种形式的区别静态接口Static Interface可以表示为Static InterfaceInterface Definition,Implemented by Derived Types \text{Static Interface} { \text{Interface Definition}, \text{Implemented by Derived Types} }Static InterfaceInterface Definition,Implemented by Derived Types其中接口定义在基类中而具体实现由派生类提供利用静态多态机制进行调用。Mixin可以表示为MixinImplementation Details,Inherited by Derived Types \text{Mixin} { \text{Implementation Details}, \text{Inherited by Derived Types} }MixinImplementation Details,Inherited by Derived Types在这种形式中基类提供的是实现细节派生类继承并补充具体的实现且没有引入接口层的抽象。总结“静态接口”Static Interface是一种设计模式它通过 CRTP 定义了类型族的公共接口依赖于静态多态。“Mixin”是一种功能扩展的方式它通过 CRTP 为派生类提供实现细节而不引入抽象接口。当你想定义一组有共同接口的类型时使用“静态接口”当你希望继承实现细节而非接口时使用“Mixin”。这种区分有助于减少对 CRTP 术语的歧义使得代码的意图更加明确。1.C20 Concepts在 C20 中Concepts是一种新的机制允许对模板参数进行约束确保它们满足特定的条件。这样就可以在编译时进行类型检查避免出现不匹配的类型。templatetypenameTconceptAnimalrequires(T animal){animal.make_sound();};这里定义了一个Animalconcept它要求类型T必须有一个make_sound()成员函数。具体来说requires语句表示类型T必须能够成功地调用animal.make_sound()这就是对T类型进行约束的方式。这种方式的优势在于通过Concepts可以清晰地表达类型的需求减少模板类型参数错误的发生。2.使用 Concept 的print函数templateAnimal Tvoidprint(Tconstanimal){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::coutbaa;}};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 模式要求派生类显式地继承基类并实现基类中的接口。例如AnimalSheep就显式地要求Sheep类型来提供实现。Concepts允许隐式选择即任何实现了特定方法的类型都能满足 concept 的要求不需要显式地声明继承关系。这使得Sheep仅仅通过满足Animalconcept 的要求就能够传入print()函数而没有显式的父类关系。数学公式类比假设我们要定义一个接口或功能的约束静态接口Static Interface可以表示为Static InterfaceBase Class,Derived Classes Explicitly Opt-in \text{Static Interface} { \text{Base Class}, \text{Derived Classes Explicitly Opt-in} }Static InterfaceBase Class,Derived Classes Explicitly Opt-in这里基类作为接口派生类显式继承并实现它体现了显式选择。Concepts则可以表示为ConceptsType Constraint,Implicit Opt-in \text{Concepts} { \text{Type Constraint}, \text{Implicit Opt-in} }ConceptsType Constraint,Implicit Opt-in这里类型只要满足特定的约束例如拥有make_sound()方法就能被接受体现了隐式选择。总结静态接口Static Interface通过 CRTP 强制类型显式选择加入属于一种设计模式强调接口的抽象和类型家族的构建。Concepts使得类型的约束变得更为灵活和简洁。它允许任何满足要求的类型都能够加入而不需要显式的继承或基类的设计。CRTP更侧重于类型之间的关系和设计模式适合在需要显式控制继承关系和接口时使用。而Concepts则提供了一种更松散、灵活的方式来约束类型适用于那些没有强继承关系的场景。1.AnimalTag标签类classAnimalTag{};这里定义了一个空的类AnimalTag它作为一个标记类tag class使用。它本身并不包含任何功能只是为了作为类型的一部分帮助区分哪些类型是可以作为 “Animal” 的类型。这种做法类似于 CRTP 中的 “Tag Dispatching”通过特定的标签类来标识类型进而让编译器知道该类型是否符合某些约束。2.定义AnimalConcepttemplatetypenameTconceptAnimalrequires(T animal){animal.make_sound();}std::derived_fromT,AnimalTag;Animal是一个Concept它用于约束传入的类型T。要满足Animal的要求类型T必须拥有make_sound()方法这通过requires子句来检查。继承自AnimalTag使用std::derived_fromT, AnimalTag。这一点确保只有显式声明为动物的类型才会符合该概念。这里std::derived_fromT, AnimalTag是 C20 中的一个标准库工具它检查类型T是否是AnimalTag的派生类。通过这样的设计只有显式继承了AnimalTag的类型才能满足Animal概念因此能够保证类型明确地选择加入Animal的接口。3.print函数templateAnimal Tvoidprint(Tconstanimal){animal.make_sound();}print()函数的模板参数T被约束为必须满足Animalconcept即T必须有make_sound()方法并且继承自AnimalTag。在print()函数中我们调用了animal.make_sound()因此只有符合Animal概念的类型才会被接受并执行此操作。4.Sheep类classSheep:publicAnimalTag{public:voidmake_sound()const{std::coutbaa;}};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。主要要点和总结显式选择加入与 CRTP 的不同之处在于类型如Sheep显式地通过继承AnimalTag来表明自己是一个符合Animal概念的类型。这是一种显式的 opt-in即明确声明自己符合某种约束。这与 CRTP 中通过继承基类来实现静态接口的方式有所不同。Animal概念Animal概念要求类型T必须实现make_sound()方法并且继承自AnimalTag。这保证了只有显式选择加入的类型才能成为Animal避免了误用或错误类型的传递。静态接口实现通过Concepts我们可以更加灵活地定义静态接口而不需要显式的基类继承关系。这使得代码更加简洁和易于理解且避免了冗长的继承链。与 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-inConcepts通过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结论通过ConceptsC20 提供了一种新的方式来实现静态接口避免了传统的 CRTP 所带来的冗长继承链和强制的类型关系。通过Concepts类型只需要符合约束条件如实现make_sound()和继承AnimalTag即可简化了类型的选择过程。这种方法避免了强制的继承并提供了更灵活、简洁的接口设计方式。主要准则1.“Static Interface” 用于表示静态类型族的创建解释当我们打算创建一组相关的类型并且希望它们共享一个公共接口时应该使用“Static Interface”这个术语。这种方式通常通过CRTPCuriously Recurring Template Pattern实现。设计模式Static Interface是一种设计模式通过静态多态实现类型家族的统一接口。在这种模式下基类定义了接口而具体的实现则由派生类提供。数学公式类比Static InterfaceBase 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 InterfaceBase 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 或其他方式来实现但它并不强制要求类型显式实现某个公共接口。数学公式类比MixinBase Class with Implementation Details,Derived Classes Inheriting Behavior \text{Mixin} { \text{Base Class with Implementation Details}, \text{Derived Classes Inheriting Behavior} }MixinBase Class with Implementation Details,Derived Classes Inheriting BehaviorMixin强调了类型之间通过继承共享实现细节而不是接口。4.Mixin 不是设计模式而是实现细节解释Mixin是一种代码复用机制通常被认为是实现细节而非设计模式。它通过将特定的实现行为提取到基类中使得多个派生类能够共享这些行为而无需重新实现。显式对象参数作为替代方案Explicit Object Parameters可以用来代替 Mixin因为它允许在函数调用时明确传递对象参数从而避免继承的复杂性。显式传递对象可以在某些情况下取代 Mixin 的设计尤其是在不需要复杂继承体系时。数学公式类比Explicit Object ParametersFunction 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 ParametersFunction with Object Passed as Parameter,No Inheritance Involved这表明显式对象参数只是将对象作为参数传递避免了继承和类型之间的依赖。总结“Static Interface”当我们希望在代码中定义一组相关类型并且希望它们共享一个公共接口时应该使用Static Interface。这种方式是一个设计模式通常通过CRTP来实现强调基类提供接口派生类提供实现。“Mixin”当我们希望通过继承共享实现细节时使用Mixin。这种方式是一个实现细节用于代码复用和功能扩展而不是定义接口。它更关注的是将实现细节通过继承的方式复用而不是定义明确的接口。显式对象参数的替代性在某些情况下显式对象参数可以替代Mixin。通过直接传递对象而不是依赖继承可以实现类似的功能扩展特别是在不需要复杂继承关系时。数学公式总结Static Interface强调基类提供接口派生类提供实现Static InterfaceBase 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 InterfaceBase Class with Common Interface,Derived Classes Implementing the InterfaceMixin强调基类提供实现细节派生类共享行为MixinBase Class with Implementation Details,Derived Classes Inheriting Behavior \text{Mixin} { \text{Base Class with Implementation Details}, \text{Derived Classes Inheriting Behavior} }MixinBase Class with Implementation Details,Derived Classes Inheriting Behavior显式对象参数可以用来替代Mixin避免继承的复杂性Explicit Object ParametersFunction 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 ParametersFunction with Object Passed as Parameter,No Inheritance Involved通过这些准则和数学公式我们能够更清晰地理解Static Interface和Mixin在设计中的不同角色并能够根据需要选择合适的设计方式。Mixin是一种常用于代码重用的技术通常用于将某些功能通过继承的方式引入到类中而不需要强制要求类继承某个共同的接口或基类。在 C 中Mixin类通常是一个不定义接口的基类而是提供一些功能性的实现细节。派生类可以选择继承这些实现添加或覆盖某些功能。下面是几个Mixin的示例展示如何通过继承实现细节来增强类的功能。1.日志功能 Mixin假设我们需要为多个类添加日志功能我们可以创建一个日志 Mixin 类将日志功能的实现放在基类中派生类只需要继承即可。#includeiostream#includestring// Mixin 类提供日志功能classLogging{public:voidlog(conststd::stringmessage)const{std::coutLog: messagestd::endl;}};// 一个具体的类继承了 Logging MixinclassFileHandler:publicLogging{public:voidopenFile(conststd::stringfilename){log(Opening file: filename);// 其他文件操作}};// 另一个具体的类继承了 Logging MixinclassNetworkHandler:publicLogging{public:voidconnect(conststd::stringserver){log(Connecting to server: server);// 其他网络操作}};intmain(){FileHandler fileHandler;fileHandler.openFile(data.txt);NetworkHandler networkHandler;networkHandler.connect(127.0.0.1);return0;}解释Logging是一个Mixin类它提供了一个log()方法用于记录日志。FileHandler和NetworkHandler继承了Logging并分别实现了文件操作和网络操作的功能。FileHandler和NetworkHandler通过继承Logging来复用日志功能而不需要显式实现日志功能。2.计时功能 Mixin我们可以使用Mixin为类添加计时功能记录操作的耗时。#includeiostream#includechrono#includethread// Mixin 类提供计时功能classTimer{public:voidstartTimer(){start_timestd::chrono::high_resolution_clock::now();}voidstopTimer(){autoend_timestd::chrono::high_resolution_clock::now();std::chrono::durationdoubledurationend_time-start_time;std::coutElapsed time: duration.count() secondsstd::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来实现。#includeiostream#includecmath// Mixin 类提供数学运算功能classMathOperations{public:doublesquare(doublevalue)const{returnvalue*value;}doublesquareRoot(doublevalue)const{returnstd::sqrt(value);}};// 具体的类继承 MathOperations MixinclassCalculator:publicMathOperations{public:voidcalculate(){doublex9;doubleresult1square(x);doubleresult2squareRoot(x);std::coutSquare of x is: result1std::endl;std::coutSquare root of x is: result2std::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是 C17 引入的标准库模板它允许我们定义一个可以存储多种类型的变量。这个特性在设计可扩展的系统时非常有用尤其是在面临多种可能类型的情况下。比如在绘制形状的问题中我们可能有很多不同的形状如圆形、矩形等而使用std::variant可以帮助我们将这些形状统一表示。如何使用std::variant解决问题假设我们需要绘制不同类型的形状我们可以通过std::variant来表示形状的类型。以下是一个简化的示例#includeiostream#includevariant#includevector// 定义不同的形状structCircle{doubleradius;};structRectangle{doublewidth;doubleheight;};// 使用 std::variant 来表示一个形状可以是 Circle 或 RectangleusingShapestd::variantCircle,Rectangle;// 绘制函数根据不同的形状类型绘制voiddraw(constShapeshape){std::visit([](autos){usingTstd::decay_tdecltype(s);ifconstexpr(std::is_same_vT,Circle){std::coutDrawing a Circle with radius s.radiusstd::endl;}elseifconstexpr(std::is_same_vT,Rectangle){std::coutDrawing a Rectangle with width s.width and height s.heightstd::endl;}},shape);}intmain(){// 创建不同的形状Circle circle{5.0};Rectangle rectangle{10.0,20.0};// 将形状存入 std::variantstd::vectorShapeshapes{circle,rectangle};// 绘制所有形状for(constautoshape:shapes){draw(shape);}return0;}解析形状定义我们定义了两种形状Circle和Rectangle。std::variant使用使用std::variantCircle, Rectangle来表示一个形状类型Shape可以是Circle或Rectangle。std::visitstd::visit用来访问std::variant中的具体类型并根据类型执行不同的逻辑。在这个例子中我们根据是Circle还是Rectangle来调用不同的绘制逻辑。可扩展性如果未来我们想增加新的形状例如Triangle只需在std::variant中添加新类型并在std::visit中添加相应的处理逻辑。数学公式类比假设我们有一个形状类型可以表示多种类型的形状。我们可以通过以下方式表示它ShapeCircle,Rectangle,Triangle,… \text{Shape} { \text{Circle}, \text{Rectangle}, \text{Triangle}, \dots }ShapeCircle,Rectangle,Triangle,…在这个表示中Shape可以是多种形状之一通过std::variant来表示这种多态关系。总结需求分析在一个庞大、团队协作的系统中设计一个可扩展的绘制系统非常重要。需要通过抽象和可扩展的方式管理不同类型的形状。std::variant是一种处理多种类型的简洁方式能够有效地支持不同形状类型的扩展。可扩展性通过使用std::variant和std::visit系统可以轻松地添加新类型而不需要修改现有代码库中的大量逻辑。这种设计方式保证了系统的灵活性和可扩展性并能够随着需求的变化轻松增加新功能。展示了一个典型的面向对象Object-Oriented解决方案用于实现不同形状的绘制。解决方案使用了策略模式Strategy Pattern和多态性来实现灵活的绘制行为。接下来我会逐行解析这段代码及其设计思想。1.DrawStrategy 类templatetypenameConcreteShapeclassDrawStrategy{public:virtual~DrawStrategy()default;virtualvoiddraw(ConcreteShapeconstshape)const0;};目的DrawStrategy是一个抽象策略类它定义了一个接口draw()这个接口将被不同的具体形状类实现。每个具体形状的绘制方式是不同的因此我们把绘制的行为封装在一个策略类中。泛型设计DrawStrategy是一个模板类ConcreteShape是其模板参数。ConcreteShape代表具体的形状类比如Circle、Rectangle等它将会被用来为每种形状提供具体的绘制实现。draw()方法纯虚函数draw()所有派生类都必须实现该方法来定义如何绘制具体的形状。2.Shape 类classShape{public:virtual~Shape()default;virtualvoiddraw()const0;// ... several other virtual functions};目的Shape类是一个抽象基类它定义了所有形状类的共同接口其他具体形状类如Circle将继承自Shape并实现自己的绘制逻辑。draw()方法纯虚函数要求每个具体的形状类都提供自己的draw()实现。通过这种方式可以利用多态性来在运行时决定绘制哪种形状。3.Circle 类classCircle:publicShape{public:Circle(doublerad,std::unique_ptrDrawStrategyCircleds):radius{rad},drawer{std::move(ds)}{}doublegetRadius()const;// ... getCenter(), getRotation(), ...};继承自ShapeCircle是Shape的派生类表示具体的圆形。它必须实现draw()方法通过DrawStrategy来委托绘制逻辑。构造函数Circle类通过构造函数接收半径 (rad) 和一个DrawStrategyCircle的独特指针 (std::unique_ptrDrawStrategyCircle ds)。drawer是指向绘制策略的指针策略类负责提供draw()方法的具体实现。drawer成员drawer成员保存了一个DrawStrategyCircle类型的策略对象它决定了如何绘制圆形。std::move(ds)表示将外部传入的指针转移到Circle对象中避免不必要的复制。关键设计思想策略模式Strategy PatternDrawStrategy类就是典型的策略模式。这个模式允许我们在运行时选择不同的绘制策略并使得不同形状的绘制行为可以独立变化。比如圆形可以使用不同的绘制方法如通过 SVG 绘制、通过 OpenGL 绘制等而不会影响其他形状的绘制。依赖注入Dependency Injection在构造Circle对象时我们通过构造函数注入了一个具体的绘制策略。这样Circle类并不需要直接知道绘制的细节它只需要依赖于DrawStrategy提供的接口。4.如何使用此设计模式在这种设计中绘制的实现与形状的定义是分离的这使得代码更加灵活和可扩展。如果将来想添加新的绘制策略比如使用不同的渲染引擎我们只需要创建新的DrawStrategy类而不需要修改原有的Shape或Circle类。这种开闭原则对扩展开放对修改封闭是面向对象设计中的一个核心原则。5.绘制行为的多态性由于Circle继承自Shape并实现了draw()方法而draw()方法的实现由DrawStrategyCircle提供我们可以通过以下方式来利用多态性绘制不同的形状std::unique_ptrDrawStrategyCirclecircleDrawerstd::make_uniqueCircleDrawer();// CircleDrawer 是具体的绘制策略Circlecircle(10.0,std::move(circleDrawer));circle.draw();// 调用具体的绘制策略来绘制圆形在上述代码中circle的绘制行为由CircleDrawer类决定这个策略类实现了DrawStrategyCircle接口。6.如何扩展这种设计非常容易扩展。假设我们想增加一个新的形状例如Rectangle只需要创建一个DrawStrategyRectangle类来实现矩形的绘制。创建一个Rectangle类继承自Shape并在构造时注入一个DrawStrategyRectangle对象。使用相应的绘制策略来绘制矩形。classRectangle:publicShape{public:Rectangle(doublew,doubleh,std::unique_ptrDrawStrategyRectangleds):width(w),height(h),drawer(std::move(ds)){}voiddraw()constoverride{drawer-draw(*this);// 使用相应的策略绘制矩形}private:doublewidth,height;std::unique_ptrDrawStrategyRectangledrawer;};数学公式类比假设每个形状的绘制策略是一个函数映射我们可以将不同形状的绘制方式表示为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 类templatetypenameConcreteShapeclassDrawStrategy{public:virtual~DrawStrategy()default;virtualvoiddraw(ConcreteShapeconstshape)const0;};目的DrawStrategy是一个抽象策略类它为每个具体的形状提供绘制接口。它使用模板使得每种具体的形状例如Circle或Square都有自己的绘制策略。实现每个具体形状将实现该策略的draw()方法具体的绘制细节将由DrawStrategyConcreteShape提供。2.Shape 类classShape{public:virtual~Shape()default;virtualvoiddraw()const0;// ... several other virtual functions};目的Shape是所有具体形状类的基类。它定义了所有形状类的共同接口特别是draw()方法它是纯虚函数要求派生类提供具体实现。可扩展性通过继承Shape类未来可以轻松地添加新的形状类型而不需要更改现有的代码。3.Circle 类classCircle:publicShape{public:Circle(doublerad,std::unique_ptrDrawStrategyCircleds):radius{rad},drawer{std::move(ds)}{}doublegetRadius()const;voiddraw()constoverride;// ... several other virtual functionsprivate:doubleradius;std::unique_ptrDrawStrategyCircledrawer;};目的Circle类继承自Shape并提供了一个构造函数该构造函数接受一个半径和一个DrawStrategyCircle类型的策略对象。drawerdrawer是DrawStrategyCircle类型的指针它委托给该策略来实现圆形的绘制。通过使用std::unique_ptr确保了策略对象的所有权由Circle管理。draw()Circle类重写了Shape类的draw()方法并将绘制的实际逻辑委托给drawer对象。4.Square 类classSquare:publicShape{public:Square(doubles,std::unique_ptrDrawStrategySquareds):side{s},drawer{std::move(ds)}{}doublegetSide()const;voiddraw()constoverride;// ... several other virtual functionsprivate:doubleside;std::unique_ptrDrawStrategySquaredrawer;};目的Square类与Circle类类似也继承自Shape并实现了draw()方法。构造函数接受边长side和一个绘制策略对象。扩展性如果未来需要增加更多形状只需要继承Shape类并实现draw()方法即可。5.ShapesFactory 类classShapesFactory{public:virtual~ShapesFactory()default;virtualShapescreate(std::string_view filename)const0;};目的ShapesFactory是一个工厂类负责根据文件中的数据动态创建形状实例。通过该类可以将形状的创建与实际使用解耦。功能create()方法读取文件并返回一组形状对象这样就实现了从外部输入数据到内部对象创建的映射。6.绘制所有形状的函数voiddrawAllShapes(Shapesconstshapes){for(autoconsts: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_fileshape){if(shapecircle){doubleradius;shape_fileradius;shapes.emplace_back(std::make_uniqueCircle(radius,std::make_uniqueOpenGLDrawer()));}elseif(shapesquare){doubleside;shape_fileside;shapes.emplace_back(std::make_uniqueSquare(side,std::make_uniqueOpenGLDrawer()));}else{break;}}returnshapes;}};目的YourShapesFactory继承自ShapesFactory实现了create()方法根据文件内容动态创建不同类型的形状。该工厂读取一个文件其中描述了每个形状的类型及其参数如半径、边长等然后返回一个形状对象列表。灵活性如果以后需要支持更多形状只需在create()方法中添加相应的判断和创建代码。8.绘制所有形状并从文件创建voidcreateAndDrawShapes(ShapesFactoryconstfactory,std::string_view filename){Shapes shapesfactory.create(filename);drawAllShapes(shapes);}目的createAndDrawShapes()函数通过工厂创建所有形状并将其传递给drawAllShapes()进行绘制。此函数简化了创建和绘制形状的过程。9.OpenGLDrawer 类classOpenGLDrawer:publicDrawStrategyCircle,publicDrawStrategySquare{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voiddraw(Circleconstcircle)constoverride;voiddraw(Squareconstsquare)constoverride;private:// ... Data members (color, texture, transparency, ...)};目的OpenGLDrawer是一个具体的绘制策略类它分别为Circle和Square提供绘制实现。这是一个多重继承的例子OpenGLDrawer同时继承了DrawStrategyCircle和DrawStrategySquare使得它能够为不同形状提供绘制策略。总结面向对象设计的优点通过策略模式和工厂模式的结合系统提供了非常高的灵活性和扩展性。新的形状可以通过继承Shape类并通过DrawStrategy实现绘制逻辑。工厂模式ShapesFactory负责形状对象的创建这让我们能够通过外部文件动态配置需要绘制的形状类型而无需在程序中硬编码。策略模式DrawStrategy模式让我们能够为不同形状提供不同的绘制策略保持了高度的可扩展性。例如新增一个形状如Triangle时只需继承Shape类并实现自己的绘制策略。可扩展性若未来想要添加更多的形状或绘制策略只需要添加新的类和方法不会破坏现有的代码结构。实际应用这种设计适合用于需要高灵活性和可扩展性的图形系统中例如绘图软件、游戏引擎或可视化应用等。经典面向对象解决方案分析展示了一个典型的面向对象设计模式结合了策略模式、工厂模式和多态性来实现图形形状的绘制。它的目标是通过可扩展的设计来处理不同类型的形状并且能够根据需求灵活地增加新的形状类型。以下是对代码各部分的详细分析1.drawAllShapes函数voiddrawAllShapes(Shapesconstshapes){for(autoconsts:shapes){s-draw();}}目的drawAllShapes()函数遍历所有的形状对象并调用它们的draw()方法。由于每个形状都有自己独特的绘制方式因此draw()方法会根据具体的形状类型调用对应的绘制策略。多态性该函数展示了多态性即不同类型的形状例如Circle、Square、Rectangle都可以通过基类指针 (Shape) 调用draw()方法实际调用的是具体类型的draw()实现。2.createAndDrawShapes函数voidcreateAndDrawShapes(ShapesFactoryconstfactory,std::string_view filename){Shapes shapesfactory.create(filename);drawAllShapes(shapes);}目的该函数通过工厂类ShapesFactory从文件中读取形状信息创建形状对象的集合并使用drawAllShapes()进行绘制。解耦这个函数解耦了形状的创建和绘制使得可以独立扩展形状类型和绘制方式。例如如果需要改变形状的创建方式或增加新的绘制策略可以轻松修改工厂或策略而不需要修改其他部分的代码。3.OpenGLDrawer类classOpenGLDrawer:publicDrawStrategyCircle,publicDrawStrategySquare,publicDrawStrategyRectangle{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voiddraw(Circleconstcircle)constoverride;voiddraw(Squareconstsquare)constoverride;voiddraw(Rectangleconstrectangle)constoverride;private:// ... Data members (color, texture, transparency, ...)};目的OpenGLDrawer类是具体的绘制策略类继承了DrawStrategy模板类并为Circle、Square、Rectangle提供了不同的绘制实现。多重继承通过多重继承OpenGLDrawer实现了为多个形状提供绘制策略的功能这是一种将具体实现与抽象绘制行为分离的方式。数据成员OpenGLDrawer还可以包含一些与绘制相关的额外数据如颜色、纹理、透明度等这些可以在绘制时使用。4.Rectangle类classRectangle:publicShape{public:Rectangle(doublewidth,doubleheight,std::unique_ptrDrawStrategyRectangledrawer):width_{width},height_{height},drawer_{std::move(drawer)}{}doublewidth()const{returnwidth_;}doubleheight()const{returnheight_;}voiddraw()constoverride{drawer_-draw(*this);}private:doublewidth_;doubleheight_;std::unique_ptrDrawStrategyRectangledrawer_;};目的Rectangle类继承自Shape类表示一个矩形形状。与Circle和Square类似它也拥有自己的绘制策略drawer_。绘制策略通过接受一个DrawStrategyRectangle类型的对象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_fileshape){if(shapecircle){// ... Creating a circle}elseif(shapesquare){// ... Creating a square}elseif(shaperectangle){doublewidth,height;shape_filewidthheight;shapes.emplace_back(std::make_uniqueRectangle(width,height,std::make_uniqueOpenGLDrawer()));}else{break;}}returnshapes;}};目的YourShapesFactory类继承自ShapesFactory实现了create()方法。该方法根据输入文件中的形状描述动态创建不同类型的形状对象并将它们存储在Shapes容器中返回。灵活性如果需要支持新类型的形状只需在create()方法中添加相应的判断并创建新的形状类实例。这种方式使得工厂类高度灵活且易于扩展。6.main()函数intmain(){YourShapesFactory factory{};createAndDrawShapes(factory,shapes.txt);}目的在main()函数中使用YourShapesFactory创建形状对象并将它们绘制出来。shapes.txt文件描述了需要创建的形状类型和它们的属性如半径、边长等。高内聚低耦合这个设计保持了良好的高内聚和低耦合特性。每个模块如Shape类、Drawer类、Factory类都有清晰的职责且模块之间通过接口解耦。总结与讨论优点高扩展性通过使用策略模式和工厂模式新的形状类型可以在不修改现有代码的情况下轻松地加入到系统中。模块化设计每个类都有清晰的职责且通过接口和抽象类解耦提高了系统的可维护性和可重用性。灵活性通过工厂类动态生成形状并通过策略类管理绘制逻辑极大地提升了系统的灵活性和可配置性。缺点代码冗长虽然面向对象设计提供了很好的扩展性但同时也带来了较多的冗余代码。每新增一个形状类型几乎每个相关类都需要进行修改如ShapesFactory和Drawer。性能开销多层的继承和虚函数调用可能导致性能损失尤其是在性能敏感的环境中可能需要使用更优化的设计。这种设计模式适合于大型、复杂的系统例如图形软件、游戏引擎和可视化应用等能够平衡扩展性和灵活性。在实际使用时根据项目的规模和需求可以进一步优化或简化设计。《The Fallen Paradigm》C现代设计的转变与应用这部分代码中我们看到一种完全现代化的 C 设计方式通过std::variant替代了传统的面向对象编程OOP和继承体系彻底简化了代码结构并避免了很多遗留问题。下面是对这种现代化设计方式的详细解读1.“Fallen Paradigm” 和 OOP 的反思引用中的一位未知评论者提出了一个批判性观点传统的面向对象编程尤其是继承的使用已经被高估。C 语言本身有强大的模板系统和std::variant这使得大多数依赖继承的设计变得不再必要。模板和std::variant提供了比传统的继承体系更灵活、更高效的设计方式。对象的多态性可以通过std::variant来替代而不需要定义复杂的继承关系。2.传统的面向对象解决方案与std::variant的替代在传统的面向对象设计中我们通常会使用继承来定义形状如Circle、Square等并通过一个基类如Shape来统一接口。而在现代 C 中使用std::variant让设计变得更加简洁。传统设计使用继承和多态classShape{public:virtualvoiddraw()const0;};classCircle:publicShape{public:voiddraw()constoverride{/* circle-specific drawing logic */}};classSquare:publicShape{public:voiddraw()constoverride{/* square-specific drawing logic */}};缺点需要处理基类指针和虚函数这可能导致性能损失。每增加一个新的形状都需要修改所有相关代码如工厂类、绘制策略等。内存管理复杂尤其是在使用多态时需要关注动态内存的分配和释放。现代 C 设计使用std::variantusingShapestd::variantCircle,Square;usingShapesstd::vectorShape;通过std::variant我们可以将不同类型的形状如Circle和Square组合在一起而不需要继承自同一个基类。std::variant提供了一个类型安全的联合体它可以存储多种类型但同时只存储一个类型的值。无需继承可以使用模板和类型推导来实现类似的功能。3.ShapesFactory和OpenGLDrawer简化工厂和绘制策略使用std::variant和函数对象Functors可以简化工厂类和绘制策略的实现。工厂类classShapesFactory{public:Shapescreate(std::string_view filename){Shapes shapes{};std::string shape{};std::ifstream shape_file{filename};while(shape_fileshape){if(shapecircle){doubleradius;shape_fileradius;shapes.emplace_back(Circle{radius});}elseif(shapesquare){doubleside;shape_fileside;shapes.emplace_back(Square{side});}else{break;}}returnshapes;}};简化不需要为每个形状定义一个工厂类或添加工厂方法来创建每个具体类型的实例。灵活性只需通过std::variant存储多种类型代码更加简洁易于扩展。绘制策略classOpenGLDrawer{public:explicitOpenGLDrawer(/* ... */){}voidoperator()(Circleconstcircle)const{/* OpenGL drawing logic for Circle */}voidoperator()(Squareconstsquare)const{/* OpenGL drawing logic for Square */}};函数对象Functors通过operator()可以在运行时动态调度各种形状的绘制逻辑。使用std::variant不同类型的形状和对应的绘制逻辑可以通过std::visit来访问和调用。4.drawAllShapes和createAndDrawShapes现代绘制流程voiddrawAllShapes(Shapesconstshapes,Drawer drawer){for(autoconstshape:shapes){std::visit([](autod,autos){d(s);},drawer,shape);}}voidcreateAndDrawShapes(Factory factory,std::string_view filename,Drawer drawer){Shapes shapesstd::visit([filename](autof){returnf.create(filename);},factory);drawAllShapes(shapes,drawer);}std::visit它是std::variant提供的一个工具用来访问存储在variant中的具体类型并执行相应操作。在createAndDrawShapes中使用std::visit从工厂创建形状然后传递给drawAllShapes进行绘制。运行时派发通过std::visit可以在运行时动态选择正确的类型并执行相应的绘制操作而不需要使用虚函数。5.总结std::variant替代继承的优势简化代码避免了继承链的复杂性减少了基类和派生类之间的紧密耦合。减少内存管理不再需要手动管理指针的生命周期std::variant提供了更简洁的内存管理方式。提高灵活性使用std::variant我们可以灵活地添加新的形状类型而不需要修改现有的代码结构。类型安全std::variant保证了类型安全在编译时捕获类型错误而不像传统的多态方法那样容易出错。现代 C 的优势与挑战优势通过std::variant和函数对象可以实现更简洁和高效的代码。对于需要频繁扩展功能的场景现代 C 提供的模板和变体提供了更强的可扩展性。挑战对于初学者而言std::variant和std::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.0Clang 18.1.4以及在以下配置下Intel Core i78核3.8 GHz64 GB 主内存测试结果经典OOP方法vsstd::variantvsmpark::variant结果显示std::variant在性能上显著优于传统的OOP方法。在执行相同数量的操作时std::variant方法的性能表现明显更好。性能比较的结果图示GCC 13.2.0和Clang 18.1.4编译器中std::variant的执行时间远低于经典的OOP方法。3.核心代码解析通过std::variant和std::visit的结合我们能够简洁地处理形状和其绘制操作而无需使用多层继承和虚函数。形状类定义usingShapestd::variantCircle,Square;usingShapesstd::vectorShape;通过std::variant我们能够存储不同类型的形状如圆形和方形而无需为每种形状创建不同的类。绘制类OpenGLDrawerclassOpenGLDrawer{public:explicitOpenGLDrawer(/*... color, texture, transparency, ...*/){}voidoperator()(Circleconstcircle)const;voidoperator()(Squareconstsquare)const;private:// ... Data members (color, texture, transparency, ...)};OpenGLDrawer是一个函数对象负责执行不同形状的绘制操作。operator()方法使其成为一个可调用对象并且通过std::variant实现多种类型的图形绘制。绘制操作voiddrawAllShapes(Shapesconstshapes,Drawer drawer){for(autoconstshape: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 shapesstd::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的一大特性允许代码在编译时生成不同类型的实例。在这里我们通过将绘制形状和创建形状的操作提取为函数模板极大地减少了代码重复并提供了更加灵活的接口。绘制形状模板templatetypenameShapes,typenameDrawervoiddrawAllShapes(Shapesconstshapes,Drawer drawer){for(autoconstshape:shapes){std::visit(drawer,shape);}}std::visit通过std::visit我们能够在不同类型的shape上调用相应的绘制方法而无需显式地检查每种类型。Drawer这个模板参数允许我们传递不同类型的绘制对象如OpenGLDrawer从而支持多种绘制方式。创建并绘制形状模板templatetypenameFactory,typenameDrawervoidcreateAndDrawShapes(Factory factory,std::string_view filename,Drawer drawer){autoshapesfactory.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::variant和std::visit的相关部分。2. 设计模式与架构选择设计模式的作用设计模式不仅仅是代码中的解决方案它们还代表了依赖结构。不同的设计模式具有不同的架构特性。理解每种设计模式背后的架构思维可以帮助我们在不同的情况下做出最佳选择。CRTP (Curiously Recurring Template Pattern)和std::variant都有其各自的优势但它们并不能直接替代虚函数。CRTP通过模板参数和编译时类型推导避免了虚函数的运行时开销但它并不适用于所有情境尤其是在需要动态多态的地方。std::variant提供了另一种处理多类型数据的方法但它缺乏虚函数所提供的真正的动态分派能力尤其是在处理需要运行时决定的多态行为时。架构决策的正确顺序首先考虑架构设计而不是实现细节。架构决定了系统如何组织和扩展因此应优先考虑。根据需求选择合适的设计模式/抽象。设计模式应根据需求进行选择而不是为了性能或简单性而盲目使用某种模式。避免性能驱动的设计。设计模式的选择不应该仅仅为了性能优化而是应基于系统的实际需求和可维护性。3. 总结虚函数、std::variant与架构选择虚函数是处理动态多态性和运行时选择操作的标准方法特别适合需要对象之间交互和动态扩展的场景。std::variant提供了一种替代继承体系的方式通过类型安全的std::visit使得对不同类型的操作变得更加灵活但它不具备虚函数的动态多态能力。架构设计应该是第一步设计模式和抽象应根据系统的需求来选定而不是盲目追求某种技术或性能的优化。