5

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?

AlwaysLearning
  • 7,257
  • 4
  • 33
  • 68
  • 3
    Why don't you just overload `<<` instead of these dump methods? It's much more intuitive and trivial to implement. – erip Dec 09 '15 at 12:13
  • 1
    Why not simply overload `operator<<` inside `A`? – luk32 Dec 09 '15 at 12:13
  • @luk32 `operator<<` cannot be a member function, can it? So the `dump` member functions are more convenient, since they can access the data members directly (without the need to use the dot operator). – AlwaysLearning Dec 09 '15 at 12:25
  • @AlwaysLearning You are right, but there is a "canonical" way. I wrote it as the answer. – luk32 Dec 09 '15 at 12:28
  • @AlwaysLearning this is a very small convenience, so small I hesitate to even call it that. On the other hand, doing things the canonical, expected way is a very large convenience for people reading your code. It's common to put the convenience of the code reader ahead of the convenience of the code writer (real life code gets read much more than it gets written). – Nir Friedman Dec 09 '15 at 12:51

4 Answers4

9
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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • In C++14 you can drop the trailing return type :) – OMGtechy Dec 09 '15 at 12:23
  • Doesn't this solution have namespace pollution written all over it? – Richard Hodges Dec 09 '15 at 12:28
  • That is a nice answer (and I'm still learning this myself, so I don't have the syntax of the better answer) but I think it would be preferred to not just limit to T that has `dump(str)` defined but to T for which `dump(str)` returns `std::basic_ostream & str` (because well formed stream operations do that). So this is an issue with the meaning of "appropriate" in your answer. – JSF Dec 09 '15 at 12:43
  • @JSF This is also possible with more verbosity, but I would rather not do that. Instead I would add a `static_assert` inside `operator<<` (see updated answer). – n. m. could be an AI Dec 09 '15 at 13:45
  • @RichardHodges don't think so. No more than normal overloads of `operator<<`. – n. m. could be an AI Dec 09 '15 at 13:46
  • @RichardHodges How would this be updated in c++17 to use is_invocable? – Gardener Mar 23 '19 at 12:48
  • 1
    @Gardener I'm not sure it gets any easier with `is_invocable`. you'd have to call via a generic lambda in case the `dump` method was overloaded. – Richard Hodges Mar 24 '19 at 07:06
3

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)

Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 1
    This is a good answer for OP's problem, but I still think OP is overcomplicating a very simple problem. – erip Dec 09 '15 at 12:17
1

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.

Community
  • 1
  • 1
luk32
  • 15,812
  • 38
  • 62
  • The `dump` member functions are more convenient, since they can access the data members directly (without the need to use the dot operator). – AlwaysLearning Dec 09 '15 at 12:28
  • 1
    Debatable aesthetics. You save `a.` but you introduce a redundant method, and you clutter global namespace with overloaded `operator<<`. This method contains the overloaded operator within the definition of `A`, and the compiler still will find and match it. – luk32 Dec 09 '15 at 12:32
  • 1
    You don't clutter the global namespace with anything in either case. You define the operator << in the same namespace as the class and it's found via ADL. – Nir Friedman Dec 09 '15 at 12:48
1

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
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Could you please add some comments to this code to explain two types separated by comma in `decltype` and all the other high-tech? – AlwaysLearning Dec 09 '15 at 17:02
  • Brillant. I wish that C++ were made this easier. – Gardener Mar 04 '19 at 18:46
  • @JohnMurray as of c++17 there is the `is_invocable` type trait. c++20 will have concepts, which will make this easier again to express. https://en.cppreference.com/w/cpp/types/is_invocable – Richard Hodges Mar 05 '19 at 19:17