可以在C ++中完全避免模板吗?

时间:2011-07-19 02:23:50

标签: c++ templates polymorphism

我的问题,在最后一段,需要(在我看来)一些解释性设置。基本上,我想知道你是否可以避免使用模板而不是你创建所有你想成为的模板类而是从一个声明你将要使用的虚方法的基类继承,其中包括一个内存分配函数,当实现时,将返回指向派生(非基础)类型的指针。

BEGIN设置

C ++似乎没有“通用基类”的概念,从中可以隐式得出所有内容;我想这个课程的定义如下:

class universal_base
{
};

当然,现在我已经定义了它,我可以让我的所有类都来自它。然后,由于多态性,我传递给universal_base的任何引用或指针将基本上与模板参数相同:

template <typename T>

class C
{
   T &x;
   int f(T &y);
   C(T &z): x(z + 1) {}
};

class C
{
   universal_base &x;
   int f(universal_base &y);
   C(universal_base &z): x(z + 1) {}
};

不同之处在于,在第一个构造中,表达式z + 1不能保证有效;您只需告诉用户T必须超载operator+。在第二个构造中,我可以向universal_base添加虚拟此类运算符:

// in universal_base
public:
 virtual universal_base& operator+(const universal_base &x) = 0;

并在实现中使用typeiddynamic_cast来使参数正确。这样,就不可能编写格式错误的代码,因为如果你没有实现operator+,编译器会抱怨。

当然,这种方式无法声明非引用类型的成员:

class C: public universal_base
{
  universal_base x; // Error: universal_base is a virtual type
};

但是,通过仔细使用初始化可以解决这个问题。事实上,如果我想为上面的内容创建一个模板,

template <typename T>
class C: public universal_base
{
  T x;
};

我几乎肯定会在某个时候给它T类型的对象。在这种情况下,我没有理由不能做到以下几点:

class universal_base
{
  public:
   virtual universal_base& clone() = 0;
};

class C: public universal_base
{
  universal_base &x;
  C(universal_base &y) : x(y.clone()) {}
}

实际上,我创建了一个在运行时确定的类型变量。这当然要求C类型的每个对象都要进行适当的初始化,但我认为这不是一个巨大的牺牲。

这不是学术性的,因为它具有以下用途:如果我正在编写一个旨在链接到其他程序并以某种通用方式处理其数据的模块,我不可能知道将要使用的类型。模板在这种情况下没有帮助,但上面的技术工作正常。

END设置

所以,我的问题:这是否完全取代模板,模块化初始化的东西?某种程度上它效率低或危险吗?

7 个答案:

答案 0 :(得分:13)

模板提供编译时多态性;虚函数提供运行时多态性。

当然,在编译时你无法做任何在运行时无法做到的事情。不同之处是:

  1. 性能
  2. 编译时检查
  3. 使用typeiddynamic_cast会导致运行时性能下降。一般来说虚拟功能也是如此;在现代CPU上,对可变位置的调用比调用固定位置要慢几百倍(因为它们往往会破坏指令预取机制)。

    所以表现绝对是一个问题。

    下一步...除非您强制每个类实现每个运算符,否则您将面临运行时检查失败的风险。如果模板试图在未实现它的类型上调用+,则结果将是编译时错误。

    通常,模板的静态特性允许更好的编译时检查和优化。但是你的想法没有任何语义错误。

答案 1 :(得分:4)

  

这不是学术性的,因为它具有以下用途:如果我正在编写一个旨在链接到其他程序并以某种通用方式处理其数据的模块,我不可能知道将要使用的类型。模板在这种情况下没有帮助,但上面的技术工作正常。

如果您只想简单地链接和运行,那么模板就没用了:它们是编译时多态的,而不是链接时或运行时。虚拟分派是一个选项,但只有在你至少可以定义一个类型应该支持的公共接口时才能真正起作用(它使安全使用更加困难,但如果有必要,它们可以提供一些“发现”机制,这样调用代码就可以解决了哪些位实际工作)。使用“胖接口”(在Stroustrup的C ++编程语言中查找)是一个脆弱而丑陋的解决方案,但有时它可能是最好的。

根据您的评论进行修改...

模板可以做的一些事情,虚拟调度不能:

  • 编译时优化:内联和随后的优化,例如死代码消除,固定大小的循环展开。
  • 奇怪的重复模板模式(CRTP)提供了构造函数中可调用的实现,
  • 替换失败不是错误(SFINAE)允许更智能的函数调用解析,包括对参数类型的接口进行内省的能力有限
  • 模板化类可以提供适用于某些参数类型/值但不适用于其他参数的函数,只有当这些函数实际用于不受支持的参数时才会生成编译时错误(例如,+可能会被“转发” “参数的+ - 无论是成员还是非成员 - 但仅在可用的情况下)
  • 专业化:允许模板以不同方式处理特殊情况。
  • 概念(还没有在C ++中 - 也不会生成C ++ 11--但阅读它们会强调模板如何依赖于语义,这比虚拟调度中的设置函数签名更灵活)
  • 模板是参数多态的一种形式,因此客户端代码可以简单地尝试使用它们而不引入基类,或者为虚拟分派指针占用空间。
    • 运行时多态性不适用于访问共享内存中对象的多个进程,因为由一个进程设置的虚拟分派指针可能不会指向另一个进程的正确位置。
  • 避免(慢)堆。虚拟分派通常涉及在堆上创建对象,因为它们的大小在派生层次之间变化,并且在编译时是未知的。模板避免了这些成本。
    • 有时可以将数组大小规定为模板参数,避免动态分配。

