Is there real static polymorphism in C++?

2019-03-18 06:19发布

Here is a simple code in C++:

#include <iostream>
#include <typeinfo>

template<typename T>
void function()
{
   std::cout << typeid(T).name() << std::endl;
}

int main()
{
   function<int>();
   function<double>();
   return 0;
}

I have read that templates in C++ is a compile-time feature, which is not like generics in C#/Java.

So as I understood, the C++ compiler will divide a single defined function into various number (depends on calls count with different type) of functions.

Am I right or not? I'm not an expert in C++ compilers, so I'm asking a piece of advice from you.

If my suggestion about compiler output is correct, I want to know if I can describe the code above as static polymorphism?

Because it seems to be not overriding, but just calling a copy from executable or... it doesn't matter what does application have in output binary image, but only the important part is in C++ code level and I musn't look at how does compiler produce an output.

5条回答
唯我独甜
2楼-- · 2019-03-18 07:08

Wikipedia lists three types of polymorphism:

  • If a function denotes different and potentially heterogeneous implementations depending on a limited range of individually specified types and combinations, it is called ad hoc polymorphism. Ad hoc polymorphism is supported in many languages using function overloading.

  • If the code is written without mention of any specific type and thus can be used transparently with any number of new types, it is called parametric polymorphism. In the object-oriented programming community, this is often known as generics or generic programming. In the functional programming community, this is often simply called polymorphism.

  • Subtyping (or inclusion polymorphism) is a concept wherein a name may denote instances of many different classes as long as they are related by some common superclass. In object-oriented programming, this is often referred to simply as polymorphism.

The first one refers to function overloading. The third type refers to late binding or runtime polymorphism, the kind you would see for example in inheritance. The second one is what we're interested in.

Templates are a compile-time construct and type deduction is a process when the compiler automatically figures out the template arguments. This is where static polymorphism comes in.

For example:

template <typename T, typename U>
auto func(const T& t, const U& u) -> decltype(t + u)
{
   return (t + u);
}

This will work for any two types with compatible plus operators. There's no need to specify the template argument if the compiler can figure it out. It would be ad hoc polymorphism if you wrote function overloads that performed different behavior, for example string concatenation vs integer addition.

However, in your example, you have instantiations for your functions that are distinct, function<int> and function<double>. Here's a quote:

To be polymorphic, [a()] must be able to operate with values of at least two distinct types (e.g. int and double), finding and executing type-appropriate code.

In that case, the instantiations are specific for the type in which they were instantiated, so there is no polymorphism involved.

查看更多
Melony?
3楼-- · 2019-03-18 07:14

Is there real static polymorhism in C++?

Absolutely - there are three mechanisms for static polymorphism: templates, macros and function overloading.

So as I understood, the C++ compiler will divide a single defined function into various number (depends on calls count with different type) of functions. Am I right or not?

That's the general idea. The number of functions that get instantiated depends on the number of permutations of template parameters, which may be explicitly specified as in function<int> and function<double> or - for templates that use the template parameters to match function arguments - automatically derived from the function arguments, for example:

template <typename T, size_t N>
void f(T (&array)[N])
{ }

double d[2];
f(d);   // instantiates/uses f<double, 2>()

You should end up with a single copy of each instantiated template in the executable binary image.


I want to know if I can describe the code above as static polymorphism?

