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

Wan*_*eek 293 c++ templates virtual-functions c++-faq function-templates

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

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

sbi*_*sbi 312

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

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

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

  • 我没有看到_language_这个原因,只有_implementation_原因.vtables不是语言的一部分 - 只是编译器实现语言的标准方式. (29认同)
  • `虚拟函数都是关于运行时系统确定在运行时调用哪个函数` - 抱歉,这是一个相当错误的方法,而且相当令人困惑.它只是间接的,并且没有涉及"运行时计算",在编译时知道要调用的函数是vtable中第n个指针所指向的函数."搞清楚"意味着存在类型检查等等,但事实并非如此.`一旦运行时系统发现它需要调用一个模板化的虚函数 - 无论函数是否为虚函数在编译时都是已知的. (14认同)
  • @sbi:你为什么根据我的名字做出假设?我并没有混淆泛型和模板.我知道Java的泛型是纯粹的运行时间.你没有详尽地解释为什么你不能在C++中拥有虚拟成员函数模板,但是InQsitive却做到了.您过度简化了模板和虚拟机制以"编译时间"与"运行时间",并得出结论"您不能拥有虚拟成员函数模板".我引用了InQsitive的答案,它引用了"C++模板完整指南".我不认为这是"挥手".祝你今天愉快. (9认同)
  • @ddriver:__1 .__如果编译器看到`void f(concr_base&cb,virt_base&vb){cb.f(); vb.f(); }`,然后它"知道"在调用`cb.f()`时调用哪个函数,并且不知道`vb.f()`.后者必须在运行时系统_的运行时_,__中找到.无论你是想称之为"搞清楚",以及这是否效率更高或更低,都不会改变这些事实. (8认同)
  • @ddriver:__2 .__(成员)函数模板的实例是(成员)函数,因此将指向此类实例的指针放入vtable完全没有问题.但是,只有在编译调用者时才知道需要哪些模板实例,而在编译基类和派生类时设置vtable.这些都是单独编译的.更糟糕的是 - 新的派生类可以在运行时链接到正在运行的系统(想想你的浏览器动态加载插件).当创建新的派生类时,甚至调用者的源代码也可能长期丢失. (6认同)
  • 我认为在v-table中有一个指向模板特化的指针是不可能的.拥有虚拟主义和模板是完全可行的.即使我不知道C++不支持这一点的动机,你列出的原因与它无关,因为它们在实践中甚至没有意义.在编译期间解析对模板的调用,这是编译器知道尝试生成特化的方式.虚函数提供了一种机制,可以从一个类型系列的统一位置调用不同的函数,不涉及"实时计算". (4认同)
  • @sbi--很明显,如果你决定实施虚拟模板会有限制,但这并不是既不可能也不是不切实际.标准委员会根本没有决定采用它,因为您可以使用一些包装糖轻松获得相同的"有限功能".我的观点是你的答案是最受欢迎的,你可能想要改进它,所以它不会给人们错误的想法.另外,即使单独编译,它们仍然会被链接,它不像是不透明的指针或者其他东西......二进制布局会以任何一种方式传播. (3认同)
  • @ddriver:虽然我提供了真实的,坚硬的,实用的论证,为什么这不起作用,你回来时挥手坚持"它可以有限制地完成"而不提供任何证据如何.那么,[这里](/sf/ask/164794731/?noredirect=1#comment28349285_2354671)回复你.手. (3认同)
  • 刚碰到这个,欣赏mag和你的答案.我试图不要把我的希望变得太高,但类型擦除可能是我一直在寻找的. (2认同)
  • @ InQusitive的答案更完整.它回答了这个问题,解释了为什么当前的实施不支持这一点,解释了未来如何支持它,并引用了一个可信的来源.此外,你似乎在说这个功能是不可能的,这是错误的(因此投票,对不起). (2认同)

InQ*_*ive 126

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

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

  • 我认为当今的C ++编译器和链接器,尤其是具有链接时间优化支持的链接器,应该能够在链接时生成所需的vtable和偏移量。那么也许我们将在C ++ 2b中获得此功能? (6认同)
  • 我认为这在很长一段时间内不会起作用。请记住,带有模板虚函数的接口类不仅可以在您自己的代码中使用,还可以包含在多个“客户端”二进制文件中,并可能编译为动态链接共享库。现在,假设每个库都继承自您的类并引入一个新的函数实例。想象一下,您动态地打开这些共享库,例如通过“dlopen”。发生“dlopen”时的链接过程会很麻烦,可能需要为内存中已经存在的对象重新创建虚函数表! (2认同)

pmr*_*pmr 32

C++目前不允许使用虚拟模板成员函数.最可能的原因是实施它的复杂性.拉金德拉很好地说明了为什么现在无法做到这一点,但可以通过合理的标准变更来实现.特别是如果你考虑虚函数调用的位置,那么实际存在模板函数的实例数并且构建vtable似乎很困难.标准人员现在还有许多其他事情需要做,而C++ 1x对于编译器编写者来说也是很多工作.

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

  • @pmr:可以从编译函数时甚至不存在的代码调用虚函数.编译器如何确定(理论上)虚拟模板成员函数的哪些实例为甚至不存在的代码生成? (5认同)
  • @sbi:是的,单独的编译将是一个巨大的问题.我根本不是C++编译器的专家,所以我无法提供解决方案.与模板化函数一样,它应该在每个编译单元中再次实例化,对吧?这不会解决问题吗? (2认同)
  • @sbi如果你指的是动态加载库,这是模板类/函数的一般问题,而不仅仅是虚拟模板方法. (2认同)

Mar*_*sel 16

虚函数表

让我们从虚拟功能表及其工作原理(源代码)的背景知识开始:

[20.3]虚拟和非虚拟成员函数的调用方式有何区别?

非虚拟成员函数是静态解析的.也就是说,基于对象的指针(或引用)的类型静态地(在编译时)选择成员函数.

相反,虚拟成员函数是动态解析的(在运行时).也就是说,基于对象的类型动态地(在运行时)选择成员函数,而不是指向该对象的指针/引用的类型.这称为"动态绑定".大多数编译器使用以下技术的一些变体:如果对象具有一个或多个虚函数,则编译器会在对象中放置一个名为"虚拟指针"或"v指针"的隐藏指针.此v指针指向称为"虚拟表"或"v表"的全局表.

编译器为每个具有至少一个虚函数的类创建一个v表.例如,如果类Circle具有draw()和move()以及resize()的虚函数,则只有一个v-table与类Circle相关联,即使存在大量的Circle对象,并且v指针也是如此.每个Circle对象都指向Circle v表.v表本身具有指向类中每个虚函数的指针.例如,Circle v-table将有三个指针:指向Circle :: draw()的指针,指向Circle :: move()的指针,以及指向Circle :: resize()的指针.

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

上述技术的空间成本开销是标称的:每个对象一个额外的指针(但仅适用于需要动态绑定的对象),以及每个方法的额外指针(但仅适用于虚方法).时间成本开销也是相当标准的:与普通函数调用相比,虚函数调用需要两次额外的提取(一次获取v指针的值,第二次获取方法的地址).非虚函数不会发生此运行时活动,因为编译器会根据指针类型在编译时专门解析非虚函数.


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

我正在尝试使用类似这样的东西,用于具有模板化优化加载函数的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;
Run Code Online (Sandbox Code Playgroud)

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

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;
Run Code Online (Sandbox Code Playgroud)

我最终将模板声明移到了类级别.该解决方案将迫使程序在读取之前了解它们将读取的特定类型的数据,这是不可接受的.

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

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;
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

请注意,LoadAnyCube未在基类中声明.


这是另一个堆栈溢出答案,解决方法: 需要虚拟模板成员解决方法.


Bre*_*t81 13

在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);
}
Run Code Online (Sandbox Code Playgroud)

输出是:

A:A<string> a
A<--B:B<string> c
A<--B:3
Run Code Online (Sandbox Code Playgroud)

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

class X
{
public:
    template <typename T>
    virtual void func2(const T& p)
    {
        cout<<"C:"<<p<<endl;
    }
};
Run Code Online (Sandbox Code Playgroud)

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

X x;
x.func2<string>("X x");
Run Code Online (Sandbox Code Playgroud)

g ++报告以下错误:

vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'
Run Code Online (Sandbox Code Playgroud)

所以很明显:

  • 虚拟成员函数可以在类模板中使用.编译器很容易构造vtable
  • 将类模板成员函数定义为虚拟是不可能的,如您所见,很难确定函数签名并分配vtable条目.

  • 类模板可以具有虚拟成员函数.成员函数可能不是成员函数模板和虚拟成员函数. (18认同)
  • 这与提出的问题完全不同.这里整个基类都是模板化的.我以前编译过这种东西.这也可以在Visual Studio 2010上编译 (2认同)

Tom*_*Tom 11

不,他们不能.但:

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);
};
Run Code Online (Sandbox Code Playgroud)

如果您只想拥有一个通用接口并将实现推迟到子类,则会产生相同的效果.

  • 如果有人好奇,这被称为 CRTP。 (7认同)
  • 但这对于具有类层次结构并希望能够调用指向基类的指针的虚拟方法的情况没有帮助。您的“Foo”指针被限定为“Foo&lt;Bar&gt;”,它不能指向“Foo&lt;Barf&gt;”或“Foo&lt;XXX&gt;”。 (3认同)

dir*_*tly 6

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

  • 我的好奇心是:为什么?编译器这样做面临哪些问题? (9认同)
  • 您需要在范围内声明(至少,为了使类型正确)。标准(和语言)要求在您使用的标识符的范围内进行声明。 (2认同)

and*_*net 6

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

  • 模板函数对于使用不同类型只编写一次代码很有用。
  • 虚拟函数对于为不同的类拥有一个公共接口很有用。

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

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

正方形面积为1,圆面积为3.1415926535897932385

在这里试试