xamix
xamix

Reputation: 13

C++ inherited operator '<<' on templated class

I have the following C++ code, which demonstrate my problem. My goal is to override the stream operator in inherited class in order to allow me to print specific stream depending on object type:

#include <iostream>
#include <unordered_set>

using namespace std;

template <typename T>
class Base {
    public:
        Base(){}
        Base(T n): value_(n){}

        friend inline ostream &operator<<(ostream &os, const Base &b) {
            b.to_str(os);
            return os;
        }

    protected:

        T value_;

        // All object should implement this function
        virtual void to_str(ostream& os) const {
            os << value_;
        }
};

template <typename T>
class Child: public Base<T> {
    public:
        Child(T n): Base<T>(n){}

    protected:
        void to_str(ostream& os) const override {
         os << "{";
            for (auto v = this->value_.begin(); v != this->value_.end(); v++) {
                if(v != this->value_.begin())
                    os << ",";
                os << (*v);
            }
            os << "}";
        }
};

int main()
{
    Base<string> b("base");
    Child<unordered_set<string>> c({"child"});
    cout << "b: " << b << endl;
    cout << "c: " << c << endl;

    return 0;
}

For now the code is not compiling:

main.cpp:31:16: error: no match for ‘operator<<’ (operand types are
‘std::ostream {aka std::basic_ostream}’ and ‘const std::unordered_set
>’)

It seem that the virtual method to_str() from base is used by the compiler instead of the overrided one for the Child class. If I comment the body of the base to_str() function then it compile and print the correct result for unordered_set Child class but then it print nothing with the base implementation. So the override is working, but why it do not compile when base to_str() have a body?

How can I force the compiler to use the one from the derived one (Child)?

Regards

Upvotes: 1

Views: 80

Answers (3)

xamix
xamix

Reputation: 13

This is what I finally implemented by using the "pure virtual solution", Tested with some more basic type:

#include <iostream>
#include <unordered_set>

using namespace std;

template <typename T>
class Base_ {

    public:
        Base_(){}
        Base_(T n): value_(n){}

        friend inline ostream &operator<<(ostream &os, const Base_ &b) {
            b.to_str(os);
            return os;
        }

    protected:
        T value_;

        // All object should implement this function
        virtual void to_str(ostream& os) const = 0;
};

template <typename T>
class Base: public Base_<T> {
    public:
        Base(){}
        Base(T n): Base_<T>(n){}

    protected:

        // All object should implement this function
        void to_str(ostream& os) const override {
            os << this->value_;
        }
};

template <typename T>
class Child: public Base_<T> {
    public:
        Child(T n): Base_<T>(n){}

    protected:
        void to_str(ostream& os) const override {
         os << "{";
            for (auto v = this->value_.begin(); v != this->value_.end(); v++) {
                if(v != this->value_.begin())
                    os << ",";
                os << (*v);
            }
            os << "}";
        }
};

template <typename T>
class Boolean: public Base_<T> {
    public:
        Boolean(T n): Base_<T>(n){}

    protected:
        void to_str(ostream& os) const override {
         os << (this->value_ ? "true" : "false");
        }
};

int main()
{
    Base<string> s("string");
    Base<int> i(42);
    Boolean<bool> b(true);
    Child<unordered_set<string>> u({"child1", "child2"});
    cout << "s: " << s << endl;
    cout << "i: " << i << endl;
    cout << "b: " << b << endl;
    cout << "u: " << u << endl;

    return 0;
}

Result:

s: string
i: 42
b: true
u: {child2,child1}

Upvotes: 0

NathanOliver
NathanOliver

Reputation: 180630

The virtual call actually does happen. You can see that by changing the code to

template <typename T>
class Base {
    public:
        Base(){}
        Base(T n): value_(n){}

        friend inline ostream &operator<<(ostream &os, const Base &b) {
            b.to_str(os);
            return os;
        }

    protected:

        T value_;

        virtual void to_str(ostream& os) const = 0;
};

template <typename T>
class Child: public Base<T> {
    public:
        Child(T n): Base<T>(n){}

    protected:
        void to_str(ostream& os) const override {
         os << "{";
            for (auto v = this->value_.begin(); v != this->value_.end(); v++) {
                if(v != this->value_.begin())
                    os << ",";
                os << (*v);
            }
            os << "}";
        }
};

int main()
{
    Child<unordered_set<string>> c({"child"});
    cout << "c: " << c << endl;
    return 0;
}

What is actually happening is that in Base the compiler is going to stamp out

Base<unordered_set<string>>::to_str

and that function is invalid since os << value_ is not defined. As you can see you need to either make it pure virtual, or put in a stub that will compile no matter what value_ is.

Upvotes: 0

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275405

The base to_str gets compiled even if it is never run.

So

    virtual void to_str(ostream& os) const {
        os << value_;
    }

fails to compile.

When you create a type with a vtable, its entries are (well, I believe the standard says "can be") populated even if they aren't called. This is different than a "normal" template class, where unused methods have thier bodies "skipped".

It is possible you want the CRTP for compile-time polymorphism.

template <class T, class D_in=void>
class Base {
    using D=std::conditional_t< std::is_same<D_in,void>{}, Base, D_in >;
    public:
        Base(){}
        Base(T n): value_(n){}

        friend inline ostream &operator<<(ostream &os, const Base &b) {
            static_cast<D const&>(b).to_str(os);
            return os;
        }

    protected:

        T value_;

        // All object should implement this function
        void to_str(ostream& os) const {
            os << value_;
        }
};

template <typename T>
class Child: public Base<T, Child<T>> {
    friend class Base<T, Child<T>>;
    public:
        Child(T n): Base<T>(n){}

    protected:
        void to_str(ostream& os) const {
         os << "{";
            for (auto v = this->value_.begin(); v != this->value_.end(); v++) {
                if(v != this->value_.begin())
                    os << ",";
                os << (*v);
            }
            os << "}";
        }
};

or somesuch.

Upvotes: 1

Related Questions