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

Can a class member function template be virtual?

提问人:WannaBeGeek 提问时间:3/1/2010 最后编辑:Jan SchultkeWannaBeGeek 更新时间:9/28/2023 访问量:278779

问:

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

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

C++ 模板 虚拟 函数模板

评论

15赞 Tamás Szelei 10/26/2011
我也遇到了类似的问题,也了解到同时使用虚拟和模板是有争议的。我的解决方案是编写在派生类中常见的模板魔术,并调用一个执行专用部分的纯虚函数。这当然与我问题的性质有关,因此可能并非在所有情况下都有效。

答:

13赞 dirkgently 3/1/2010 #1

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

评论

10赞 WannaBeGeek 3/1/2010
我的好奇心是:为什么?编译器在这样做时会遇到什么问题?
2赞 dirkgently 3/1/2010
您需要在作用域中声明(至少,为了获得正确的类型)。标准(和语言)要求在您使用的标识符的范围内声明。
376赞 sbi 3/1/2010 #2

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

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

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

评论

55赞 gerardw 10/3/2013
我没有看到语言原因,只有实现原因。vtables 不是语言的一部分,只是编译器实现语言的标准方式。
37赞 dtech 8/12/2015
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
15赞 sbi 8/15/2015
@ddriver: 1.如果编译器看到 ,那么它“知道”在调用点调用了哪个函数,而不知道 。后者必须在运行时由运行时系统找出。无论你是否想称之为“弄清楚”,以及这是否更有效率,都不会改变这些事实。void f(concr_base& cb, virt_base& vb) { cb.f(); vb.f(); }cb.f()vb.f()
10赞 sbi 8/15/2015
@ddriver: 2. (成员)函数模板的实例是(成员)函数,因此将指向此类实例的指针放入 vtable 完全没有问题。但是,只有在编译调用方时才知道需要哪些模板实例,而在编译基类和派生类时设置 vtables。而这些都是单独编译的。更糟糕的是,新的派生类可以在运行时链接到正在运行的系统中(想想你的浏览器动态加载插件)。在创建新的派生类时,甚至调用方的源代码也可能长期丢失。
17赞 Javanator 9/10/2016
@sbi:你为什么要根据我的名字做出假设?我没有混淆泛型和模板。我知道 Java 的泛型是纯粹的运行时。您没有详尽地解释为什么在 C++ 中不能使用虚拟成员函数模板,但 InQsitive 做到了。您过度简化了模板和虚拟机制,以“编译时”与“运行时”,并得出结论“您不能拥有虚拟成员函数模板”。我参考了InQsitive的答案,其中引用了“C++模板完整指南”。我不认为这是“挥手”。有好的一天。
43赞 pmr 3/1/2010 #3

C++ 目前不允许虚拟模板成员函数。最可能的原因是实现它的复杂性。Rajendra 给出了为什么现在不能这样做的充分理由,但通过对标准的合理更改是可能的。特别是,如果您考虑虚拟函数调用的位置,那么计算模板化函数实际存在多少个实例化并构建 vtable 似乎很困难。标准人员现在还有很多其他事情要做,C++1x 对编译器编写者来说也是很多工作。

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

评论

7赞 sbi 3/1/2010
@pmr:虚拟函数可能会从编译函数时甚至不存在的代码中调用。编译器将如何确定要为甚至不存在的代码生成哪些(理论上)虚拟模板成员函数的实例?
2赞 pmr 3/1/2010
@sbi:是的,单独编译将是一个巨大的问题。我根本不是C++编译器方面的专家,所以我无法提供解决方案。与一般的模板化函数一样,它应该在每个编译单元中再次实例化,对吧?这难道不能解决问题吗?
2赞 Oak 7/18/2011
@sbi,如果您指的是动态加载库,这是模板类/函数的普遍问题,而不仅仅是虚拟模板方法。
0赞 Aconcagua 1/4/2018
“C++ 不允许 [...]”- 希望看到对标准的引用(无论是编写答案时的最新标准还是八年后的最新标准)......
1赞 CygnusX1 7/8/2020
一种可能的解决方案是启用某种稳定的运行时类型反射,然后创建一个 (type, function-ptr) 而不是 vtable 的哈希映射。这是可行的。但非常复杂,与我们现在所拥有的非常不同。
19赞 Brent81 5/24/2010 #4

