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

386
我听说C++类成员函数模板不能是虚函数。这是真的吗?
如果它们可以是虚函数,那么有什么情况下会使用这样的函数的例子呢?

15
我曾遇到类似的问题,也了解到同时使用虚函数和模板是具有争议的。我的解决方案是编写模板魔术代码,这些代码将在派生类中共用,并调用一个纯虚函数来执行特定的操作。当然,这与我的问题本质相关,可能不适用于每个情况。 - Tamás Szelei
15个回答

376

模板是关于编译器在编译时生成代码的。虚函数是关于运行时系统在运行时 figuring out 哪个函数需要被调用。

一旦运行时系统 figured out 需要调用一个模版化的虚函数,编译工作就完成了,编译器无法再生成适当的实例。因此,你不能有虚成员函数模板。

然而,有一些强大而有趣的技术结合了多态性和模板,特别是所谓的 type erasure


55
我并没有看到与语言相关的原因,只有实现方面的原因。虚函数表(vtables)不是语言的一部分,而是编译器实现语言的标准方式。 - gerardw
37
虚函数的作用在于运行时系统在运行时确定要调用哪个函数。抱歉,但这种说法是不太准确的,也很容易引起混淆。实际上,虚函数只是通过间接寻址实现的,并不涉及“运行时确定”。在编译时就已经知道需要调用vtable中第n个指针所指向的函数,因此并不存在类型检查等情况。关于“运行时确定”,这是错误的理解。另外,无论函数是否为虚函数,在编译时都已经确定了。 - dtech
15
如果编译器看到void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); },那么它“知道”在调用cb.f()时调用了哪个函数,并且不知道vb.f()调用的是哪个函数。后者必须在运行时由运行时系统来查找。无论您是否称其为“查找”,以及这是否更有效率,这些事实都没有改变。 - sbi
10
@ddriver: 2. 函数模板的实例是成员函数,因此将指向这种实例的指针放入虚函数表中没有任何问题。但是,只有在调用方进行编译时才知道需要哪些模板实例,而虚函数表在基类和派生类编译时设置。这些都是分别编译的。更糟糕的是,在运行时可以将新的派生类链接到系统中(例如浏览器动态加载插件)。即使调用方的源代码早已丢失,创建新的派生类时也可能无法访问该源代码。 - sbi
17
@sbi:你为什么要根据我的名字做出假设?我没有混淆泛型和模板。我知道Java的泛型纯粹是运行时的。你没有详细解释为什么C++不能有虚拟成员函数模板,但InQsitive给出了答案。你过于简化了模板和虚拟机制,将其归结为“编译时”和“运行时”,并得出“你不能有虚拟成员函数模板”的结论。我引用了InQsitive的答案,他引用了《C++ Templates The Complete Guide》。我不认为那是“敷衍了事”。祝你愉快。 - Javanator
显示剩余5条评论

180

来自《C++模板完全指南》:

成员函数模板不能被声明为虚函数。这是因为通常实现虚函数调用机制使用一个固定大小的表格,每个虚函数有一个条目。然而,成员函数模板的实例化数量在整个程序被翻译之前并不确定。因此,支持虚成员函数模板需要在C++编译器和链接器中支持一种全新的机制。相比之下,类模板的普通成员函数可以是虚函数,因为它们的数量在类被实例化时已经固定。


15
我认为现在的C++编译器和链接器,特别是支持链接时优化的编译器,应该能够在链接时生成所需的虚函数表和偏移量。因此,也许我们会在C++2b中获得这个功能? - Kai Petzke
4
我认为它不会长期有效。请记住,您的接口类具有模板虚函数,可能不仅在您自己的代码中使用,还可能被包含在多个“客户端”二进制文件中,这些文件可能编译为动态链接共享库。现在,想象一下每个库都继承了您的类并引入了新的函数实例。然后假设您通过 dlopen 动态打开这些共享库。此时的链接过程将会很麻烦,可能需要重新创建已经存在于内存中的对象的虚表! - CygnusX1
1
@CygnusX1,我不认为这是个问题。整个vtable不必实现为固定大小的指针连续数组。它们可以由一个固定大小的部分组成,以考虑非模板虚拟成员,并且(例如)一个映射(或映射的映射)用于模板化的虚拟函数实例化。这会略微增加开销,但会释放不幸的约束条件的好处。 - forestgril

43

目前C++不允许虚拟模板成员函数。最可能的原因是实现的复杂性。Rajendra提供了为什么现在不能完成它的好理由,但通过合理的标准更改,这是可能的。特别是要确定一个模板化函数的实例化数量并建立虚函数表,在考虑虚函数调用的位置时似乎很困难。标准人员现在只有很多其他事情要做,而且为编译器编写者来说,C++1x也是很多工作。

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


7
一个虚函数可能会被在编译该函数时并不存在的代码调用。对于那些甚至还未存在的代码,编译器如何决定为(理论上的)虚模板成员函数生成哪些实例呢? - sbi
2
@sbi:是的,分离编译将是一个巨大的问题。我对C++编译器一窍不通,所以无法提供解决方案。就像通常的模板函数一样,它应该在每个编译单元中再次实例化,对吗?那不就解决了问题吗? - pmr
2
如果您正在引用动态加载库,那么这是模板类/函数的一般问题,而不仅仅是虚拟模板方法的问题。 - Oak
1
一个可能的解决方案是启用某种稳定的运行时类型反射,然后创建一个(类型、函数指针)哈希映射表,而不是虚函数表。这是可行的。但非常复杂,与我们现在拥有的非常不同。 - CygnusX1
虚拟模板将是一场灾难。在 C++ 中实现虚函数的常见方法是使用虚表。这对于虚函数来说是不可能的,因为可能性是无穷无尽的。一种支持模板的使函数成为虚函数的方法是遍历类层次结构以找到最后一个实现该函数的类。这样做的优点是,只有实现的方法才会出现在表中。然而,这将会慢得多,因为我们得到的是 O(n*q) 搜索时间(要遍历的类数,方法数)。 - user13947194
显示剩余2条评论

