我有一个具有虚函数的基类。我想在构造期间调用该类,因为我想要为每个派生类调用该函数。我知道在构造过程中我不能调用虚函数,但我想不出优雅(即避免重复代码)的解决方案。
在施工期间调用虚拟功能有哪些方法?
我想避免这种情况的原因是因为我不想创建只调用基类的构造函数。
class A {
public:
A() {
read();
}
// This never needs to be called
virtual void read() = 0;
}
class B:A {
public:
B():A() { };
read() { /*Do something special for B here.*/ }
}
class C:A {
public:
C():A() { };
read() { /*Do something special for C here.*/ }
}
PS:Python的做法只是raise NotImplementedError
中的A::read()
。我回到了C ++,我比我想象的更生疏。
答案 0 :(得分:1)
实现此目的的一种方法,只是将其委托给另一个类(可能是朋友),并且可以确保在完全构造时调用它。
class A
{
friend class C;
private:
C& _c; // this is the actual class!
public:
A(C& c) : _c(c) { };
virtual ~A() { };
virtual void read() = 0;
};
class B : public A
{
public:
B(C& c) : A(c) { };
virtual ~B() { };
virtual void read() {
// actual implementation
};
};
class C
{
private:
std::unique_ptr<A> _a;
public:
C() : _a(new B(*this)) { // looks dangerous? not at this point...
_a->read(); // safe now
};
};
在这个例子中,我只是创建一个B
,但是你如何做到这一点取决于你想要实现的目标,并在必要时在C上使用模板,例如:
template<typename VIRTUAL>
class C
{
private:
using Ptr = std::unique_ptr<VIRTUAL>;
Ptr _ptr;
public:
C() : _ptr(new VIRTUAL(*this)) {
_ptr->read();
};
}; // eo class C
答案 1 :(得分:1)
这是工厂方法方法,将工厂放入基类:
class A {
public:
virtual void read() = 0;
template<class X> static X* create() {X* r = new X;X->read();return X;}
virtual A* clone() const = 0;
};
class B : public A {
B():A() { };
friend class A;
public:
void read() { /*Do something special for B here.*/ }
B* clone() const {return new B(*this);}
};
class C : public A {
C():A() { };
friend class A;
public:
void read() { /*Do something special for C here.*/ }
C* clone() const {return new C(*this);}
};
添加了clone
- 方法,其中包含协变返回类型作为奖励。
使用CRTP:
class A {
public:
// This never needs to be called
virtual void read() = 0;
virtual A* clone() const = 0;
};
template<class D, class B> struct CRTP : B {
D* clone() {return new D(*this);}
static D* create() {return new D();}
};
class B : public CRTP<B, A> {
B() { };
public:
void read() { /*Do something special for B here.*/ }
};
class C : public CRTP<C, A> {
C() { };
public:
void read() { /*Do something special for C here.*/ }
};
答案 2 :(得分:1)
这是一个常见问题。
请参阅标题为“Okay, but is there a way to simulate that behavior as if dynamic binding worked on the this object within my base class's constructor?”的C ++ FAQ项目。
在询问之前检查常见问题解答(通常是谷歌搜索或altavista')通常是一个好主意。
<小时/>
要清楚,虽然上面的字面问题是
“在施工过程中调用虚函数有哪些工作原理?”
很明显,这意味着
“如何设计基类B
,以便每个派生类可以指定B
构建期间发生的部分内容?”
一个主要的例子是C风格的GUI功能由C ++类包装。然后,通用Widget
构造函数可能需要实例化API级别的小部件,该小部件取决于最派生的类,应该是按钮小部件或列表框小部件或其他。因此,派生程度最高的类必须以某种方式影响Widget
构造函数中的内容。
换句话说,我们正在讨论派生类特定基础构造。
Marshall Cline称之为“构造期间的动态绑定”,并且它在C ++中存在问题,因为在C ++中,类T
构造和销毁期间对象的动态类型是T
。这有助于类型安全,因为在初始化该子对象或其初始化已开始之前,不会在派生类子对象上调用虚拟成员函数。但是主要成本是DBDI(显然)不能以简单和安全的方式完成。
<小时/>
在问题中,派生类特定操作被称为read
。在这里我称之为derived_action
。调用derived_action
的位置有三种主要可能性:
由实例化代码调用,称为两阶段构造 这基本上意味着有一个非常不可用的未完全初始化的对象,一个僵尸对象。但是,随着C ++ 11移动语义变得越来越普遍和被接受(无论如何,它可以通过使用工厂在某种程度上得到缓解)。一个主要问题是,在构建的第二阶段,由于构造期间的动态类型更改,不存在针对未初始化子对象的虚拟调用的普通C ++保护。
Derived
构造函数调用。
例如,derived_action
可以作为Base
构造函数的参数表达式调用。一种并非完全不常见的技术是使用类模板来生成大多数派生类,例如,提供derived_action
。
Base
构造函数调用。
这意味着derived_action
的知识必须动态或静态地传递给构造函数。一种不错的方法是使用默认的构造函数参数。这导致了并行类层次结构的概念,即派生类操作的层次结构。
此列表是为了提高复杂程度和类型安全性,并且据我所知,反映了各种技术的历史用途。
E.g。在微软的MFC和Borland的ObjectWindows GUI 1990年初,图书馆的两阶段建设很常见,截至2014年,这种设计现在被视为非常不合理。
答案 3 :(得分:0)
解决方法是在构建之后调用虚拟函数。然后,您可以在工厂功能中将两个操作(构造+虚拟调用)耦合在一起。这是基本的想法:
class FactoryA
{
public:
A *MakeA() const
{
A *ptr = CreateA();
ptr->read();
return ptr;
}
virtual ~FactoryA() {}
private:
virtual A *CreateA() const = 0;
};
class FactoryB : public FactoryA
{
private:
virtual A *CreateA() const { return new B; }
};
// client code:
void f(FactoryA &factory)
{
A *ptr = factory.MakeA();
}
答案 4 :(得分:-1)
如Benjamin Bannier所述,您可以使用CRTP(定义实际read()函数的模板。)该方法的一个问题是模板必须始终以内联方式编写。这有时会成为问题,特别是如果你要编写非常大的函数。
另一种方法是将函数指针传递给构造函数。虽然在某种程度上,它类似于在构造函数中调用函数,但它会强制您传递指针(尽管在C ++中,您总是可以传递nullptr。)
class A
{
public:
A(func_t f)
{
// if(!f) throw ...;
(*f)();
}
};
class B : A
{
public:
B() : A(read) {}
void read() { ... }
};
显然,你在read()函数及其调用的任何函数中都有“无法调用其他虚函数”的问题。另外,B的可变成员尚未初始化。在这种情况下,这可能是一个非常严重的问题......
因此,以这种方式编写更安全:
B() : A()
{
read();
}
但是,在这种情况下,这可能是你使用init()函数的时候。 init()函数可以在A()中实现(如果你可以访问它:即在派生时使用public A)并且该函数可以按预期调用所有虚函数:
class A
{
public:
void init()
{
read();
}
};
class B : public A
{
public:
...
};
我知道很多人都说init()函数是邪恶的,因为创建B对象的人现在需要知道调用它...但是你没有其他的东西可以做。话虽这么说,你可以有一种工厂形式,工厂可以根据需要进行init()调用。
class B : public A
{
public:
static B *create() { B *b(new B); b->init(); return b; }
private:
B() { ... } // prevent creation without calling create()
};