以下代码可以在 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 条目。

评论

28赞 James McNellis 5/24/2010
类模板可能具有虚拟成员函数。成员函数不能既是成员函数模板,也不能是虚拟成员函数。
1赞 Chenna V 11/21/2010
它实际上在 GCC 4.4.3 中失败了。在我的系统上肯定是 Ubuntu 10.04
4赞 ds-bos-msk 2/13/2014
这与问题所问的完全不同。这里是模板化的整个基类。我以前编译过这种东西。这也将在 Visual Studio 2010 上编译
3赞 exclipy 9/28/2011 #5

回答问题的第二部分:

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

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

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

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

评论

6赞 Brice M. Dempsey 6/5/2014
Java 泛型是用于强制转换的语法糖。它们与模板不同。
2赞 einpoklum 5/11/2016
@BriceM.Dempsey:你可以说强制转换是 Java 实现泛型的方式,而不是相反......从语义上讲,exclipy 呈现的用例是有效的 IMO。
28赞 Mark Essel 11/12/2011 #6

虚拟功能表

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

[20.3] 虚拟和非虚拟有什么区别 成员函数被调用?

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

相比之下,虚拟成员函数是动态解析的(在 运行时)。也就是说,成员函数是动态选择的(在 run-time) 基于对象的类型,而不是 指向该对象的指针/引用。这称为“动态绑定”。 大多数编译器使用以下技术的某种变体:如果 对象有一个或多个虚函数,编译器把一个隐藏的 对象中的指针称为“虚拟指针”或“V 指针”。这 V-Pointer 指向一个名为“virtual-table”或 “V型桌子。”

编译器为每个至少有一个 v-table 的类创建一个 v-table。 虚拟功能。例如,如果类 Circle 具有虚函数 对于 draw() 和 move() 和 resize(),正好有一个 v 表 与类 Circle 相关联,即使有无数个 Circle 对象,并且每个 Circle 对象的 v 指针将指向 到 Circle v 表。v-table 本身有指向每个 类中的虚拟函数。例如,Circle v 表将 有三个指针:指向 Circle::d raw() 的指针,指向 Circle::move() 和指向 Circle::resize() 的指针。

在分派虚拟功能期间,运行时系统将遵循 对象的 V 指针指向类的 V 表,然后遵循 V-Table 中适当的插槽到方法代码。