28

虚函数表

让我们先从虚函数表的背景和工作原理开始 (来源):

[20.3] 虚函数和非虚函数调用的区别是什么?

非虚函数使用静态解析。也就是说,成员函数在编译时根据指向对象的指针(或引用)的类型静态选择。

相比之下,虚成员函数使用动态解析。也就是说,成员函数在运行时根据对象的类型动态选择,而不是指向该对象的指针/引用的类型。这被称为“动态绑定”。大多数编译器使用以下技术的变体: 如果对象具有一个或多个虚函数,则编译器在对象中放置一个名为“虚指针”或“v指针”的隐藏指针。 这个v指针指向一个全局表,称为“虚表”或“v表”。

编译器为每个至少有一个虚函数的类创建一个虚表。例如,如果圆形类具有用于draw()、move()和resize()的虚函数,则与圆形类关联的恰好有一个v表,即使有亿万个圆形对象,每个圆形对象的v指针也将指向圆形v表。 v表本身具有指向类中每个虚函数的指针。例如,圆形v表将具有三个指针:一个指向Circle::draw()、一个指向Circle::move()和一个指向Circle::resize()。

在调度虚函数期间,运行时系统跟随对象的v指针到类的v表,然后跟随v表中的适当插槽到方法代码。

上述技术的空间成本开销很小:每个对象多一个指针(但仅限于需要进行动态绑定的对象),每个方法多一个指针(但仅限于虚方法)。时间成本开销也相当小:与普通函数调用相比,虚函数调用需要两个额外的获取操作(一个获取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在基类中未声明。
以下是另一个堆栈溢出的答案,其中提供了解决方法:需要虚拟模板成员绕过

1
我遇到了相同的情况,以及大量类的继承结构。宏起了帮助作用。 - ZFY

20
不可以,但是有以下方法可以实现:
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);
};

如果你只想拥有一个通用接口并将实现延迟到子类中,那么这种方法的效果基本相同。


8
如果有人好奇的话,这被称为CRTP。 - Michael Choi
3
但是这并不能解决存在类继承关系的情况,当需要调用指向基类的虚函数指针时。你的 Foo 指针被限定为 Foo<Bar>,它无法指向 Foo<Barf> 或者 Foo<XXX> - Kai Petzke
@KaiPetzke:你不能构造一个无限制的指针。但是,你可以将任何不需要知道具体类型的代码进行模板化,这具有相同的效果(至少在概念上是如此 - 显然实现完全不同)。 - Tom

19

以下代码可以使用MinGW G++ 3.4.5在Windows 7上编译并正确运行:

#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&)'

显然有:

  • 虚成员函数可以在类模板中使用。编译器可以轻松构建虚表。
  • 不可能将类模板成员函数定义为虚函数,因为很难确定函数签名并分配虚表条目。

28
一个类模板可以有虚成员函数。成员函数不能同时是成员函数模板和虚成员函数。 - James McNellis
1
它在gcc 4.4.3上实际上失败了。在我的系统上,肯定是Ubuntu 10.04。 - Chenna V
4
这与问题所要求的完全不同。这里整个基类都是模板化的。我以前编译过这种东西。在Visual Studio 2010上也可以编译。 - ds-bos-msk

13

不行,模板成员函数不能是虚函数。


10
我的好奇心是:为什么?编译器在这样做时会面临什么问题? - WannaBeGeek
2
你需要在作用域内进行声明(至少为了获得正确的类型)。 标准(和语言)要求您对使用的标识符进行声明。 - dirkgently

7
在其它答案中,提议的模板函数是一个外观,没有提供任何实际好处。
模板函数可用于编写一次代码,并使用不同的类型。虚拟函数可用于为不同类提供公共接口。
语言不允许虚拟模板函数,但通过一种解决方法可以同时使用两者,例如为每个类定义一个模板实现和一个虚拟的公共接口。
然而,必须为每个模板类型组合定义一个虚拟包装函数。
#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

这里尝试一下


3

虽然这是一个被许多人回答过的旧问题,但我认为一个简洁的方法(与其他发布的方法并没有太大区别)是使用一个小巧的宏来帮助减少类声明的重复。

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

这里的好处是,当添加新支持的类型时,可以在抽象头文件中完成所有操作,而无需在多个源/头文件中进行修正。

"IMPL_RENDER() // Builds the functions we want" 可能如何被调用?@mccatnm - Jaziri Rami
这只是一个纯宏。对于这个例子,你可以在宏定义中省略 ()。它不是用来调用的,而是通过预编译器填充所需的函数。否则,你将不得不重新定义所有函数。(例如 Box::render(int, char *)Box::render(int, short *),等等) - mccatnm

3
回答问题的第二部分:
如果它们可以是虚拟的,那么有哪些使用此类函数的场景呢?
这并不是一个不合理的要求。例如,Java(其中每个方法都是虚拟方法)在泛型方法中没有任何问题。
在C++中,需要虚拟函数模板的一个示例是接受通用迭代器的成员函数,或者接受通用函数对象的成员函数。
解决此问题的方法是使用 boost::any_range 和 boost::function 进行类型擦除,这将允许您接受通用迭代器或函数对象,而无需将您的函数作为模板。

6
Java泛型是用于类型转换的语法糖,它们与模板不同。 - Brice M. Dempsey
2
@BriceM.Dempsey:你可以这样说,类型转换是Java实现泛型的方式,而不是相反...从语义上讲,exclipy提出的用例在我看来是有效的。 - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接