【问题标题】:How to rewrite code not to call virtual function from the constructor如何重写代码不从构造函数调用虚函数
【发布时间】:2021-09-19 00:14:02
【问题描述】:

这么快就变成这样了

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 复制到构造函数的解决方案,但是重复代码是一件坏事。还有其他解决方案吗?

【问题讨论】:

  • setZero的用途是不是只能在构造函数中使用?工厂允许在构造后调用方法。
  • 从技术上讲,动态绑定在构造函数期间确实有效,因为它解析为对象的实际当前派生最多的类型,只是Base 构造函数正在运行Derived 的东西尚未构建,所以对象 isBase
  • @Jarod42 不,正如我所写,setZero 是公开的,在不同的地方被称为
  • 你能打电话给Base::setZero()两次吗?如果是,那么您可以简单地在每个构造函数中调用它(BaseDerived)。如果不是,您可以将逻辑提取到其他一些函数中,这些函数将由构造函数和setZero() 调用。
  • 对象的派生部分直到基类的构造函数运行后才被初始化。这就是虚函数不调用派生版本的原因:没有对象可以应用派生函数。基类的构造函数负责初始化基类。派生类的构造函数负责初始化派生类。与 Java 不同,C++ 不鼓励基类的构造函数尝试初始化派生类。

标签: c++ c++11 inheritance virtual clean-architecture


【解决方案1】:

你可能有工厂有“post-call”,比如:

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

【讨论】:

    【解决方案2】:

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


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

    一个可能更好的处理方法是:

    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 依赖在它上面使归零干燥。)


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

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

    但是你想去兔子洞多远?

    【讨论】:

    • 你能告诉我我应该读什么来理解 struct SetZero:B { 部分吗?我对模板的了解有限,以前从未见过这样的语法。
    • @Bruice 这就是所谓的“CRTP”,一种奇怪的重复模板模式。重复是因为你重复类型,重复是因为它一直有用并出现在很多地方,它很奇怪,因为“到底发生了什么”是对它的合理回应。
    【解决方案3】:

    作为其他答案的替代方案,将功能与 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";
      }
    };
    

    【讨论】:

      【解决方案4】:

      你可以这样解决:

      #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. 添加了对来自 Derived 构造函数的 setZero()also 的调用
      3. 添加了一个不调用其setZero() 方法的受保护 Base 构造函数,并从 Derived 的构造函数调用此构造函数,以便在创建 Derived 对象期间仅调用一次 Base::setZero()

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

      【讨论】:

        【解决方案5】:

        您可以在您的 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;
        }
        

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

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

        【讨论】:

          【解决方案6】:

          如果我正确理解了您的问题,那么您需要做的只是下面

          #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 类派生任何其他类,您将继续做同样的事情。

          【讨论】:

          • 这破坏了 OP 向类用户公开的公共接口。
          • 你的评论,我不清楚,哪个公共接口坏了?
          • 在 Derived 实例上调用 setZero() 会同时执行 setZero()
          • 我明白你的意思,修复很简单,用更新的代码更新帖子
          猜你喜欢
          • 2011-07-16
          • 2010-10-05
          • 2012-01-28
          • 2021-10-20
          • 2013-02-18
          • 1970-01-01
          • 2012-01-27
          相关资源
          最近更新 更多