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



所以很快情况是这样的

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

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

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依靠它来使归零干燥。


另一种方法是两阶段施工。 在其中,我们标记所有"原始"构造函数都受到保护。

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>
class Base
{
public:
Base() {  Base::setZero();}
virtual void setZero() {std::cout << "Set all Base class values to zeros (default) valuesn";}
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) valuesn";
}
};

我所做的,如下:

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

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

您可以在Derived类中实现一个简单的工厂方法,并从构造函数中删除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";
}
};

然后以某种方式创建派生的实例,如下所示:

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

return 0;
}

使用此方法,您还可以确保没有人可以创建未处于有效状态的类实例。

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

如果我正确理解了您的问题,那么您需要做的很简单

#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。

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

最新更新