Not really.

  • template<> function is instantiated for two types

    • crucially, polymorphism is not used to choose which of the two instantiations of function to dispatch to at the call sites

    • trivially, during such instantiations typeid(T) is evaluated for int and double and effectively behaves polymorphically from a programmer perspective (it's a compiler keyword though - implementation unknown)

  • trivially, a mix of static and nominally dynamic (but here likely optimisable to static) polymorphism supports your use of std::cout

Background - polymorphism and code generation

The requirement I consider crucial for polymorphism is:

  • when code is compiled (be it "normal" code or per template instantiation or macro substitution), the compiler automatically chooses (creates if necessary) - and either inlines or calls - distinct type-appropriate behaviour (machine code)

    • i.e. code selection/creation is done by the compiler based only on the type(s) of variable(s) involved, rather than being explicitly hard-coded by the programmer's choice between distinct function names / instantiations each only capable of handling one type or permutation of types

    • for example, std::cout << x; polymorphically invokes different code as the type of x is varied but still outputs x's value, whereas the non-polymorphic printf("%d", x) handles ints but needs to be manually modified to printf("%c", x); if x becomes a char.

But, what we're trying to achieve with polymorphism is a bit more general:

  • reuse of algorithmic code for multiple types of data without embedding explicit type-detection and branching code

    • that is, without the program source code containing if (type == X) f1(x) else f2(x);-style code
  • reduced maintenance burden as after explicitly changing a variable's type fewer consequent changes need to be manually made throughout the source code

These bigger-picture aspects are supported in C++ as follows:

  1. instantiation of the same source code to generate distinct behaviours (machine code) for some other type or permutation of types (this is an aspect of parametric polymorphism),

    • actually known as "instantiation" for templates and "substitution" for preprocessor macros, but I'll use "instantiation" hereafter for convenience; conceptually, re-compilation or re-interpretation...
  2. implicit dispatch (static or dynamic) to distinct behaviour (machine code) appropriate to the distinct type(s) of data being processed.

...and in some minor ways per my answer at Polymorphism in c++

Different types of polymorphism involve either or both of these:

  • dispatch (2) can happen during instantiation (1) for templates and preprocessor macros,

  • instantiation (1) normally happens during dispatch (2) for templates (with no matching full specialisation) and function-like macros (kind of cyclic, though macros don't expand recursively)

  • dispatch (2) can be happen without instantiation (1) when the compiler selects a pre-existing function overload or template specialisation, or when the compiler triggers virtual/dynamic dispatch.

What does your code actually use?

function<int> and function<double> reuse the function template code to create distinct code for each of those types, so you are getting instantiation (1) as above. But, you are hard-coding which instantiation to call rather than having the compiler implicitly select an instantiation based on the type of some parameter, i.e. so you don't directly utilise implicit dispatch ala (2) when calling function. Indeed, function lacks a parameter that the compiler could use for implicit selection of a template instantiation.

Instantiation (1) alone is not enough to consider your code to have used polymorphism. Still, you've achieved convenient code re-use.

So what would be unambiguously polymorphic?

To illustrate how templates can support dispatch (2) as well as instantiation (1) and unarguably provide "polymorphism", consider:

template<typename T>
void function(T t)
{
    std::cout << typeid(T).name() << std::endl;
}

function(4);      // note: int argument, use function<int>(...)
function(12.3);   // note: double argument, use function<double>(...)

The above code also utilises the implicit dispatch to type-appropriate code - aspect "2." above - of polymorphism.


Non type parameters

Interestingly, C++ provides the ability to instantiate templates with integral parameters such as boolean, int and pointer constants, and use them for all manner of things without varying your data types, and therefore without any polymorphism involved. Macros are even more flexible.


Note that using a template in a C.R.T.P. style is NOT a requirement for static polymorphism - it's an example application thereof. During instantiation, the compiler exhibits static polymorphism when matching operations to implementations in the parameter-specified type.


Discussion on terminology

Getting a definitive definition of polymorphism is difficult. wikipedia quotes Bjarne Stroustrup's online Glossary "providing a single interface to entities of different types": this implies struct X { void f(); }; struct Y { void f(); }; already manifests polymorphism, but IMHO we only get polymorphism when we use the correspondence of interface from client code, e.g. template <typename T> void poly(T& t) { t.f(); } requires static polymorphic dispatch to t.f() for each instantiation.

查看更多
萌系小妹纸
4楼-- · 2019-03-18 07:17

There is no static polymorphism in your example because there is no polymorphism. This is because function<int>() does not look the same as function<double>().

Examples of static polymorphism would include simple function overloading, function templates that can work with type deduction, type traits, and the curiously recurring template pattern (CRTP). So this variation on your example would qualify as static polymorphism:

#include <iostream>
#include <typeinfo>

template<typename T>
void function(T)
{
   std::cout << typeid(T).name() << std::endl;
}

int main()
{
   function(0);   // T is int
   function(0.0); // T is double
   return 0;
}

Here is another example:

template<typename T>
void function(T t)
{
  t.foo();
}

struct Foo() 
{
  void foo() const {}
};

struct Bar() 
{
  void foo() const {}
};

int main()
{
  Foo f;
  Bar b;
  function(f); // T is Foo
  function(b); // T is Bar
}
查看更多
Summer. ? 凉城
5楼-- · 2019-03-18 07:19

For c++ the term 'static polymorphism' is normally used for e.g. the CRTP type design patterns:

template<typename Derived> 
class Base
{
      void someFunc() {
          static_cast<Derived*>(this)->someOtherFunc();
      };
};

class ADerived : public Base<ADerived>
{
    void someOtherFunc() { 
        // ... 
    }
};

It generally means that types and inheritance constraints are deduced and verified at compile/link time. The compiler will emit error messages if operations are missing or invalid on the specified types. In that sense it's not really polymorphism.

查看更多
Explosion°爆炸
6楼-- · 2019-03-18 07:19

While it can be argued that the example in the OP does not exhibit static polymorphism, the use of specialization can make a more compelling case:

template<class T>
class Base
{
public:
      int a() { return 7; }
};

template<>
class Base<int>
{
public:
      int a() { return 42; }
};

template<>
class Base<double>
{
public:
      int a() { return 121; }
};

We see here that for most classes a() will return 7; The specialized (derived) instanciations for int and double can have radically different behaviors, demonstrated in the simple case by the differing return values, the same can be done for templates with for example int parameters, and can exhibit what can be oddly termed as static recursive polymoprphism.

While the term polymorphic is possibly being stretched, the concept is definately there. What is missing is not the ability to redefine functions, but the ability for specialized classes to automatically inherit functions that are not changing behavior.

查看更多
登录 后发表回答