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

问题描述 投票:273回答:11

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

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

c++ templates virtual-functions c++-faq
11个回答
305
投票

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

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

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


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
投票

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

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

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

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

#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

试试吧here


115
投票

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

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


32
投票

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

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


16
投票

Virtual Function Tables

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

[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指针的值,第二次获取方法的地址)。非虚函数不会发生此运行时活动,因为编译器会根据指针类型在编译时专门解析非虚函数。


My problem, or how I came here

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

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

Solution

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

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


这是另一个堆栈溢出的答案,解决方法:need a virtual template member workaround


12
投票

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

输出是:

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条目。

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

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


6
投票

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


3
投票

要回答问题的第二部分:

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

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

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

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


2
投票

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

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

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

只需要在派生实现(TMethodAImpl)中实现模板方法BImpl并使用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可以公开并直接使用)。我添加它,所以它看起来像一个真正的“虚拟”模板方法。

© www.soinside.com 2019 - 2024. All rights reserved.