Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic implementation of operator<< function in terms of the dump member function

Tags:

c++

All my classes implement a dump member function, e.g.:

struct A {
    template <typename charT>
    std::basic_ostream<charT> &
    dump(std::basic_ostream<charT> &o) const {
        return (o << x);
    }
    int x = 5;
};

I would like to implement an operator<< function once for all such classes:

template<typename charT, typename T>
std::basic_ostream<charT> &
operator<< (std::basic_ostream<charT> &o, const T &t) {
    return t.dump(o);
}

The problem is that all types are caught by this template, including the standard types. Is there a way to get around this problem?

like image 532
AlwaysLearning Avatar asked Dec 09 '15 12:12

AlwaysLearning


4 Answers

template <typename T, typename charT>
auto operator<< (std::basic_ostream<charT> & str, const T & t) -> decltype(t.dump(str))
{
    static_assert(std::is_same
                   <decltype(t.dump(str)), 
                    std::basic_ostream<charT> &>::value, 
                  ".dump(ostream&) does not return ostream& !");

    return t.dump(str);
}

This overloads operator<< only for types that define an appropriate dump member.

Edit: added static_assert for better messages.

like image 124
n. 1.8e9-where's-my-share m. Avatar answered Oct 19 '22 01:10

n. 1.8e9-where's-my-share m.


You could make an empty base class, say:

struct HasDump {};

And make HasDump a base of all your classes, that is:

struct A : HasDump ( ...

Then wrap your operator<< with std::enable_if and std::is_base_of so it only applies when HasDump is a base of T.

(I haven't focused on C++ for a year or two so this advice might be a little rusty)

like image 25
Clinton Avatar answered Oct 19 '22 02:10

Clinton


Generally this would be the advisable way, IMO:

struct A {
    int x = 5;

    friend std::ostream & operator<<(std::ostream &os, const A& a){
        return (os << a.x);
    }
};

Reason: 'friend' functions and << operator overloading: What is the proper way to overload an operator for a class?

If you really want to have a dedicated dump method, you could define a base class "collecting" dumpable objects.

like image 22
luk32 Avatar answered Oct 19 '22 00:10

luk32


Just added this for fun. In case you happen to have more than one method that prints/dumps on different classes:

#include <iostream>
#include <type_traits>

namespace tests {

    // this is a utility class to help us figure out whether a class
    // has a member function called dump that takes a reference to 
    // an ostream
    struct has_dump
    {
        // We will only be checking the TYPE of the returned
        // value of these functions, so there is no need (in fact we
        // *must not*) to provide a definition
        template<class T, class Char>
        static auto test(const T* t, std::basic_ostream<Char>& os)
        -> decltype(t->dump(os), std::true_type());

        // the comma operator in decltype works in the same way as the
        // comma operator everywhere else. It simply evaluates each
        // expression and returns the result of the last one
        // so if t->dump(os) is valid, the expression is equivalent to
        // decltype(std::true_type()) which is the type yielded by default-
        // constructing a true_type... which is true_type!


        // The above decltype will fail to compile if t->dump(os) is not
        // a valid expression. In this case, the compiler will fall back
        // to selecting this next function. this is because the overload
        // that takes a const T* is *more specific* than the one that
        // takes (...) [any arguments] so the compiler will prefer it
        // if it's well formed.

        // this one could be written like this:
        // template<class T, class Char>
        // static std::false_type test(...);
        // I just happen to use the same syntax as the first one to
        // show that they are related.

        template<class T, class Char>
        static auto test(...) -> decltype(std::false_type());
    };

    // ditto for T::print(ostream&) const    
    struct has_print
    {
        template<class T, class Char>
        static auto test(const T* t, std::basic_ostream<Char>& os)
        -> decltype(t->print(os), std::true_type());

        template<class T, class Char>
        static auto test(...) -> decltype(std::false_type());
    };
}

// constexpr means it's evaluated at compile time. This means we can
// use the result in a template expansion.
// depending on whether the expression t->dump(os) is well formed or not
// (see above) it will either return a std::true_type::value (true!) 
// or a std::false_type::value (false!)

template<class T, class Char>
static constexpr bool has_dump() {
    // the reinterpret cast stuff is so we can pass a reference without
    // actually constructing an object. remember we're being evaluated
    // during compile time, so we can't go creating ostream objects here - 
    // they don't have constexpr constructors.
    return decltype(tests::has_dump::test<T, Char>(nullptr,
                                                   *reinterpret_cast<std::basic_ostream<Char>*>(0)))::value;
}

template<class T, class Char>
static constexpr bool has_print() {
    return decltype(tests::has_print::test<T, Char>(nullptr,
                                                   *reinterpret_cast<std::basic_ostream<Char>*>(0)))::value;
}

// so now we can use our constexpr functions has_dump<> and has_print<>
// in a template expansion, because they are known at compile time.
// std::enable_if_t will ensure that the template function is only
// well formed if our condition is true, so we avoid duplicate
// definitions.
// the use of the alternative operator representations make this
// a little more readable IMHO: http://en.cppreference.com/w/cpp/language/operator_alternative

template<class T, class Char>
auto operator<< (std::basic_ostream<Char>& os, const T& t)
-> std::enable_if_t< has_dump<T, Char>() and not has_print<T, Char>(), std::basic_ostream<Char>&>
{
    t.dump(os);
    return os;
}

template<class T, class Char>
auto operator<< (std::basic_ostream<Char>& os, const T& t)
-> std::enable_if_t< has_print<T, Char>() and not has_dump<T, Char>(), std::basic_ostream<Char>&>
{
    t.print(os);
    return os;
}

template<class T, class Char>
auto operator<< (std::basic_ostream<Char>& os, const T& t)
-> std::enable_if_t< has_print<T, Char>() and has_dump<T, Char>(), std::basic_ostream<Char>&>
{
    // because of the above test, this function is only compiled
    // if T has both dump(ostream&) and print(ostream&) defined.

    t.dump(os);
    os << ":";
    t.print(os);
    return os;
}



struct base
{
    template<class Char>
    void dump(std::basic_ostream<Char>& os) const
    {
        os << x;
    }

    int x = 5;
};
namespace animals
{
    class donkey : public base
    {

    public:
        template<class Char>
        void dump(std::basic_ostream<Char>& s) const {
            s << "donkey: ";
            base::dump(s);
        }
    };

    class horse // not dumpable, but is printable
    {
    public:
        template<class Char>
        void print(std::basic_ostream<Char>& s) const {
            s << "horse";
        }
    };

    // this class provides both dump() and print()        
    class banana : public base
    {
    public:

        void dump(std::ostream& os) const {
            os << "banana!?!";
            base::dump(os);
        }

        void print(std::ostream& os) const {
            os << ":printed";
        }

    };
}


auto main() -> int
{
    using namespace std;

    animals::donkey d;
    animals::horse h;

    cout << d << ", " << h << ", " << animals::banana() << endl;

    return 0;
}

expected output:

donkey: 5, horse, banana!?!5::printed
like image 33
Richard Hodges Avatar answered Oct 19 '22 02:10

Richard Hodges