类成员函数模板可以是虚的吗?

我听说c++的类成员函数模板不能是虚的。这是真的吗?

如果它们可以是虚拟的,那么有什么场景可以使用这样的函数呢?

255993 次浏览

不可以,模板成员函数不能为虚函数。

模板都是关于编译器在编译时生成代码的。虚函数都是关于运行时系统确定在运行时调用哪个函数。

一旦运行时系统发现它需要调用模板化的虚函数,编译就完成了,编译器就不能再生成相应的实例了。因此,不能使用虚拟成员函数模板。

然而,有一些强大而有趣的技术来自于组合多态性和模板,特别是所谓的type擦除

c++现在不允许虚拟模板成员函数。最可能的原因是实现它的复杂性。Rajendra给出了很好的理由,为什么现在不能做到,但只要对标准进行合理的改变,就有可能做到。如果考虑到虚函数调用的位置,那么计算一个模板化函数实际存在多少个实例化以及构建虚表似乎很困难。标准人员现在有很多其他事情要做,c++ 1x对编译器编写者来说也有很多工作要做。

什么时候需要模板成员函数?我曾经遇到过这样的情况,我试图用纯虚拟基类重构一个层次结构。这是一种执行不同策略的糟糕风格。我想将其中一个虚函数的实参更改为数值类型,而不是重载成员函数并覆盖所有子类中的每一个重载,我尝试使用虚模板函数(并且不得不发现它们不存在)。

下面的代码可以在windows 7上使用mingwg++ 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&)'

所以很明显:

  • 虚成员函数可以在类模板中使用。编译器可以很容易地构造虚表
  • 将类模板成员函数定义为虚函数是不可能的,如你所见,很难确定函数签名和分配虚表项。

回答问题的第二部分:

如果它们可以是虚拟的,那么有什么场景可以使用这样的函数呢?

这并不是一件不合理的事情。例如,Java(每个方法都是虚的)使用泛型方法没有问题。

c++中需要虚函数模板的一个例子是接受泛型迭代器的成员函数。或接受泛型函数对象的成员函数。

这个问题的解决方案是使用boost::any_range和boost::function的类型擦除,这将允许您接受泛型迭代器或函子,而不需要使您的函数成为模板。

虚函数表

让我们从虚函数表及其工作原理()的一些背景知识开始:

[20.3]虚拟和非虚拟的区别是什么 成员函数被调用?< / p >

非虚成员函数静态解析。也就是 成员函数是静态选择的(在编译时) 指向对象的指针(或引用)的类型 相比之下,虚成员函数是动态解析的(at 运行时)。也就是说,成员函数是动态选择的(在 对象的类型,而不是对象的类型 指向该对象的指针/引用。这被称为“动态绑定”。 大多数编译器使用以下技术的一些变体 对象具有一个或多个虚函数时,编译器将其隐藏 对象中的指针称为“虚拟指针”或“v指针”。这 v指针指向一个全局表,称为“虚拟表”或 “v-table。" < / p > 编译器为每个至少有一个的类创建一个v-table 虚拟函数。例如,如果类Circle具有虚函数 对于draw()和move()和resize(),将只有一个v-table 与类圈相关,即使有无数个圈 对象,每个Circle对象的v指针都会指向 到圆形v桌。v-table本身有指向 类中的虚函数。例如,Circle v-table会 有三个指针:一个指向Circle::draw()的指针,一个指向 Circle::move()和指向Circle::resize()的指针 在虚函数的分派过程中,运行时系统紧随其后 对象的v指针指向类的v表,然后跟随

.在v-table中为方法代码设置合适的槽 以上技术的空间成本开销是名义上的:额外的 每个对象的指针(但仅适用于需要动态操作的对象 绑定),加上每个方法一个额外的指针(但仅用于virtual 方法)。时间成本开销也是相当微不足道的:与 普通函数调用,虚函数调用需要两个额外的函数 获取(一个获取v指针的值,第二个获取 方法的地址)。没有任何运行时活动发生在 非虚函数,因为编译器解析非虚函数 类的类型,在编译时独占地调用 指针。< / p >

我的问题,或者我是怎么来的

我尝试使用类似这样的东西,现在cubefile基类与模板优化加载函数,这将实现不同类型的立方体(一些存储像素,一些通过图像等)。

一些代码:

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没有在基类中声明。


这是另一个堆栈溢出的答案,有一个工作: 需要一个虚拟模板成员解决方案。< / p >

从c++模板的完整指南:

成员函数模板不能声明为虚函数。这个约束 是不是因为平时强制实现的虚函数 调用机制使用固定大小的表,每个虚表有一个条目 函数。但是,成员函数的实例化数量 模板在翻译完整个程序之前是不固定的。 因此,支持虚拟成员函数模板将需要 支持c++编译器中的一种全新机制 链接器。相反,类模板的普通成员可以是 虚的,因为当类实例化

时,它们的数量是固定的

不,他们不能。但是:

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

如果您想要做的只是拥有一个公共接口并将实现推迟到子类,则效果大致相同。

至少在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

如果预先知道模板方法的类型集,则'虚拟模板方法'有一个变通方法。

为了说明这个思想,在下面的例子中只使用了两种类型(intdouble)。

在那里,一个“虚拟”模板方法(Base::Method)调用相应的虚拟方法(Base::VMethod之一),后者依次调用模板方法实现(Impl::TMethod)。

只需要在派生实现(AImplBImpl)中实现模板方法TMethod,并使用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
< p >注: Base::Method实际上是实际代码的剩余部分(VMethod可以被公开并直接使用)。 我添加它,使它看起来像一个实际的“虚拟”模板方法

在其他答案中,建议的模板函数是一个门面,并不能提供任何实际的好处。

  • 模板函数对于只编写一次代码是有用的 不同的类型。李< / >
  • 虚函数对于为不同的类提供公共接口非常有用。

该语言不允许虚拟模板函数,但通过一个变通方法,可以同时拥有两者,例如,每个类都有一个模板实现和一个虚拟公共接口。

但是,有必要为每个模板类型组合定义一个虚拟包装器函数:

#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

试试看在这里

虽然很多人已经回答了一个老问题,但我相信一个简洁的方法,与其他发布的方法没有太大不同,就是使用一个小宏来帮助减轻类声明的重复。

// 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
};

这样做的好处是,当添加一个新支持的类型时,它可以从抽象头文件中完成,而不必在多个源文件/头文件中进行修改。

我目前的解决方案如下(禁用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
);
}
}

我唯一不喜欢的是你必须定义/注册所有的子类。

在虚函数的情况下如何调用正确的函数?

虚表将包含类的每个虚函数的条目,在运行时,它将选择特定函数的地址,并调用各自的函数。

如何正确的函数必须被调用在虚拟情况下连同函数模板?

在函数模板的情况下,用户可以使用任何类型调用该函数。这里相同的函数根据类型有几个版本。现在,在这种情况下,对于同一个函数,由于版本不同,必须维护vtable中的许多项。

我看了所有的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
}

我希望你们能明白我的意思。你不能使用虚拟模板函数,因为就编译器而言,它们是两个完全不同的函数;作为隐式参数,此参数具有不同的类型。

虚拟模板不能工作的另一个原因同样有效。因为虚表是快速实现虚函数的最佳方式。