微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何重写代码不从构造函数调用虚函数

如何解决如何重写代码不从构造函数调用虚函数

这么快就变成这个样子了

class Base
{
public:
    Base() {  setZero();}
    virtual void setZero() {std::cout << "Set all Base class values to zeros (default) values";}
};

class Derived : public Base
{
public:
    Derived () { }
    
    void setZero() override {
        Base::setZero(); 
        std::cout << "Set all Derived class values to zeros (default) values";
    }
};

setZero 是公共的,在不同的地方被调用,它也有一些逻辑,不仅仅是赋值,因为 BaseDerived 类非常大。 但这一切都没有按预期工作,因为从构造函数调用函数时动态绑定不起作用。 我看到了将代码从 setZero 复制到构造函数解决方案,但是代码重复是一件坏事。还有其他解决方案吗?

解决方法

您可能有工厂进行“电话后”,例如:

template <typename T,typename ... Ts>
T CreateBaseType(Ts&&... args)
{
    T t(std::forward<Ts>(args)...);
    t.setZero();
    return t;
}
,

TL;DR - 两阶段建设很糟糕。尽量让你的构造函数构造一些东西,而不是调用任何虚拟方法,或者需要它来运行。


如果您希望在对象构造(包括 vtables)之后进行初始化,您需要对您的对象进行单独的初始化阶段。

可能更好的处理方法是:

class Base
{
  int x = 0; // notice the =0 here
public:
  Base() {} // nothing
  virtual setZero() {*this = Base{};} // use operator= to assign zeros
};
class Derived : public Base
{
  double d = 0.; // notice the = 0. here
public:
  Derived () { } // nothing

  void setZero() override {*this = Derived{};}
};

我们也可以避免重写 setZero

template<class D,class B=void>
struct SetZero:B {
  void setZero() override {
    *static_cast<D*>(this) = D{};
  }
};
template<class D>
struct SetZero<D,void> {
  virtual void setZero() {
    *static_cast<D*>(this) = D{};
  }
};

现在我们可以:

class Base:public SetZero<Base>
{
  int x = 0; // notice the =0 here
public:
  A() {} // nothing
};
class Derived : public SetZero<Derived,Base>
{
  double d = 0.; // notice the = 0. here
public:
  Derived () { } // nothing
};

setZero 是为我们写的。

这里的 DRY 是默认构造零,我们将零放在声明变量的位置旁边。 setZero 然后只是成为一个辅助方法,可以使用默认构造的对象复制自己。

现在,在具有 vtable 的类上公开值语义复制/移动操作是一个糟糕的计划。所以你可能想要保护复制/移动并添加友元声明。

template<class D,class B=void>
struct SetZero:B {
  void setZero() override {
    *static_cast<D*>(this) = D{};
  }
  SetZero()=default;
protected:
  SetZero(SetZero&&)=default;
  SetZero& operator=(SetZero&&)=default;
  SetZero(SetZero const&)=default;
  SetZero& operator=(SetZero const&)=default;
  ~SetZero() override=default;
};

template<class D>
struct SetZero<D,void> {
  virtual void setZero() {
    *static_cast<D*>(this) = D{};
  }
  SetZero()=default;
protected:
  SetZero(SetZero&&)=default;
  SetZero& operator=(SetZero&&)=default;
  SetZero(SetZero const&)=default;
  SetZero& operator=(SetZero const&)=default;
  virtual ~SetZero()=default;
};

所以那些会变长。

BaseDerived中,因为有vtables,建议添加

protected:
  Derived(Derived&&)=default;
  Derived& operator=(Derived&&)=default;
};

阻止外部访问移动/复制构造和移动/复制分配。无论您如何编写 setZero 都建议这样做(任何此类移动/复制都将面临切片风险,因此将其公开给您班级的所有用户是一个糟糕的计划。我在这里将其设为 protected,因为setZero 依靠它来使归零 DRY。)


另一种方法是两阶段构建。在其中,我们将所有“原始”构造函数标记为受保护。

class Base {
  int x;
protected:
  Base() {} // nothing
public:
  virtual setZero() { x = 0; }
};

然后我们添加一个非构造函数构造函数:

class Base {
  int x;
protected:
  Base() {} // nothing
public:
  template<class...Ts>
  static Base Construct(Ts&&...ts){
    Base b{std::forward<Ts>(ts)...};
    b.setZero();
  }
  virtual setZero() { x = 0; }
};

和外部用户必须 Base::Construct 才能获得 Base 对象。这种很糟糕,因为我们的类型不再是正则的,但我们已经有了 vtable,这使得它首先不太可能是正则的。

我们可以对其进行 CRTP;

template<class D,class B=void>
struct TwoPhaseConstruct:B {
  template<class...Ts>
  D Construct(Ts&&...ts) {
    D d{std::forward<Ts>(ts...));
    d.setZero();
    return d;
  }
};
template<class D>
struct TwoPhaseConstruct<D,void> {
  template<class...Ts>
  D Construct(Ts&&...ts) {
    D d{std::forward<Ts>(ts...));
    d.setZero();
    return d;
  }
};

