关于C++:一个类成员函数模板可以是虚拟的吗?

Can a class member function template be virtual?

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

如果它们可以是虚拟的,那么使用这种函数的场景的例子是什么?


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

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

然而,有一些强大而有趣的技术源于结合多态性和模板,特别是所谓的类型擦除。


来自C++模板的完整指南:

Member function templates cannot be declared virtual. This constraint
is imposed because the usual implementation of the virtual function
call mechanism uses a fixed-size table with one entry per virtual
function. However, the number of instantiations of a member function
template is not fixed until the entire program has been translated.
Hence, supporting virtual member function templates would require
support for a whole new kind of mechanism in C++ compilers and
linkers. In contrast, the ordinary members of class templates can be
virtual because their number is fixed when a class is instantiated


C++现在不允许虚拟模板成员函数。最可能的原因是实现它的复杂性。Rajendra给出了一个很好的理由来解释为什么现在不能做到这一点,但是如果标准有合理的改变,这是可能的。特别是,如果考虑到虚拟函数调用的位置,那么计算实际存在多少个模板函数的实例化并构建vtable似乎很困难。标准人们现在有很多其他事情要做,C++1X也为编译器编写者做了大量工作。

您何时需要模板化成员函数?我曾经遇到过这样一种情况,我试图用纯虚拟基类重构层次结构。这是一种执行不同战略的糟糕方式。我想将其中一个虚函数的参数更改为数值类型,而不是重载成员函数,并重写我试图使用虚模板函数的所有子类中的每个重载(并且必须发现它们不存在)。


虚拟函数表

让我们从一些关于虚拟函数表及其工作方式的背景开始(源代码):

[20.3] What's the difference between how virtual and non-virtual
member functions are called?

Non-virtual member functions are resolved statically. That is, the
member function is selected statically (at compile-time) based on the
type of the pointer (or reference) to the object.

In contrast, virtual member functions are resolved dynamically (at
run-time). That is, the member function is selected dynamically (at
run-time) based on the type of the object, not the type of the
pointer/reference to that object. This is called"dynamic binding."
Most compilers use some variant of the following technique: if the
object has one or more virtual functions, the compiler puts a hidden
pointer in the object called a"virtual-pointer" or"v-pointer." This
v-pointer points to a global table called the"virtual-table" or
"v-table."

The compiler creates a v-table for each class that has at least one
virtual function. For example, if class Circle has virtual functions
for draw() and move() and resize(), there would be exactly one v-table
associated with class Circle, even if there were a gazillion Circle
objects, and the v-pointer of each of those Circle objects would point
to the Circle v-table. The v-table itself has pointers to each of the
virtual functions in the class. For example, the Circle v-table would
have three pointers: a pointer to Circle::draw(), a pointer to
Circle::move(), and a pointer to Circle::resize().

During a dispatch of a virtual function, the run-time system follows
the object's v-pointer to the class's v-table, then follows the
appropriate slot in the v-table to the method code.

The space-cost overhead of the above technique is nominal: an extra
pointer per object (but only for objects that will need to do dynamic
binding), plus an extra pointer per method (but only for virtual
methods). The time-cost overhead is also fairly nominal: compared to a
normal function call, a virtual function call requires two extra
fetches (one to get the value of the v-pointer, a second to get the
address of the method). None of this runtime activity happens with
non-virtual functions, since the compiler resolves non-virtual
functions exclusively at compile-time based on the type of the
pointer.

我的问题,或者我是怎么来到这里的

我现在正尝试对一个具有模板化优化加载函数的CubeFile基类使用类似的方法,这些函数将针对不同类型的多维数据集(一些按像素存储,一些按图像存储等)以不同的方式实现。

一些代码:

1
2
3
4
5
6
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;

我希望它是什么,但由于虚拟模板组合,它无法编译:

1
2
3
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;

I find up moving the template declaration to the class level.。这种解决方案会迫使程序在读取数据之前就知道要读取的特定类型的数据,这是不可接受的。

解决方案

警告,这不是很漂亮,但它允许我删除重复的执行代码

1)在基类中

1
2
3
4
5
6
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)在儿童班

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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未在基类中声明。

下面是另一个关于堆栈溢出的答案:需要虚拟模板成员解决方法。


在窗口7上使用mingw g++3.4.5可以正确编译和运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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);
}

输出为:

1
2
3
A:A<string> a
A<--B:B<string> c
A<--B:3

后来我又添加了一个新的类X:

1
2
3
4
5
6
7
8
9
class X
{
public:
    template <typename T>
    virtual void func2(const T& p)
    {
        cout<<"C:"<<p<<endl;
    }
};

当我尝试在main()中使用类x时,如下所示:

1
2
X x;
x.func2<string>("X x");

G++报告以下错误:

1
2
vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'

所以很明显:

  • 可以在类模板中使用虚拟成员函数。编译器很容易构造vtable
  • 无法将类模板成员函数定义为虚函数,如您所见,很难确定函数签名和分配vtable项。


不,他们不能。但是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
};

如果您只想拥有一个公共接口并将实现推迟到子类,那么它的效果就差不多了。


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


回答问题的第二部分:

If they can be virtual, what is an example of a scenario in which one would use such a function?

这不是一件不合理的事情。例如,Java(其中每种方法都是虚拟的)对泛型方法没有问题。

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

这个问题的解决方案是将类型擦除与boost::any_range和boost::function一起使用,这样您就可以接受一个通用的迭代器或functor,而无需将函数设置为模板。


如果模板方法的类型集提前已知,则"虚拟模板方法"有一个变通方法。

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

这里有一个"虚拟"模板方法(Base::Method调用对应的虚拟方法(Base::VMethod中的一个),该方法反过来调用模板方法实现(Impl::TMethod)。

只需在派生实现(AImplBImpl)中实现模板方法TMethod,使用Derived<*Impl>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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;
}

输出:

1
2
3
4
0
1
2
3

NB:Base::Method实际上是实际代码的盈余(VMethod可以公开并直接使用)。我添加了它,所以它看起来像一个实际的"虚拟"模板方法。


在另一个答案中,建议的模板函数是一个外观,没有任何实际的好处。

  • 模板函数仅对使用不同类型。
  • 虚拟函数对于为不同的类提供公共接口很有用。

该语言不允许使用虚拟模板函数,但是有了一个变通方案,就可以同时使用这两种功能,例如每个类都有一个模板实现和一个虚拟公共接口。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#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;
}

输出:

Square area is 1, Circle area is 3.1415926535897932385

试试看


至少在GCC5.4中,虚拟函数可以是模板成员,但必须是模板本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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;
}

输出

1
2
mix before a2
Process finished with exit code 0