2011-09-04

Templates vs. virtual functions in C++, part I

Virtual functions and templates are two major tools for supporting polymorphism in C++.

Virtual functions are basic blocks for supporting dynamic polymorphism and thus object-oriented paradigm in C++. Templates are useful for generic programming and metaprogramming; they allow static polymorphism.

Each tool has its own applications. Here is an advise from Bjarne Stroustrup from his book The C++ Programming Language, 13.8:
  1. Prefer a template over derived classes when run-time efficiency is at a premium.
  2. Prefer derived classes over a template if adding new variants without recompilation is important.
  3. Prefer a template over derived classes when no common base can be defined.
  4. Prefer a template over derived classes when built-in types and structures with compatibility constraints are important.
Note that it leaves only one case for virtual functions: when adding new variants without recompilation is important. And if you don't need this benefit, then start with templates. There are several reasons for it:

Templates are non-intrusive

Once type meets requirements imposed by function or class template, it can be used unmodified. Usually that means that type is required to provide some functions or member-functions with predefined semantics.

To pass object to function that operates via pointers/references to root of some hierarchy, type should be specially prepared (either it should be derived from this root directly on indirectly from the beginning, or it should be adapted later) and should have some member-functions with exact signature (*):

    template <class T>
    void f1(T const& t)
    {
        // no requirements on T except that it should provide
        // member-function do_something that
        // returns something convertible to bool
        // and can be called without arguments (so can have i.e default parameters)
        bool flag = t.do_something();
    }

    class Base
    {
    public:
        virtual bool do_something() const;
        // ...
    };

    void f2(Base const& t)
    {
        // first, t should be of class derived from Base
        // second, Derived::do_something() should follow 
        // the signature of Base::do_something()
        bool flag = t.do_something();
    }
Templates produce faster and smaller code

Call it "premature optimization" if you like. But if you are implementing a library you often don't know what will be on the critical path of an application using it. And if you are implementing a C++ library, you'd better make everything as fast as possible, and without using unnecessary memory.

    template <class T>
    void f1(T const& t)
    {
        // no virtual call overhead
        // t doesn't have to store a pointer to vtable
        bool flag = t.do_something();
    }

    void f2(Base const& t)
    {
        // probably virtual call overhead
        // stores pointer to vtable
        bool flag = t.do_something();

        // that can be undesirable especially for small objects
        // and trivial and/or frequently called operations

    }

Templates don't lose information about types

    template <class T>
    void f1(T const& t)
    {
        // here you know the exact type of t and can make further decisions
        // based on it (perhaps using traits or strategies)
    }

    void f2(Base const& t)
    {
        // here information about exact type erased
        // you cannot get it back without run-time type queries
        // (which is inefficient and doesn't scale on a number of derived classes)
    }

Making up hierarchy costs nothing

... but not vice versa. You can always wrap your types designed for using as template parameters with object-oriented hierarchy -- without run-time penalty (**), but once you have "virtuality" in place, its overhead with you forever!

I consider this one the most important. Because it means that, other thing being equal, you probably won't make mistake starting with templates. It is possible because of the previous benefits: performance, non-intrusiveness and keeping type information.

In part II we will deal with some arguments in favor of using dynamic polymorphism.

_____________________________________________________________________


(*) There is relaxed for covariant return types (and for exception specifications which can be stricter in derived class -- provided you consider them part of signature)


(**) Of course, compared with having hierarchy from the start.

No comments:

Post a Comment