class Base:public TwoPhaseConstruct<Base> {
  int x;
protected:
  Base() {} // nothing
public:
  virtual setZero() { x = 0; }
};
class Derived:public TwoPhaseConstruct<Derived,Base> {
  int y;
protected:
  Derived() {} // nothing
public:
  virtual setZero() { Base::setZero(); y = 0; }
};

这里是兔子洞,如果你想要 make_shared 或类似的,我们必须添加一个辅助类型。

template<class F>
struct constructor_t {
  F f;
  template<std::constructible_from<std::invoke_result_t<F const&>> T>
  operator T()const&{ f(); }
  template<std::constructible_from<std::invoke_result_t<F&&>> T>
  operator T()&&{ std::move(f)(); }
};

这让我们

auto pBase = std::make_shared<Base>( constructor_t{[]{ return Base::Construct(); }} );

但是你想在兔子洞里走多远?

,

替代其他答案,将功能与 API 分开可以让您使用所需的一般流程,同时避免整个“在构造函数中使用 vtable”问题。

class Base
{
public:
    Base() {
      setZeroImpl_();
    }

    virtual void setZero() { 
      setZeroImpl_(); 
    }

private:
  void setZeroImpl_() {
    std::cout << "Set all Base class values to zeros (default) values";
  }
};

class Derived : public Base
{
public:
    Derived () {
      setZeroImpl_();
    }
    
    void setZero() override {
        Base::setZero(); 
        setZeroImpl_();
    }

private:
  void setZeroImpl_() {
    std::cout << "Set all Derived class values to zeros (default) values";
  }
};
,

如果我正确理解你的问题,那么你需要做的就是下面简单

#include <iostream>
using std::cout;
using std::endl;

class Base
{
    void init() {std::cout << "Set all Base class values to zeros (default) values" << endl;}
public:
    Base() {init(); }
    virtual void setZero() {init();}
};
class Derived : public Base
{
    void init() { std::cout << "Set all Derived class values to zeros (default) values" << endl; }
public:
    Derived () { init(); }

    void setZero() override {
        Base::setZero();
        init();

    }
};
int main()
{
    Derived d1;
    cout << endl;
    d1.setZero();
}

您为代码编写了以下语句

但这一切都没有按预期工作,因为从构造函数调用函数时动态绑定不起作用。

是的,当从基类构造函数调用 setZero() 时,虚拟行为不起作用,原因是派生类尚未构造。

您需要的是在构造每个类时对其进行初始化,这应该发生在各自的构造函数中,这就是我们在上面的代码中所做的。

基类构造函数会调用自己的setZero,派生类构造函数会调用自己的setZero。

如果您从 Derived 类派生任何其他类,您将继续做同样的事情。

,

你可以这样解决:

#include <iostream>

class Base
{
public:
    Base() {  Base::setZero();}
    virtual void setZero() {std::cout << "Set all Base class values to zeros (default) values\n";}

protected:
    Base(bool) {};
};

class Derived : public Base
{
public:
    Derived () : Base(true) { Derived::setZero(); }
    
    void setZero() override {
        Base::setZero(); 
        std::cout << "Set all Derived class values to zeros (default) values\n";
    }
};

我所做的如下:

  1. 明确哪个setZero()方法被哪个构造函数调用
  2. 也从派生构造函数中添加了对 setZero() 的调用
  3. 添加了一个受保护的 Base 构造函数,该构造函数不调用其 setZero() 方法,并从 Derived 的构造函数调用此构造函数,以便在创建 Derived 对象期间只调用一次 Base::setZero()

通过这种方式,您可以创建 Base 或 Derived 并按预期调用 zerZero()。

,

您可以在 Derived 类中实现一个简单的 factory method 并删除 setZero() 来自构造函数的调用。然后使构造函数非公开将告诉类的使用者使用工厂方法而不是构造函数进行正确的实例化。像这样:

class Base
{
protected:
    Base() { }
    virtual void setZero() {std::cout << "Set all Base class values to zeros (default) values";}
};

class Derived : public Base
{
public:
    static Derived createInstance()
    {
        Derived derived;
        derived.setZero();
        return derived;
    }    
private:
    Derived() { }
    
    void setZero() override {
        Base::setZero(); 
        std::cout << "Set all Derived class values to zeros (default) values";
    }
};

然后像这样创建你的 Derived 实例:

int main()
{
    Derived derived = Derived::createInstance(); 
    // do something...
    
    return 0;
}

通过这种方法,您还可以确保没有人可以创建处于无效状态的类的实例。

注意:不知道您是否在某些地方直接使用基类,但如果是这种情况,您也可以为其提供工厂方法。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。