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


304

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

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


12
我遇到了类似的问题,并且还了解到同时使用虚拟和模板存在争议。我的解决方案是编写在派生类之间通用的模板魔术,然后调用一个纯虚拟函数来完成专门的工作。当然,这与我的问题的性质有关,因此可能并非在每种情况下都有效。
陶Szelei

Answers:


329

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

一旦确定了运行时系统,它将需要调用模板化的虚拟函数,编译已全部完成,并且编译器无法再生成适当的实例。因此,您不能具有虚拟成员功能模板。

但是,结合多态性和模板可以使用一些强大而有趣的技术,特别是所谓的类型擦除


32
我没有看到语言方面的原因,只是实现方面的原因。vtables不是该语言的一部分-只是编译器实现该语言的标准方式。
gerardw 2013年

16
Virtual functions are all about the run-time system figuring out which function to call at run-time-很抱歉,但这是一种相当错误的方式,并且令人困惑。只是间接的,没有“运行时计算”,在编译时就知道要调用的函数是vtable中第n个指针指向的函数。“搞清楚”意味着存在类型检查,事实并非如此。Once the run-time system figured out it would need to call a templatized virtual function-在编译时就知道函数是否为虚函数。
dtech

8
@ddriver:1.如果编译器看到void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); },则它“知道”在调用点调用了哪个函数cb.f(),而对于则不知道vb.f()。后者已被发现在运行时运行时系统。您是否要称其为“弄清楚”,或者说效率更高或更低,都不会改变这些事实。
2015年

8
@ddriver:2 .(成员)函数模板的实例是(成员)函数,因此将指向此类实例的指针放入vtable根本没有问题。但是仅在编译调用方时才知道需要哪些模板实例,而在编译基类和派生类时才建立vtable。这些都是单独编译的。更糟糕的是,新的派生类可以在运行时链接到正在运行的系统中(想想您的浏览器会动态加载插件)。创建新的派生类时,甚至调用者的源代码也可能长时间丢失。
2015年

9
@sbi:您为什么基于我的名字做假设?我没有混淆泛型和模板。我知道Java的泛型纯粹是运行时。您没有详尽解释为什么C ++中无法拥有虚拟成员函数模板,但是InQsitive却没有。您过度地将模板和虚拟机制简化为“编译时间”与“运行时间”,并得出结论:“您不能拥有虚拟成员函数模板”。我引用了InQsitive的答案,该答案引用了“ C ++模板完整指南”。我不认为这是“挥手”。祝你今天愉快。
Javanator

133

从C ++模板完整指南:

成员函数模板不能声明为虚拟。由于虚拟函数调用机制的通常实现使用固定大小的表,每个虚拟函数只有一个条目,因此施加了此约束。但是,直到翻译完整个程序后,成员函数模板的实例化数量才固定。因此,支持虚拟成员函数模板将需要支持C ++编译器和链接器中的一种全新的机制。相反,类模板的普通成员可以是虚拟的,因为在实例化一个类时其数目是固定的


8
我认为当今的C ++编译器和链接器,尤其是具有链接时间优化支持的链接器,应该能够在链接时生成所需的vtable和偏移量。那么也许我们将在C ++ 2b中获得此功能?
凯·佩兹克

33

C ++目前不允许虚拟模板成员函数。最可能的原因是实施它的复杂性。拉金德拉(Rajendra)有充分的理由说明为什么现在无法完成,但是只要合理更改标准,它就有可能实现。如果考虑虚拟函数调用的位置,特别是要计算出实际上存在多少个模板化函数实例并建立vtable似乎很困难。标准人员现在还有很多其他事情要做,C ++ 1x对于编译器编写者来说也是很多工作。

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


5
@pmr:可能会从编译函数时甚至不存在的代码中调用虚拟函数。编译器将如何确定为根本不存在的代码生成(理论上的)虚拟模板成员函数的哪些实例?
2010年

2
@sbi:是的,单独编译将是一个巨大的问题。我根本不是C ++编译器的专家,所以我无法提供解决方案。通常,与模板化函数一样,应该在每个编译单元中再次实例化它,对吗?那不能解决问题吗?
2010年

2
@sbi(如果您指的是动态加载库),这是模板类/函数的普遍问题,而不仅仅是虚拟模板方法的问题。
橡树

“ C ++不允许[...]” -希望看到对标准的引用(无论是撰写答案时的最新版本还是八年后的最新版本)...
阿空加瓜

19

虚函数表

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