上述技术的空间成本开销是名义上的:额外的 每个对象的指针(但仅适用于需要执行动态操作的对象 binding),加上每个方法的额外指针(但仅适用于虚拟的 方法)。时间成本开销也是相当可观的:与 普通函数调用,一个虚函数调用需要两个额外的函数 fetches(一个用于获取 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 3/4/2020
我遇到了同样的情况,以及群众阶级的继承结构。宏有所帮助。
180赞 Sreeraj Chundayil 12/31/2014 #7

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

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

评论

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

不,他们不能。但:

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赞 Michael Choi 2/7/2019
如果有人好奇,这被称为 CRTP。
3赞 Kai Petzke 2/8/2019
但这对具有类层次结构并希望能够调用指向基类的指针的虚拟方法的情况没有帮助。指针的限定条件为 ,它不能指向 或 。FooFoo<Bar>Foo<Barf>Foo<XXX>
0赞 Tom 3/3/2020
@KaiPetzke:你不能构造一个不受约束的指针,不行。但是你可以对任何不需要知道具体类型的代码进行模板化,这具有大致相同的效果(至少在概念上 - 显然完全不同的实现)。
0赞 Maxim Sinev 8/12/2016 #9

至少在 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
2赞 sad1raf 3/30/2017 #10

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

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

其中,“虚拟”模板方法()调用相应的虚拟方法(其中之一),而虚拟方法又调用模板方法实现()。Base::MethodBase::VMethodImpl::TMethod

只需要在派生实现中实现模板方法(,)并使用。TMethodAImplBImplDerived<*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::MethodVMethod

评论

0赞 sad1raf 3/30/2017
我在解决工作中的问题时想出了这个解决方案。它似乎与上面的 Mark Esel 相似,但我希望能更好地实现和解释。
0赞 Aconcagua 1/4/2018
我已经将此定性为代码混淆,并且您仍然无法解决这样一个事实,即每次需要调用参数类型与迄今为止实现的参数类型不兼容的模板函数时,都必须修改原始类。避免这种必要性是模板的意图......Base
0赞 Aconcagua 1/4/2018
Essels 方法完全不同:普通的虚函数接受不同的模板实例化 - 派生类中的最终模板函数仅用于避免代码重复,甚至在基类中没有对应的部分......
7赞 andreaplanet 8/17/2018 #11

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

  • 模板函数对于仅使用一次编写代码很有用 不同的类型。
  • 虚函数对于为不同类提供通用接口很有用。

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

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

#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赞 mccatnm 9/4/2019 #12

虽然许多人已经回答了一个较老的问题,但我相信一个简洁的方法,与发布的其他方法没有太大区别,是使用一个小宏来帮助缓解类声明的重复。

// 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赞 Jaziri Rami 7/7/2020
“IMPL_RENDER() // 构建我们想要的函数”如何称呼?@mccatnm
0赞 mccatnm 7/9/2020
它纯粹是一个宏。在此示例中,您可以排除宏定义。它不是用来调用的,而是通过预编译器填写所需的函数。否则,您将不得不重新定义所有函数。(例如 、 等)()Box::render(int, char *)Box::render(int, short *)
0赞 Bernd 11/14/2020 #13

我目前的解决方案如下(禁用 RTTI - 您也可以使用 std::type_index):

#include <type_traits>
#include <iostream>
#include <tuple>

class Type
{
};

template<typename T>
class TypeImpl : public Type
{

};

template<typename T>
inline Type* typeOf() {
    static Type* typePtr = new TypeImpl<T>();
    return typePtr;
}

/* ------------- */

template<
    typename Calling
    , typename Result = void
    , typename From
    , typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action);

template<typename Cls>
class ChildClasses
{
public:
    using type = std::tuple<>;
};

template<typename... Childs>
class ChildClassesHelper
{
public:
    using type = std::tuple<Childs...>;
};

//--------------------------

class A;
class B;
class C;
class D;

template<>
class ChildClasses<A> : public ChildClassesHelper<B, C, D> {};

template<>
class ChildClasses<B> : public ChildClassesHelper<C, D> {};

template<>
class ChildClasses<C> : public ChildClassesHelper<D> {};

//-------------------------------------------

class A
{
public:
    virtual Type* GetType()
    {
        return typeOf<A>();
    }

    template<
        typename T,
        bool checkType = true
    >
        /*virtual*/void DoVirtualGeneric()
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<A>(this, [&](auto* other) -> decltype(auto)
                {
                    return other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "A";
    }
};

class B : public A
{
public:
    virtual Type* GetType()
    {
        return typeOf<B>();
    }
    template<
        typename T,
        bool checkType = true
    >
    /*virtual*/void DoVirtualGeneric() /*override*/
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<B>(this, [&](auto* other) -> decltype(auto)
                {
                    other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "B";
    }
};

class C : public B
{
public:
    virtual Type* GetType() {
        return typeOf<C>();
    }

