DsCpp
DsCpp

Reputation: 2489

template specialization sub class

I have a problem that can be minimized to the following example

#include <iostream>
#include <string>


class A{
  public:
    const char* chr;
    A(){chr = "aaa";}
};

class B : A{
  public:
    const char* chr;
    B(){chr = "bbb";}
};

template <class T>
std::string to_str(T) = delete;

template<>
inline std::string
to_str<A>(A object) {
    std::string str;
    return str.assign((object.chr));
}

int main() {
  A a;
  B b;
  std::cout << to_str(b) << std::endl;
}

when changing it to std::cout << to_str(a) << std::endl; the code runs and prints 'aaa', but like this, it stops at compilation and outputs

main.cpp: In function 'int main()':
main.cpp:30:24: error: use of deleted function 'std::__cxx11::string to_str(T) [with T = B; std::__cxx11::string = std::__cxx11::basic_string<char>]'
   std::cout << to_str(b) << std::endl;
                        ^
main.cpp:18:13: note: declared here
 std::string to_str(T) = delete;
             ^~~~~~

exit status 1

now lets say i have a lot of classes that inherit A, can i 'tell' the compiler they all can go to the same function (the one accepts A)?

Thanks.

Upvotes: 1

Views: 99

Answers (4)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275405

Template function specialization does not work that way. There is no overload resolution; it merely permits replacing a specific function body with specific template arguments with your specialized one. It is rarely useful.

What you want is overload resolution, possibly with tag dispatch.

First remove this completely:

template <class T>
std::string to_str(T) = delete;

next, write an overload:

inline std::string to_str(A object) {
  std::string str;
  return str.assign((object.chr));
}

and done. Overload resolution dispatches B to the A overload.

Your next problem is slicing. B has two members named chr: A::chr and B::chr. For no good reason. In addition you are needlessly copying A (or the A subobject of B).

inline std::string to_str(A const& object) {
  std::string str;
  return str.assign((object.chr));
}

this avoids a needless copy of A.

class A{
  public:
    const char* chr;
    A(){chr = "aaa";}
};

class B : public A{ // public, not private
  public:
    // const char* chr; // no need for this
    B(){chr = "bbb";}
};

Upvotes: 1

AndyG
AndyG

Reputation: 41100

I question if this really is a minimized example of your problem. I think that what's happened is that some important details got lost in the translation, because the code you've shown us here has a number of issues.

  1. B inherits privately from A. There is no way we can really treat a B like an A in this case.

If you changed inheritance to public, then we could attempt to force a B in like so:

class B : public A{/*...*/};
// ...
std::cout << to_str(*static_cast<A*>(&b)) << std::endl;

But the output will remain "aaa", which leads me to my next points

  1. Your to_str specialization for A accepts by value. This is important because even if we wanted to force a B in, we end up slicing the object, this matters because

  2. B redefines the const char* chr effectively hiding A::chr, and since we've sliced, there's no way to recover B's chr.

We could start fixing things but first fixing the slicing by accepting A by reference instead (or const reference), and always preferring an overload instead of a template specialization for a function:

std::string to_str(A& object) {/*...*/}
  1. The next problem is that there is no way direct to recover B's chr from an instance of A. We could go one of two ways here.
    1. Use a std::string member in A and do not redeclare it in any derived class, then derived classes can set it on initialization.

Example:

class A{
  public:
    std::string chr;
    A():chr{"aaa"}{}
};

class B : public A{
  public:
    B(){chr = "bbb";}
};  
  1. We write a virtual const char* get_char() method in A that derived classes can override.

Example:

class A{
  public:
    const char* chr;
    A(){chr = "aaa";}
    virtual const char* get_chr() const{return chr;}
};

class B : public A{
  public:
    const char* chr;
    B(){chr = "bbb";}
    const char* get_chr() const override {return chr;}
};

template <class T>
std::string to_str(T) = delete;

std::string to_str(A& object) {
    std::string str;
    return str.assign((object.get_chr()));
// ...
std::cout << to_str(*static_cast<A*>(&b)) << std::endl;

Note that at this point we're still forcing each B to be an A, which leads me to my next point

  1. template <class T> std::string to_str(T) = delete; will always exactly match every type you don't explicitly specialize for, being preferred in the worst case and causing ambiguity in the best case.

    If you don't have any control over this function, then we're stuck with what we've got. However, if we do, then we can achieve what we need using type_traits to accept anything that is derived from A.

In this way we can keep your private inheritance, and also keep your redeclared chr member, while simultaneously disabling to_str for everything else and not requiring we static_cast our b.

Example:

#include <type_traits>

class A{
  public:
    const char* chr;
    A(){chr = "aaa";}
};

class B : A{
  public:
    const char* chr;
    B(){chr = "bbb";}
};

template<class T, class = std::enable_if_t<std::is_base_of<A, T>::value, int>>
inline std::string to_str(T& object) {
    std::string str;
    return str.assign((object.chr));
}

int main() {
  A a;
  B b;
  std::cout << to_str(b) << std::endl;
}

Overall, I think that the best approach would be to give A a protected std::string chr that each derived class sets on initialization, and then have your to_string function that is specialized for A& (as an overload) print that member.


Edit: I forgot one last note. Issue #6: There are no virtual members in A. Therefore you will never be able to dynamic_cast a pointer to A to any derived class.

Upvotes: 1

max66
max66

Reputation: 66200

can i 'tell' the compiler they all can go to the same function(the one accepts A)?

Yes, using SFINAE and std::is_base_of

template <typename T>
typename std::enable_if<std::is_base_of<A, T>::value, std::string>::type
      to_str (T const & t)
 { return t.chr; }

The following is a full working example

#include <type_traits>
#include <iostream>
#include <string>

struct A     { char const * chr; A() : chr{"aaa"} {} };
struct B : A { char const * chr; B() : chr{"bbb"} {} };
struct C     { char const * chr; C() : chr{"ccc"} {} };    

template <typename T>
typename std::enable_if<std::is_base_of<A, T>::value, std::string>::type
      to_str (T const & t)
 { return t.chr; }

int main()
 {
   A a;
   B b;
   C c;

   std::cout << to_str(a) << std::endl;    // print aaa
   std::cout << to_str(b) << std::endl;    // print bbb
   // std::cout << to_str(c) << std::endl; // compilation error
 }

Upvotes: 5

Simone Cifani
Simone Cifani

Reputation: 784

According to 14.7.3 [temp.expl.spec] paragraph 1, only non-deleted function templates may be explicitly specialized

C++ Standard Core Language Defect Reports and Accepted Issues, Revision 97

So, if you change

template <class T>
std::string to_str(T) = delete;

in, for example,

template <class T>
std::string to_str(T) { return ""; }

everything should work

Upvotes: -3

Related Questions