[20.3]如何调用虚拟和非虚拟成员函数之间的区别?

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

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

编译器为具有至少一个虚函数的每个类创建一个v表。例如,如果Circle类具有针对draw()和move()以及resize()的虚函数,则即使有成千上万个Circle对象,也将只有一个与Circle类关联的v表。每个Circle对象将指向Circle v表。v表本身具有指向类中每个虚拟函数的指针。例如,Circle 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

16

在Windows 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&)'

因此很明显:

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

19
类模板可以具有虚拟成员函数。成员函数可能既不是成员函数模板又不是虚拟成员函数。
James McNellis 2010年

1
它实际上在gcc 4.4.3中失败。在我的系统上肯定可以使用Ubuntu 10.04
blueskin 2010年

3
这与所提问题完全不同。这里,整个基类都已模板化。我以前已经编译过这种东西。这也可以在Visual Studio 2010上进行编译
ds-bos-msk

14

不,他们不能。但:

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

如果您只想拥有一个通用接口并将实现推迟到子类,则效果几乎相同。


3
如果有人好奇,这就是CRTP。
迈克尔·崔

1
但这对那些具有类层次结构并希望能够调用指向基类的指针的虚拟方法的情况无济于事。您的Foo指针限定为Foo<Bar>,不能指向Foo<Barf>Foo<XXX>
凯·佩兹克

@KaiPetzke:您不能构造不受约束的指针,不。但是,您可以将不需要知道具体类型的任何代码模板化,其效果大致相同(至少在概念上-显然完全不同的实现)。
汤姆

8

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


9
我的好奇心是:为什么?编译器这样做面临哪些问题?
WannaBeGeek

1
您需要在范围内声明(至少是为了使类型正确)。标准(和语言)要求在使用的标识符范围内声明。
2010年

4

在其他答案中,建议的模板功能是外观,没有任何实际好处。

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

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

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

#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

要回答问题的第二部分:

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

这不是要做的不合理的事情。例如,Java(每种方法都是虚拟的)对通用方法没有任何问题。

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

解决此问题的方法是将类型擦除与boost :: any_range和boost :: function一起使用,这将允许您接受通用的迭代器或仿函数,而无需将函数设为模板。


6
Java泛型是用于转换的语法糖。它们与模板不同。
Brice M. Dempsey 2014年

2
@ BriceM.Dempsey:您可以说强制转换是Java实现泛型的方式,而不是相反的方式……从语义上讲,用例的摘要是有效的IMO。
einpoklum's

2

如果事先知道模板方法的类型集,则有一种“虚拟模板方法”的解决方法。

为了说明这一点,在下面的示例中,仅使用两种类型(intdouble)。

在这里,“虚拟”模板方法(Base::Method)调用了相应的虚拟方法(之一Base::VMethod),而虚拟方法又调用了模板方法实现(Impl::TMethod)。

只需TMethod在派生的实现(AImplBImpl)和use中实现模板方法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可以公开并直接使用)。我添加了它,使它看起来像是一种实际的“虚拟”模板方法。


我在解决工作中的问题时想出了这种解决方案。似乎与上述的Mark Essel相似,但我希望可以更好地实施和解释。
sad1raf

我已经将此视为代码混淆,并且您仍然无法避免Base每次需要调用参数类型与到目前为止实现的参数类型不兼容的模板函数时都必须修改原始类的事实。模板的目的是避免这种必要……
空加瓜

Essels的方法完全不同:普通的虚函数接受不同的模板实例化-派生类中的最终模板函数仅用于避免代码重复,甚至在基类中都不起作用...
Aconcagua

2

尽管我认为许多人已经回答了一个较老的问题,但我认为一种简洁的方法(与发布的其他方法没有什么不同)是使用次要宏来帮助简化类声明的重复。

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

这样做的好处是,当添加新支持的类型时,都可以从抽象标头中完成所有操作,并且可以放弃在多个源/标头文件中对其进行纠正。


0

至少对于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

0

尝试这个:

在classeder.h中编写:

template <typename T>
class Example{
public:
    T c_value;

    Example(){}

    T Set(T variable)
    {
          return variable;
    }

    virtual Example VirtualFunc(Example paraM)
    {
         return paraM.Set(c_value);
    }

检查是否可以在main.cpp中编写以下代码:

#include <iostream>
#include <classeder.h>

int main()
{
     Example exmpl;
     exmpl.c_value = "Hello, world!";
     std::cout << exmpl.VirtualFunc(exmpl);
     return 0;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.