    template<
        typename T,
        bool checkType = true
    >
    /*virtual*/void DoVirtualGeneric() /*override*/
    {
        if constexpr (checkType)
        {
            return DoComplexDispatch<C>(this, [&](auto* other) -> decltype(auto)
                {
                    other->template DoVirtualGeneric<T, false>();
                });
        }
        std::cout << "C";
    }
};

class D : public C
{
public:
    virtual Type* GetType() {
        return typeOf<D>();
    }
};

int main()
{
    A* a = new A();
    a->DoVirtualGeneric<int>();
}

// --------------------------

template<typename Tuple>
class RestTuple {};

template<
    template<typename...> typename Tuple,
    typename First,
    typename... Rest
>
class RestTuple<Tuple<First, Rest...>> {
public:
    using type = Tuple<Rest...>;
};

// -------------
template<
    typename CandidatesTuple
    , typename Result
    , typename From
    , typename Action
>
inline constexpr Result DoComplexDispatchInternal(From* from, Action&& action, Type* fromType)
{
    using FirstCandidate = std::tuple_element_t<0, CandidatesTuple>;

    if constexpr (std::tuple_size_v<CandidatesTuple> == 1)
    {
        return action(static_cast<FirstCandidate*>(from));
    }
    else {
        if (fromType == typeOf<FirstCandidate>())
        {
            return action(static_cast<FirstCandidate*>(from));
        }
        else {
            return DoComplexDispatchInternal<typename RestTuple<CandidatesTuple>::type, Result>(
                from, action, fromType
            );
        }
    }
}

template<
    typename Calling
    , typename Result
    , typename From
    , typename Action
>
inline Result DoComplexDispatch(From* from, Action&& action)
{
    using ChildsOfCalling = typename ChildClasses<Calling>::type;
    if constexpr (std::tuple_size_v<ChildsOfCalling> == 0)
    {
        return action(static_cast<Calling*>(from));
    }
    else {
        auto fromType = from->GetType();
        using Candidates = decltype(std::tuple_cat(std::declval<std::tuple<Calling>>(), std::declval<ChildsOfCalling>()));
        return DoComplexDispatchInternal<Candidates, Result>(
            from, std::forward<Action>(action), fromType
        );
    }
}

我唯一不喜欢的是您必须定义/注册所有子类。

-1赞 VINOTH ENERGETIC 8/13/2021 #14

在虚拟的情况下如何调用正确的函数?

Vtable 将包含类的每个虚函数的条目,在运行时它将选择特定函数的地址并调用相应的函数。

在虚拟的情况下,必须如何调用正确的函数以及函数模板?

对于函数模板,用户可以使用任何类型调用此函数。在这里,同一函数根据类型有多个版本。现在,在这种情况下,由于版本不同而对同一函数,必须维护 vtable 中的许多条目。

0赞 user13947194 11/18/2021 #15

我已经查看了所有 14 个答案,有些有虚拟模板功能无法工作的原因,有些则显示了解决方法。一个答案甚至表明,虚拟班级可以具有虚拟功能。这不应该太令人惊讶。

我的回答将给出一个直截了当的理由,为什么该标准不允许虚拟模板化函数。因为很多人一直在抱怨。首先,我不敢相信有些人评论说可以在编译时推导虚函数。这是我听过的最愚蠢的事情。

无论如何。我确信标准规定指向对象的 this 指针是其成员函数的第一个参数。

struct MyClass
{
 void myFunction();
}

// translate to
void myFunction(MyClass*);

现在我们很清楚这一点。然后,我们需要了解模板的转换规则。模板化参数极限于它可以隐式转换为的内容。我不记得全部了,但你可以检查完整的参考资料。例如,T* 可转换为常量 T*。数组可转换为指针。但是,派生类不能作为模板化参数转换为基类。C++ Primer

struct A {};
struct B : A {};

template<class T>
void myFunction(T&);

template<>
void myFunction<A>(A&) {}

int main()
{
 A a;
 B b;

 myFunction(a); //compiles perfectly
 myFunction((A&)b); // compiles nicely
 myFunction(b); //compiler error, use of undefined template function
}

所以我希望你明白我在哪里。您不能有虚拟模板函数,因为就编译器而言,它们是两个完全不同的函数;作为它们的隐式,此参数是不同类型的。

虚拟模板无法工作的另一个原因同样有效。由于虚拟表是快速实现虚拟功能的最佳方式。

评论

0赞 Sz. 2/23/2023
那么......既然隐式参数是隐式的:为什么它应该是一个模板参数呢?它可以被排除在整个恶作剧之外,然后解析的函数会像以前一样对它做任何事情(没有模板)。所以,对我来说,这与这个问题是正交的。this