我听说 C++ 类成员函数模板不能是虚拟的。这是真的?
如果它们可以是虚拟的,那么使用这种功能的场景示例是什么?
我听说 C++ 类成员函数模板不能是虚拟的。这是真的?
如果它们可以是虚拟的,那么使用这种功能的场景示例是什么?
模板都是关于编译器在编译时生成代码的。虚函数都是关于运行时系统确定在运行时调用哪个函数。
一旦运行时系统发现它需要调用一个模板化的虚函数,编译就完成了,编译器就不能再生成适当的实例了。因此,您不能拥有虚拟成员函数模板。
然而,有一些强大而有趣的技术源于多态性和模板的结合,特别是所谓的类型擦除。
来自 C++ 模板完整指南:
成员函数模板不能声明为虚拟的。施加此约束是因为虚函数调用机制的通常实现使用一个固定大小的表,每个虚函数有一个条目。然而,成员函数模板的实例化数量在整个程序被翻译之前是不固定的。因此,支持虚拟成员函数模板需要在 C++ 编译器和链接器中支持一种全新的机制。相反,类模板的普通成员可以是虚拟的,因为它们的数量在类被实例化时是固定的
C++ 目前不允许虚拟模板成员函数。最可能的原因是实现它的复杂性。Rajendra 给出了现在不能完成的充分理由,但可以通过对标准进行合理的更改来实现。如果考虑到虚函数调用的位置,特别是要计算出实际存在多少模板化函数的实例并构建 vtable 似乎很困难。标准人员现在还有很多其他事情要做,而 C++1x 对编译器编写者来说也是很多工作。
什么时候需要模板化成员函数?我曾经遇到过这样一种情况,我试图用纯虚拟基类重构层次结构。对于实施不同的策略,这是一种糟糕的风格。我想将其中一个虚拟函数的参数更改为数字类型,而不是重载成员函数并覆盖所有子类中的每个重载,我尝试使用虚拟模板函数(并且不得不发现它们不存在.)
让我们从一些关于虚函数表的背景以及它们如何工作(source)开始:
[20.3] 虚拟和非虚拟成员函数的调用方式有什么区别?
非虚拟成员函数是静态解析的。也就是说,成员函数是根据指向对象的指针(或引用)的类型(在编译时)静态选择的。
相反,虚拟成员函数是动态解析的(在运行时)。也就是说,成员函数是根据对象的类型(在运行时)动态选择的,而不是基于该对象的指针/引用的类型。这称为“动态绑定”。大多数编译器使用以下技术的一些变体:如果对象具有一个或多个虚函数,编译器会在对象中放置一个隐藏指针,称为“虚指针”或“V 指针”。这个 v-pointer 指向一个名为“virtual-table”或“v-table”的全局表。
编译器为每个具有至少一个虚函数的类创建一个 v-table。例如,如果类 Circle 具有 draw() 和 move() 和 resize() 的虚函数,那么将只有一个 v-table 与类 Circle 相关联,即使有无数个 Circle 对象,并且 v-指针这些 Circle 对象中的每一个都将指向 Circle v-table。v-table 本身具有指向类中每个虚函数的指针。例如,Circle v-table 将具有三个指针:指向 Circle::draw() 的指针、指向 Circle::move() 的指针和指向 Circle::resize() 的指针。
在一个虚函数的分派期间,运行时系统跟随对象的 v-pointer 指向类的 v-table,然后跟随 v-table 中适当的槽到方法代码。
上述技术的空间成本开销是名义上的:每个对象有一个额外的指针(但仅适用于需要进行动态绑定的对象),以及每个方法的额外指针(但仅适用于虚拟方法)。时间成本开销也相当小:与普通函数调用相比,虚函数调用需要两次额外的提取(一次获取 v 指针的值,第二次获取方法的地址)。非虚拟函数不会发生这种运行时活动,因为编译器会在编译时根据指针的类型专门解析非虚拟函数。
我现在正在尝试将类似的东西用于具有模板化优化加载函数的多维数据集文件基类,这些函数将针对不同类型的多维数据集(一些按像素存储,一些按图像存储等)以不同方式实现。
一些代码:
virtual void LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
我希望它是什么,但由于虚拟模板组合而无法编译:
template<class T>
virtual void LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
我最终将模板声明移至类级别。这种解决方案将迫使程序在读取它们之前了解它们将读取的特定类型的数据,这是不可接受的。
警告,这不是很漂亮,但它允许我删除重复的执行代码
1)在基类中
virtual void LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
2) 在子类中
void LoadCube(UtpBipCube<float> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
void LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
void LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }
template<class T>
void LoadAnyCube(UtpBipCube<T> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1);
请注意,LoadAnyCube 未在基类中声明。
这是另一个解决方法的堆栈溢出答案: 需要一个虚拟模板成员解决方法。
在 Window 7 上使用 MinGW G++ 3.4.5 可以正确编译和运行以下代码:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class A{
public:
virtual void func1(const T& p)
{
cout<<"A:"<<p<<endl;
}
};
template <typename T>
class B
: public A<T>
{
public:
virtual void func1(const T& p)
{
cout<<"A<--B:"<<p<<endl;
}
};
int main(int argc, char** argv)
{
A<string> a;
B<int> b;
B<string> c;
A<string>* p = &a;
p->func1("A<string> a");
p = dynamic_cast<A<string>*>(&c);
p->func1("B<string> c");
B<int>* q = &b;
q->func1(3);
}
输出是:
A:A<string> a
A<--B:B<string> c
A<--B:3
后来我添加了一个新的类 X:
class X
{
public:
template <typename T>
virtual void func2(const T& p)
{
cout<<"C:"<<p<<endl;
}
};
当我尝试像这样在 main() 中使用 X 类时:
X x;
x.func2<string>("X x");
g++报如下错误:
vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'
所以很明显:
不,他们不能。但:
template<typename T>
class Foo {
public:
template<typename P>
void f(const P& p) {
((T*)this)->f<P>(p);
}
};
class Bar : public Foo<Bar> {
public:
template<typename P>
void f(const P& p) {
std::cout << p << std::endl;
}
};
int main() {
Bar bar;
Bar *pbar = &bar;
pbar -> f(1);
Foo<Bar> *pfoo = &bar;
pfoo -> f(1);
};
如果您只想拥有一个公共接口并将实现推迟到子类,则具有相同的效果。
不,模板成员函数不能是虚拟的。
在其他答案中,建议的模板功能是一种外观,并没有提供任何实际好处。
该语言不允许使用虚拟模板功能,但有一种解决方法可以同时拥有这两种功能,例如,每个类都有一个模板实现和一个虚拟通用接口。
然而,有必要为每个模板类型组合定义一个虚拟包装函数:
#include <memory>
#include <iostream>
#include <iomanip>
//---------------------------------------------
// Abstract class with virtual functions
class Geometry {
public:
virtual void getArea(float &area) = 0;
virtual void getArea(long double &area) = 0;
};
//---------------------------------------------
// Square
class Square : public Geometry {
public:
float size {1};
// virtual wrapper functions call template function for square
virtual void getArea(float &area) { getAreaT(area); }
virtual void getArea(long double &area) { getAreaT(area); }
private:
// Template function for squares
template <typename T>
void getAreaT(T &area) {
area = static_cast<T>(size * size);
}
};
//---------------------------------------------
// Circle
class Circle : public Geometry {
public:
float radius {1};
// virtual wrapper functions call template function for circle
virtual void getArea(float &area) { getAreaT(area); }
virtual void getArea(long double &area) { getAreaT(area); }
private:
// Template function for Circles
template <typename T>
void getAreaT(T &area) {
area = static_cast<T>(radius * radius * 3.1415926535897932385L);
}
};
//---------------------------------------------
// Main
int main()
{
// get area of square using template based function T=float
std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
float areaSquare;
geometry->getArea(areaSquare);
// get area of circle using template based function T=long double
geometry = std::make_unique<Circle>();
long double areaCircle;
geometry->getArea(areaCircle);
std::cout << std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl;
return 0;
}
输出:
正方形面积为1,圆形面积为3.1415926535897932385
在这里试试
要回答问题的第二部分:
如果它们可以是虚拟的,那么使用这种功能的场景示例是什么?
这不是一件不合理的事情。例如,Java(其中每个方法都是虚拟的)对于泛型方法没有问题。
C++ 中需要一个虚函数模板的一个例子是一个接受泛型迭代器的成员函数。或接受通用函数对象的成员函数。
这个问题的解决方案是使用带有 boost::any_range 和 boost::function 的类型擦除,这将允许你接受一个通用的迭代器或仿函数,而不需要让你的函数成为一个模板。
虽然许多人已经回答了一个较老的问题,但我相信一个简洁的方法,与其他发布的方法没有太大区别,是使用一个次要宏来帮助简化类声明的重复。
// abstract.h
// Simply define the types that each concrete class will use
#define IMPL_RENDER() \
void render(int a, char *b) override { render_internal<char>(a, b); } \
void render(int a, short *b) override { render_internal<short>(a, b); } \
// ...
class Renderable
{
public:
// Then, once for each on the abstract
virtual void render(int a, char *a) = 0;
virtual void render(int a, short *b) = 0;
// ...
};
所以现在,要实现我们的子类:
class Box : public Renderable
{
public:
IMPL_RENDER() // Builds the functions we want
private:
template<typename T>
void render_internal(int a, T *b); // One spot for our logic
};
这里的好处是,当添加一个新支持的类型时,这一切都可以从抽象头文件中完成,并且放弃可能在多个源/头文件中纠正它。
如果预先知道模板方法的类型集,则有一个“虚拟模板方法”的解决方法。
为了展示这个想法,在下面的示例中,仅使用了两种类型(int
和double
)。
在那里,“虚拟”模板方法 ( Base::Method
) 调用相应的虚拟方法(其中之一Base::VMethod
),而虚拟方法又调用模板方法实现 ( Impl::TMethod
)。
只需要TMethod
在派生实现中实现模板方法( AImpl
, BImpl
)并使用Derived<*Impl>
。
class Base
{
public:
virtual ~Base()
{
}
template <typename T>
T Method(T t)
{
return VMethod(t);
}
private:
virtual int VMethod(int t) = 0;
virtual double VMethod(double t) = 0;
};
template <class Impl>
class Derived : public Impl
{
public:
template <class... TArgs>
Derived(TArgs&&... args)
: Impl(std::forward<TArgs>(args)...)
{
}
private:
int VMethod(int t) final
{
return Impl::TMethod(t);
}
double VMethod(double t) final
{
return Impl::TMethod(t);
}
};
class AImpl : public Base
{
protected:
AImpl(int p)
: i(p)
{
}
template <typename T>
T TMethod(T t)
{
return t - i;
}
private:
int i;
};
using A = Derived<AImpl>;
class BImpl : public Base
{
protected:
BImpl(int p)
: i(p)
{
}
template <typename T>
T TMethod(T t)
{
return t + i;
}
private:
int i;
};
using B = Derived<BImpl>;
int main(int argc, const char* argv[])
{
A a(1);
B b(1);
Base* base = nullptr;
base = &a;
std::cout << base->Method(1) << std::endl;
std::cout << base->Method(2.0) << std::endl;
base = &b;
std::cout << base->Method(1) << std::endl;
std::cout << base->Method(2.0) << std::endl;
}
输出:
0
1
2
3
注意:
Base::Method
实际代码实际上是多余的(VMethod
可以公开并直接使用)。我添加了它,使它看起来像一个实际的“虚拟”模板方法。
我当前的解决方案如下(禁用 RTTI - 您也可以使用 std::type_index):
#include <type_traits>
#include <iostream>
#include <tuple>
class Type
{
};
template<typename T>
class TypeImpl : public Type
{
};
template<typename T>
inline Type* typeOf() {
static Type* typePtr = new TypeImpl<T>();
return typePtr;
}
/* ------------- */
template<
typename Calling
, typename Result = void
, typename From
, typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action);
template<typename Cls>
class ChildClasses
{
public:
using type = std::tuple<>;
};
template<typename... Childs>
class ChildClassesHelper
{
public:
using type = std::tuple<Childs...>;
};
//--------------------------
class A;
class B;
class C;
class D;
template<>
class ChildClasses<A> : public ChildClassesHelper<B, C, D> {};
template<>
class ChildClasses<B> : public ChildClassesHelper<C, D> {};
template<>
class ChildClasses<C> : public ChildClassesHelper<D> {};
//-------------------------------------------
class A
{
public:
virtual Type* GetType()
{
return typeOf<A>();
}
template<
typename T,
bool checkType = true
>
/*virtual*/void DoVirtualGeneric()
{
if constexpr (checkType)
{
return DoComplexDispatch<A>(this, [&](auto* other) -> decltype(auto)
{
return other->template DoVirtualGeneric<T, false>();
});
}
std::cout << "A";
}
};
class B : public A
{
public:
virtual Type* GetType()
{
return typeOf<B>();
}
template<
typename T,
bool checkType = true
>
/*virtual*/void DoVirtualGeneric() /*override*/
{
if constexpr (checkType)
{
return DoComplexDispatch<B>(this, [&](auto* other) -> decltype(auto)
{
other->template DoVirtualGeneric<T, false>();
});
}
std::cout << "B";
}
};
class C : public B
{
public:
virtual Type* GetType() {
return typeOf<C>();
}
template<
typename T,
bool checkType = true
>
/*virtual*/void DoVirtualGeneric() /*override*/
{
if constexpr (checkType)
{
return DoComplexDispatch<C>(this, [&](auto* other) -> decltype(auto)
{
other->template DoVirtualGeneric<T, false>();
});
}
std::cout << "C";
}
};
class D : public C
{
public:
virtual Type* GetType() {
return typeOf<D>();
}
};
int main()
{
A* a = new A();
a->DoVirtualGeneric<int>();
}
// --------------------------
template<typename Tuple>
class RestTuple {};
template<
template<typename...> typename Tuple,
typename First,
typename... Rest
>
class RestTuple<Tuple<First, Rest...>> {
public:
using type = Tuple<Rest...>;
};
// -------------
template<
typename CandidatesTuple
, typename Result
, typename From
, typename Action
>
inline constexpr Result DoComplexDispatchInternal(From* from, Action&& action, Type* fromType)
{
using FirstCandidate = std::tuple_element_t<0, CandidatesTuple>;
if constexpr (std::tuple_size_v<CandidatesTuple> == 1)
{
return action(static_cast<FirstCandidate*>(from));
}
else {
if (fromType == typeOf<FirstCandidate>())
{
return action(static_cast<FirstCandidate*>(from));
}
else {
return DoComplexDispatchInternal<typename RestTuple<CandidatesTuple>::type, Result>(
from, action, fromType
);
}
}
}
template<
typename Calling
, typename Result
, typename From
, typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action)
{
using ChildsOfCalling = typename ChildClasses<Calling>::type;
if constexpr (std::tuple_size_v<ChildsOfCalling> == 0)
{
return action(static_cast<Calling*>(from));
}
else {
auto fromType = from->GetType();
using Candidates = decltype(std::tuple_cat(std::declval<std::tuple<Calling>>(), std::declval<ChildsOfCalling>()));
return DoComplexDispatchInternal<Candidates, Result>(
from, std::forward<Action>(action), fromType
);
}
}
我唯一不喜欢的是您必须定义/注册所有子类。
我查看了所有 14 个答案,有些原因是虚拟模板功能无法正常工作,有些则显示了解决方法。一个答案甚至表明虚拟类可以具有虚拟功能。这不应该太令人惊讶。
我的回答将直接给出标准为什么不允许虚拟模板函数的原因。既然这么多人抱怨。首先,我不敢相信有人评论说可以在编译时推导出虚函数。这是我听过的最愚蠢的话。
无论如何。我确信标准规定指向对象的 this 指针是其成员函数的第一个参数。
struct MyClass
{
void myFunction();
}
// translate to
void myFunction(MyClass*);
现在我们清楚了这一点。然后我们需要知道模板的转换规则。模板化参数极其受限于它可以隐式转换的内容。我不记得全部了,但你可以检查C++ Primer
完整的参考。例如 T* 可转换为 const T*。数组可以转换为指针。但是,派生类不能作为模板参数转换为基类。
struct A {};
struct B : A {};
template<class T>
void myFunction(T&);
template<>
void myFunction<A>(A&) {}
int main()
{
A a;
B b;
myFunction(a); //compiles perfectly
myFunction((A&)b); // compiles nicely
myFunction(b); //compiler error, use of undefined template function
}
所以我希望你明白我的意思。您不能拥有虚拟模板函数,因为就编译器而言,它们是两个完全不同的函数;因为他们隐含的这个参数是不同的类型。
虚拟模板无法工作的另一个原因同样有效。由于虚拟表是快速实现虚拟功能的最佳方式。
至少在 gcc 5.4 中,虚函数可以是模板成员,但本身必须是模板。
#include <iostream>
#include <string>
class first {
protected:
virtual std::string a1() { return "a1"; }
virtual std::string mixt() { return a1(); }
};
class last {
protected:
virtual std::string a2() { return "a2"; }
};
template<class T> class mix: first , T {
public:
virtual std::string mixt() override;
};
template<class T> std::string mix<T>::mixt() {
return a1()+" before "+T::a2();
}
class mix2: public mix<last> {
virtual std::string a1() override { return "mix"; }
};
int main() {
std::cout << mix2().mixt();
return 0;
}
输出
mix before a2
Process finished with exit code 0
在虚拟的情况下如何调用正确的函数?
Vtable 将包含类的每个虚函数的条目,并且在运行时它将选择特定函数的地址并调用相应的函数。
在虚函数和函数模板的情况下,如何调用正确的函数?
在函数模板的情况下,用户可以使用任何类型调用此函数。这里相同的功能有几个基于类型的版本。现在,在这种情况下,由于版本不同,对于相同的功能,必须维护 vtable 中的许多条目。