Home > database >  Implementing specific template instances in source with inherited template parameters
Implementing specific template instances in source with inherited template parameters

Time:06-18

I have the following template class:

// A.hpp

class A_data {
public:
  int foo = 1;

  virtual ~A_data() = default;
};

template <class C = A_data> class A {
public:
  int foo() const; // Not implemented here

  A() { d = new C; }
  ~A() { delete d; }

  C* d;
};

I implement the default instance of A:

// A.cpp

#include "A.hpp"

// Template source implementation
template <> int A<A_data>::foo() const { return d->foo; }

Now, I can use the class with the default instance:

// main.cpp

#include <iostream>

#include "A.hpp"

int main() {
  A a;
  std::cout << a.foo(); // Prints 1
}

However, suppose I want to create a class which inherits A, along with its own data class which inherits A_data, like so:

// A.hpp

class B_data : public A_data {
public:
  int bar = 2;

  virtual ~B_data() = default;
};

// Template here is in case something needs to inherit B, like in A.
template <class C = B_data> class B : public A<C> {
public:
  int bar() const; // Not implemented here, just like A::foo.
};

// A.cpp

template <> int B<B_data>::bar() const { return d->bar; }

This results in the following problem:

#include <iostream>

#include "A.hpp"

int main() {
  B b;
  std::cout << b.bar(); // Works fine, prints 2
  std::cout << b.foo(); // Linker error: no implementation of A<B_data>::foo!
}

As you can probably glean from the example, the intent is to have a class with its own data container (in this case A and A_data), such that other classes can inherit it (B), and can optionally choose to expand the data container by inherting the parent's container (B_data).

I can get around this problem by defining the implementation in A.hpp, but in this case I don't want to provide multiple interfaces of A other than A_data and its derived classes. If I really wanted to, I could implement each instance in A.cpp, but it adds nothing of value since the implementation itself is invariant.

My expectation was that B<B_data> would be able to "know" about A<A_data> and use A <A_data>::foo, since B inherits from A. Obvously, since B<B_data> and B<A_data> are not related, this is not the case -- but can it be?

Can I achieve this "expanding class" interface without exposing the implementation in the header? If not, is there a better alternative that would still allow me to maintain this simple interface?

Part of the reason templates seemed to be appealing here as opposed to regular class inheritence is because they allow to automatically dereference the correct container data without doing any type of pointer casts. Example with regular classes:

#include "A.hpp"

int A::foo() const { return d->foo; }
int B::bar() const { return d->bar; } // Compiler error: No member 'bar' in 'A_data'

CodePudding user response:

The problem is that you've explicitly specialized foo for the template argument A_data instead of B_data.

To solve this just change the explicit specialization to be for the argument B_data as shown below:

A.cpp

#include "A.hpp"

//----------------vvvvvv---------------------------------->changed from A_data to B_data
template <> int A<B_data>::foo() const { return d->foo; }
template <> int B<B_data>::bar() const { return d->bar; }

main.cpp

#include <iostream>

#include "A.hpp"

int main() {
  B b;
  std::cout << b.bar(); // Works fine, prints 2
  std::cout << b.foo(); // WORKS FINE, prints 1
}

Working demo

The output of the above modified program is:

21

Explanation

B is instantiated with B_data and so it inherits from A<B_data>. Note carefully that it inherits from A<B_data> and not A<A_data>. This means that foo's implementation for A<B_data> should be provided. But you never provided foo's implementation for A<B_data> because what you actually provided was implementation of foo for A<A_data> and hence the error. Thus by changing A_data to B_data as shown in the modified code above, we get rid of the error.

CodePudding user response:

If you want to stick to templates, you can implement this 'mirrored inheritance' as follows: make B<B_data> inherit from A<A_data> instead of A<B_data> by declaring in B_data (or other possible template argument types) an alias base to a whatever type you want to consider base for your purposes (it's ambiguous in general, since there may be many bases of a class) and making B<C> inherit from A<C::base>. Then, for B<C>, since d is of type C::base*, it can still hold a pointer to C which is derived from C::base (assuming C::base is set adequately and is an actual base). But in this case, we need to provide a custom default constructor in B so that it constructs C object for d, not C::base which would happen if default constructor for A<C::base> would be called by implicitly defaulted default constructor of B<C> (note that there's no need to provide destructor since delete d from A<C::base>'s destructor will work correctly if C::base has virtual destructor). Then, since inside B<C> we know how we constructed the object d points to, we can safely assume we know its dynamic type and access it through static_cast-ed pointer (for safety you could e.g. add dynamic_cast-check inside assert for Debug-builds check).

Example implementation (see also comments, in particular for some changes of other aspects of your code):

// A.hpp

class A_data {
public:
    int foo = 1;

    virtual ~A_data() = default;
};

template <class C = A_data> class A {
public:
    int foo() const;

    A() { d = new C; }
    // 'virtual' is not strictly needed in our particular case since we don't delete B
    // through A*, but still it is safer and best practice with it than without
    virtual ~A() { delete d; }

    C* d;
};

class B_data : public A_data {
public:
    using base = A_data;
    int bar = 2;

    // virtual ~B_data() = default; implicitly defaulted and virtual
};

template <class C = B_data> class B : public A<typename C::base> {
public:
    B() { this->d = new C; } // 'this' needed to trigger dependent name lookup
    int bar() const;
};

// A.cpp

#include "A.hpp"

template <> int A<A_data>::foo() const { return d->foo; }
template <> int B<B_data>::bar() const { return static_cast<B_data*>(d)->bar; }

// main.cpp

#include <iostream>

#include "A.hpp"

int main() {
    B b;
    std::cout << b.bar();
    std::cout << b.foo();
}

Note also that if you want to use different template arguments C, you may want to avoid code duplication in specialization definitions in A.cpp. There, you can define your templated functions usually (not as specializations), but to force instantiations use explicit template instantiations. E.g. suppose there's B_data_2 class defined like this:

// in A.hpp
class B_data_2 : public A_data {
public:
    using base = A_data;
    int bar = 3;
};

It also has bar public data member, but with different default initializer. Now we can add another specialization for B<B_data_2>::bar() to A.cpp, but it will be basically the same as B<B_data>::bar(), so code duplication arises:

// in A.cpp
template <> int A<A_data>::foo() const { return d->foo; }
template <> int B<B_data>::bar() const { return static_cast<B_data*>(d)->bar; }
template <> int B<B_data_2>::bar() const { return static_cast<B_data_2*>(d)->bar; }

Instead, if we replace specializations with general definitions followed by explicit instantiation definitions of methods, we have:

// in A.cpp
template<class C> int A<C>::foo() const { return d->foo; }
template int A<A_data>::foo() const;

template<class C> int B<C>::bar() const {
    return static_cast<C*>(this->d)->bar; // Again 'this' to enable dependent name lookup
}
template int B<B_data>::bar() const;
template int B<B_data_2>::bar() const;

This obviously scales better, but to avoid doing explicit instantiation for every member function of the same specialization, we can do even better - explicitly instantiate whole class specializations, which will, in particular, automatically instantiate all (non-templated) member functions of that specializations:

// in A.cpp
template<class C> int A<C>::foo() const { return d->foo; }

template<class C> int B<C>::bar() const {
    return static_cast<C*>(this->d)->bar; // Yet again 'this' to enable dependent name lookup
}

template class A<A_data>;
template class B<B_data>;
template class B<B_data_2>;

Now we can test and see that it compiles and works:

// main.cpp
#include <iostream>

#include "A.hpp"

int main() {
    B b;
    B<B_data_2> b_2;
    std::cout << b.bar() << b.foo() << std::endl; // 21
    std::cout << b_2.bar() << b_2.foo() << std::endl; // 31
}
  • Related