答案 2 :(得分:4)

不,理论上甚至没有可能,没有抛弃利用大量共性的能力,或者发明一些不基于继承的其他机制(例如,面向方面的编程) )。

当然,最终可以使用它(例如,Smalltalk),但理论上也可以用纯汇编语言编写任何你想要的东西。但是,它们都不适合当前的模型并且完全考虑C ++。你可能有足够的理由以不同的方式做事,但你应该明白你正在削减粮食,可以这么说。

答案 3 :(得分:3)

我认为反对通用“对象”类的论点在这里是相关的:

Why doesn't C++ have a universal class Object?

我打算强调“编译时间与运行时间”并输入安全性,但我认为其他答案也很好。

我可能会为这样一个模块使用C接口:用C ++编写一个带有模板的动态,类型安全的后端,然后用一个带有extern“C”函数的包装器来使用它。

答案 4 :(得分:2)

您概述的一般方法相当普遍,尤其是在Java中。您可能会遇到问题,试图将其推广到您描述的那一点。例如,如果你强迫每个人都实现operator +,你是否也强迫每个人都实现所有其他运算符? (+ =, - =,/,++, - 等)如果你这样做,你最终会失去类型检查的大部分目的(即如果你允许所有类型转换为其他所有类型并拥有所有类型运算符),你最终会得到你的类型,它们具有对类型没有意义的运算符的虚拟实现。

现在,关于是否替换模板的问题,答案是否定的,但它可能会取代您想到的模板的特定用途。

答案 5 :(得分:2)

不,这是简短而又甜蜜的答案。

C ++不是像Java这样的语言进行动态分配而设计的 - 你的程序运行速度会非常慢,内存管理将是一个令人难以置信的婊子,并且有一些模板结构无论如何都无法替代。例如,您无法使用此类universal_base参与重载解析。

  

这不是学术性的,因为它具有以下用途:如果我正在写作   一个模块,旨在链接到其他程序和句柄   他们的数据以某种通用的方式,我不可能知道那些类型   将会被使用。模板在这种情况下没有帮助,但是   上面的技术工作正常。

您在头文件中发送通用代码,而不是预编译库。有许多库只是标题,这很好,很多是标题和库。如果查看最新COM库的标头,它们会在标头中实现一个模板QueryInterface,该模板委托给库中的链接QueryInterface。运送库时,大多数甚至所有代码都写在标题中并没有错。看看标准库 - 几乎所有都是模板。这应该是一个很大的提示 - 好的C ++使用通用代码模板。如果他们不想要一个他们不会使用C ++的快速库,那么没有人会使用像狗一样运行的C ++库。

让我这样说:模板存在并且被广泛使用,因为问题的其他解决方案以各种可能的方式吸收。即使是像Java和C#这样具有通用基础和垃圾收集器的OOP疯狂语言也必须引入他们自己的“模板”,甚至打破了C#的后向兼容性,因为它们要好得多。

答案 6 :(得分:0)

你正试图写一种名为C的古老而晦涩的语言。谣言有足够的时间和禅修,几乎所有的C ++都可以被处理成这种古老的表现形式。它只需要时间,蚱蜢。

严肃地说,您正在寻找的一般模式似乎是一个界面和实现。

在大多数情况下可以避免模板只是复制代码并手动定制它,但是当你需要处理这样的情况时,拥有一个定义所有重要内容的基类就是你通常开始的地方。 p>

例如:

class IAddable
{
public:
    virtual IAddable * Add(IAddable * pOther) = 0;
};

class Number
    : public IAddable
{
public:
    virtual IAddable * Add(IAddable * pOther)
    {
        return new Number(this, pOther);
    }

    Number(Number * a, Number * b)
        : m_Value(a->m_Value + b->m_Value)
    { };

private:
    int m_Value;
};

这是一个基本的,不安全的例子,但它说明了这一点。所以是的,你的概念通常是合理的。

就使用虚拟运算符而言,如果这样的事情是可能的(我自己不使用它),它似乎会起作用。

需要考虑的是确保可以操作两个操作数,您可能希望在界面中定义类型检查(类似于COM的IUnknown::QueryInterface);

您还必须观察如何处理继承类型;因为你和编译器都不知道它们是什么,所以会涉及很多指针。确保你经常检查错误和错误的类型(返回错误值或转换类型或扔掉或者你有什么)。