C ++ Mixin - 初始化期间的动态绑定习语

Posted

技术标签:

【中文标题】C ++ Mixin - 初始化期间的动态绑定习语【英文标题】:C++ Mixin - Dynamic Binding During Initialization idiom 【发布时间】:2020-04-28 11:43:31 【问题描述】:

我有一个类层次结构,我想使用多态性来调用正确的成员函数。 在基本层面上这是可行的,但是当我尝试使用 Mixin 类来扩展或更改某些类的功能时遇到了问题。 基本上,我想在构造从 mixin 继承的对象时对成员值进行一些验证。 (我来自python背景,很容易创建改变构造函数行为的mixin。方法解析顺序保证从构造函数调用的函数首先从派生类调用) 在 C++ 中,动态绑定在构造函数中被禁用(我理解原因)。对virtual void init() 函数的调用将不起作用,因为总是会调用基类函数。

有什么方法可以保证validate()函数的执行不需要再次显式定义构造函数?

工厂是一种方法,但我也希望构造函数具有不同的类型参数。

一个最小的例子如下所示。

谢谢你

class Base 

  Base();
  //... some virtual functions...


class Derived: public Base

  using Base::Base;
  // new constructors
  Derived(some pars);
  Derived(other pars);
  //... some virtual functions...


template <class B>
class Mixin: public B

  using B::B;
  Mixin()
   
    // mixin related constructor code
    // maybe some member validation or discretization of a continuous value
    // hides B::B(), but is only called if the default constructor is called, not for B::B(pars)
    this->validate();
  
  void validate();


class UsingMixin: public Mixin<Derived>

  using Mixin::Mixin; // inherit all constructors
  // I want to avoid defining the same constructors from Derived again,
  // since there would be no change

  // some functions


编辑: 实现这一点的一种方法是在 mixin 上使用模板化构造函数,但我不知道这种方法的安全性和可用性,因为我需要知道基类中构造函数参数的最大数量。


template <class B>
class Mixin: public B

  template <class Arg0>
  Mixin(Arg0 arg0)
      : B(arg0)
  
      this->validate();
  

  template <class Arg0, class Arg1>
  Mixin(Arg0 arg0, Arg1 arg1)
      : B(arg0, arg1)
  
      this->validate();
  

  template <class Arg0, class Arg1, class Arg2>
  Mixin(Arg0 arg0, Arg1 arg1, Arg2 arg2)
      : B(arg0, arg1, arg2)
  
      this->validate();
  


  template <class Arg0, class Arg1, class Arg2, class Arg3>
  Mixin(Arg0 arg0, Arg1 arg1, Arg2 arg2, Arg3 arg3)
      : B(arg0, arg1, arg2, arg3)
  
      this->validate();
  

  void validate()

【问题讨论】:

【参考方案1】:

您可以尝试使用可变参数模板和完美转发来创建您的 Mixin 构造函数,这样您就不必为每个可能的参数数量定义一个版本。

struct B : public A 
    template<typename... Args>
    B(Args&&... args) : A(std::forward<Args>(args)...)  this->validate(); 
;

你有没有想过给 Mixin 一个“验证器”类类型的成员变量,它的构造函数接受一个指向 Mixin 的指针,然后在上面调用validate?这样,您不需要为 Mixin 创建一个构造函数(只需为您的“验证器”成员定义一个默认初始化程序),它将在您的 Mixin 构造函数的确切时间运行。

struct B : public A 
    using A::A;
    struct V  V(B & b)  b.validate();  ;
    V v_ =  *this ;
;

使用 C++20 的 [[no_unique_address]] https://en.cppreference.com/w/cpp/language/attributes/no_unique_address,您甚至不必为拥有该空成员而支付任何内存损失。

方法解析顺序保证从 首先从派生类调用构造函数)在 C++ 中动态 绑定在构造函数中被禁用(我理解原因)。一个电话 到一个虚拟的 void init() 函数是行不通的,因为总是 将调用基类函数。

不确定你的意思。看这个例子:https://godbolt.org/z/RVSkpi

#include <iostream>
struct A 
    virtual int a()  std::cout << "A::a\n"; return 1; 
    virtual int b()  std::cout << "A::b\n"; return a(); 
;
struct B : public A 
    virtual int a()  std::cout << "B::a\n"; return 2; 
    virtual int b()  std::cout << "B::b\n"; return a(); 
    B() : a_(b())  b(); 
    int a_;
;
int main() 
    B b;
    return 0;

B 的成员的第一个构造函数执行之前(以及在A 的构造函数完成执行之后),正在构造的对象“变成”B 类型,并且一直保持这种状态直到B 的构造函数(之后它可能成为继承自 B 的其他类型)。在构造函数中,根本不需要虚拟查找,因为编译器知道类型正是B,并且可以静态解析方法调用。但它不能从b() 调用a(),因为它不仅可以从构造函数调用。但是,由于在示例中将调用 b() 时,对象的动态类型是 B,这些也将在运行时解析为对 B::a 的调用。

编辑: 如果您希望进一步派生类提供验证功能,如 cmets 中所述,并且您没有 C++20,您可以尝试以下操作:https://godbolt.org/z/r23xJv

#include <iostream>
struct A 
    A(int a) : a_(a) 
    int a_;
;
template<typename T, typename VF>
struct B : T 
    using A::A;
    struct V  V(B & b)  VF::verify(b);  ;
    V v_ =  *this ;
;
struct C : B<A, C> 
    using B::B;
    static void verify(B & b)  std::cout << b.a_ << "\n"; 
;
int main(int argc, char* argv[]) 
    C c(123);
    return 0;

【讨论】:

在你的最后一个例子中:当从B 派生C 并将virtual int b() 添加到它时,C 的构造函数仍将调用B::b() 而不是C::b()。 (除非将新的构造函数添加到 C)。这就是我说那句话的意思。其他建议听起来不错,我会看看我是否可以实施它们。您在第二个建议中提到了 C++ 再说一次,不是 100% 的意思,但这 godbolt.org/z/S4r4mL 是否显示有用的东西?调用哪个 a() 或 b() 将取决于调用时对象的动态类型,在这种情况下,它将更改 A->B->C,因为对象的部分得到构造。 “内存损失”我只是说 C++ 通常不允许对象大小为 0,因此 v_ 将占用 1 个字节,即使它是空的,沿具有填充/对齐含义,所以还不错......大多数时候。 对,对于init() 的讨论,我只是想澄清一下C::init() 将在C 的构造函数运行时被调用——也就是说,如果在构造B 部分之后,并且然后开始构造对象的实际 C 部分。我只是想指出,当构造 C 类型的对象时,对象将经历存在和对象 A、然后是 B、然后是 C 的阶段,因为每个部分的构造函数都会运行,并且虚拟调用将是相应地路由。 至于可变参数模板构造函数的运行时性能,这应该不是问题,应该是最有效的参数转发方式,只要能做出这样的陈述。跨度>

以上是关于C ++ Mixin - 初始化期间的动态绑定习语的主要内容,如果未能解决你的问题,请参考以下文章

如何在执行C ++期间动态查看堆

将控件绑定到键值对的动态集合

为啥事件处理程序只能在 IHttpModule 初始化期间绑定到 HttpApplication 事件?

为啥我应该更喜欢“显式类型的初始化程序”习语而不是显式给出类型

如何在单元测试期间使用 vue-test-utils 和 jest 模拟 mixin?

将 props 数据绑定到全局 mixin