Question

I wanted to share a strange example with you guys that I stumbled upon and that kept me thinking for two days.

For this example to work you need:

  • triangle-shaped virtual inheritance (on member function getAsString())
  • member function specialization of a template class (here, Value<bool>::getAsString()) overriding the virtual function
  • (automatic) inlining by the compiler

You start with a template class that virtually inherits a common interface - i.e. a set of virtual functions. Later, we will specialize one of these virtual functions. Inlining may then cause our specilization to get overloooked.

// test1.cpp and test2.cpp
#include <string>

class ValueInterface_common
{
public:
  virtual ~ValueInterface_common() {}
  virtual const std::string getAsString() const=0;
};

template <class T>
class Value :
  virtual public ValueInterface_common
{
public:
  virtual ~Value() {}
  const std::string getAsString() const;
};

template <class T>
inline const std::string Value<T>::getAsString() const
{
  return std::string("other type");
}   

Next, we have to inherit this Value class and the interface in a Parameter class that itself needs to be templated as well:

// test1.cpp
template <class T>
class Parameter :
  virtual public Value<T>,
  virtual public ValueInterface_common
{
public:
  virtual ~Parameter() {}
  const std::string getAsString() const;
};

template<typename T>
inline const std::string Parameter<T>::getAsString() const
{
  return Value<T>::getAsString();
}

Now, do not(!) give the forward declaration of a specialization for Value for type equaling bool ...

// NOT in: test1.cpp
template <>
const std::string Value<bool>::getAsString() const;

But instead simply give its definition like this ...

// test2.cpp
template <>
const std::string Value<bool>::getAsString() const
{
  return std::string("bool");
}

.. but in another module (that's important)!

And finally, we have a main() function to test what is happening:

// test1.cpp
#include <iostream>

int main(int argc, char **argv)
{
  ValueInterface_common *paraminterface = new Parameter<bool>();
  Parameter<int> paramint;
  Value<int> valint;
  Value<bool> valbool;
  Parameter<bool> parambool;

  std::cout << "paramint is " << paramint.getAsString() << std::endl;
  std::cout << "parambool is " << parambool.getAsString() << std::endl;
  std::cout << "valint is " << valint.getAsString() << std::endl;
  std::cout << "valbool is " << valbool.getAsString() << std::endl;
  std::cout << "parambool as PI is " << paraminterface->getAsString() << std::endl;

  delete paraminterface;

  return 0;
}

If you compile the code as follows (I placed it into two modules named test1.cpp and test2.cpp where the latter only contains the specialization and necessary declarations):

g++ -O3 -g test1.cpp test2.cpp -o test && ./test

The output is

paramint is other type
parambool is other type
valint is other type
valbool is bool
parambool as PI is other type

If you compile with -O0 or just -fno-inline - or also if you do give the forward declaration of the specialization - the result becomes:

paramint is other type
parambool is bool
valint is other type
valbool is bool
parambool as PI is bool

Funny, isn't it?

My explanation so far is: Inlining is at work in the first module (test.cpp). The required template functions get instantiated but some just end up being inlined in the calls to Parameter<bool>::getAsString(). On the other hand, for valbool this did not work but the template is instantiated and used as a function. The linker then finds both the instantiated template function and the specialized one given in the second module and decides for the latter.

What do you think of it?

  • do you consider this behavior a bug?
  • Why does inlining work for Parameter<bool>::getAsString() but not for Value<bool>::getAsString() although both override a virtual function?
Was it helpful?

Solution

I speculate that you have an ODR problem, so there is little sense in guessing why some compiler optimization behaves differently from another compiler setting.

In essence, the One Definition Rule states that the same entity should have the exact same definition throughout an application, otherwise the effects are undefined.

The fundamental problem is that the code that doesn't see the specialized version of your class template member function might still compile, is likely to link, and sometimes might even run. This is because in the absence of (a forward declaration of) the explicit specialization, the non-specialized version kicks in, likely implementing a generic functionality that works for your specialized type as well.

So if you are lucky, you get a compiler error about missing declarations/definitions, but if you are really unlucky you get "working" code that does not what you intend it to do.

The fix: always include (forward) declarations of all template specializations. It's best to put those in a single header and include that header from all clients that call your class for any possible template argument.

// my_template.hpp
#include "my_template_fwd.hpp"
#include "my_template_primary.hpp"
#include "my_template_spec_some_type.hpp" 

// my_template_fwd.hpp
template<typename> class my_template; // forward declaration of the primary template

// my_template_primary.hpp
#include "my_template_fwd.hpp"
template<typename T> class my_template { /* full definition */ };

// my_template_spec_some_type.hpp
#include "my_template_fwd.hpp"
template<> class my_template<some_type> { /* full definition */ };

// some_client_module.hpp
#include "my_template.hpp" // no ODR possible, compiler will always see unique definition

Obviously, you could reorganize the naming by making subdirectories for template specializations and change the include paths accordingly.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top