模板使用不当

Bad Use Of Templates?

本文关键字:使用不当      更新时间:2023-10-16

我知道模板有点被指责为二进制膨胀,我也明白模板只是一个模式。我真的不明白具体的细节。

很多时候,我看到像下面这样的代码,它返回一个基类指针。
class GameObject
{
public:
    Component* getComponent(std::string key);
};
static_cast<Comp>(obj.getComponent("Comp"));

而不是使方法成为模板方法。

class GameObject
{
public:
    template<typename T>
    T* getComponent(std::string key);
};
obj.getComponent<Comp>("Comp");

这是风格上的还是与模板相关的性能损失?

方法接受的"key"实际上似乎是一个类型(要返回的组件的类型),这表明直到运行时才真正知道该类型。如果是这种情况,那么像模板这样的编译时机制就行不通了。

唯一的选择是返回一个基类指针。通常在使用此模式时,只针对基类调用虚方法——因此派生类的实际类型无关紧要(因此不需要static_cast或dynamic_cast)。

编辑:

正如PhilCK在注释中指出的那样,该类型实际上是在编译时已知的。如果是这样的话,就不需要动态类型查找了,可以使用简单的工厂方法:

class GameObject {
    A getComponentA();
    B getComponentB();
    C getComponentC();
    // etc.
}
// which is more or less identical to:
class ComponentFactory {
    public:
    virtual Component* create() = 0;
};
class GameObject {
    std::map<std::string,ComponentFactory> m;
    public:
    GameObject() {
        // presumably a private map has to be populated with string -> factory methods here
    }
    template<class T>
    T* getComponent(const std::string& s) { 
        // either the static_cast is needed here, or an alternate (messier)
        // implementation of getComponent is needed.
        // it's still possible for the wrong type to be passed (by mistake)
        // and the compiler won't catch it (with static_cast). Since this could lead
        // to heap corruption the only safe way with this type of 
        // implementation is dynamic_cast.
        return static_cast<T>(m[s].create());
    }
};
// this doesn't even compile:
// return types:
class GameObject {
    template <class T>
    T* getComponent(const std::string& s) {
        if (s == "A") return new A();
        else if (s == "B") return new B();
        // etc..
        else throw runtime_error("bad type");
    }
}

在我看来有两种选择。

1)使用简单的工厂方法,在这种情况下根本不需要模板。2)将map -> factory方法实现与dynamic_cast一起使用(这似乎违背了使用动态类型创建的目的),并且如果不需要动态类型,则实际上不必要地复杂

按零顺序应该不会有性能差异。

模板化方法将为每个T创建一个成员函数。这并不会使代码本身变慢,但由于代码局部性问题,可能会使函数调用的成本更高,因为强制转换可能在远离调用站点的地方进行。只有分析才能告诉你。

可能还有其他需要担心的瓶颈,比如通过值传递参数而不是通过const引用。

Component* getComponent(std::string key);
Component* getComponent(const std::string& key);

同样,这个函数可能是const .

如果我正确理解你的例子,问题不是关于模板膨胀二进制文件,而是是否使用模板或多态性。

简而言之,模板在编译时允许灵活的类型,而多态性通过使用继承在运行时允许灵活的类型。当然,还有很多事情要做。使用哪一个取决于你的实际需要。在您的情况下,我会说这取决于不同类型组件之间的关系。如果它们共享功能,它们应该有一个共享的基类,这里是Component。如果它们没有共同之处,你应该使用模板。

WRT性能,除非您是实时的,否则这通常不会是在两者之间进行选择的主要原因。模板可能比常规类生成更少的二进制,因为只有您使用的才会被实例化,并且虚函数(用于继承)的开销很小。所以这通常是风格(或最佳实践)的问题。

编译器将模板代码转换为程序集。如果对许多类型都这样做,将导致汇编代码块被重复多次。这可以通过不使用模板来避免。或者经常使用模板的是非模板函数的包装函数,或者是少量的模板函数。性能损失是由于较大的文件造成的。另一方面,由于使用指针/引用而不是对象,普通函数可能会更复杂。并且编译器内联这些代码的机会更少。

膨胀的另一个原因是模板函数在使用它们的每个dll中都被实例化。

膨胀可能是模板的一个问题,而膨胀本身会导致性能问题。然而,一个好的编译器可以优化一些膨胀问题,并且有一些编程技术(在需要的时候)可以避免其余的问题。

这在这里不应该是一个问题——你的模板函数可能很琐碎,因此需要内联。原则上,这本身可能会导致一些膨胀,但由于函数是微不足道的,"膨胀"的量也是微不足道的。

模板示例中的一个严重问题是,它试图在返回值上重载纯方法。有些语言(如Ada)可以使用返回类型来解析重载,但c++不是其中之一。

因此,您需要一些其他方法来确定您打算调用的方法的特定版本。最简单的方法之一(如果只需要考虑几个类型)是分别声明和定义这些方法,每个方法有不同的名称。

看来你的用法涉及到这样或那样的强制转换。在这种情况下,我将在模板方法中进行强制转换,该方法在您的控制之下。

这两种方法之间的一个主要区别是,第一个代码片段不检查强制转换是否有效(除非使用dynamic_cast,否则不可能检查)。

在第二个代码片段中,如果要转换到的类型不是该标识符所指的对象的类型,则类可能抛出或返回NULL指针。

我假设模板膨胀(如果有的话)将是最小的,因为无论如何您可以将实际工作委托给非模板助手,模板除了执行static_cast之外唯一要做的事情。当内联时,它将与非模板版本相同(除了您有机会通过检查结果类型使函数更智能)。