How to print an object of unknown type

10,393

Solution 1

You can provide a templated << operator to catch the cases where no custom output operator is defined, since any more specialized versions will be preferred over it. For example:

#include <iostream>

namespace detail
{
    template<typename T, typename CharT, typename Traits>
    std::basic_ostream<CharT, Traits> &
    operator<<(std::basic_ostream<CharT, Traits> &os, const T &)
    {
        const char s[] = "<unknown-type>";
        os.write(s, sizeof(s));
        return os;
    }
}

struct Foo {};

int main()
{
    using namespace detail;
    std::cout << 2 << "\n" << Foo() << std::endl;
    return 0;
}

will output:

2
<unknown-type>

The detail namespace is there to keep this "default" output operator from interfering with code in places other than where it is needed. I.e. you should only use it (as in using namespace detail) in your dumpKeys() method.

Solution 2

I originally had just a more canonical way of using Staffan's answer. However, jpalecek correctly pointed out a large flaw with the approach.

As it stood, if no explicit insertion operator is found, the templated insertion operator kicks in and defines a perfect match; this destroys any possibility for existing implicit conversions.

What must be done is make that template insertion operator a conversion (while maintain it's generality), so other conversions can be considered. Once no others are found, then it will be converted to the generic insertion operator.

The utility code is as such:

#include <iosfwd>
#include <memory>

namespace outputter_any_detail
{
    // your generic output function
    template <typename T>
    std::ostream& output_generic(std::ostream& pStream, const T& pX)
    {
        // note: safe from recursion. if you accidentally try 
        // to output pX again, you'll get a compile error
        return pStream << "unknown type at address: " << &pX;
    }

    // any type can be converted to this type,
    // but all other conversions will be 
    // preferred before this one
    class any
    {
    public:
        // stores a type for later output
        template <typename T>
        any(const T& pX) :
        mPtr(new any_holder<T>(pX))
        {}

        // output the stored type generically
        std::ostream& output(std::ostream& pStream) const
        {
            return mPtr->output(pStream);
        }

    private:
        // hold any type
        class any_holder_base
        {
        public:
            virtual std::ostream& output(std::ostream& pStream) const = 0;
            virtual ~any_holder_base(void) {}
        };

        template <typename T>
        class any_holder : public any_holder_base
        {
        public:
            any_holder(const T& pX) :
            mX(pX)
            {}

            std::ostream& output(std::ostream& pStream) const
            {
                return output_generic(pStream, mX);
            }

        private:
            const T& mX;
            any_holder& operator=(const any_holder&);
        };

        std::auto_ptr<any_holder_base> mPtr;
        any& operator=(const any&);
    };

    // hidden so the generic output function
    // cannot accidentally call this fall-back
    // function (leading to infinite recursion)
    namespace detail
    {
        // output a type converted to any. this being a conversion allows
        // other conversions to partake in overload resolution
        std::ostream& operator<<(std::ostream& pStream, const any& pAny)
        {
            return pAny.output(pStream);
        }
    }

    // a transfer class, to allow
    // a unique insertion operator
    template <typename T>
    class outputter_any
    {
    public:
        outputter_any(const T& pX) :
          mX(pX)
          {}

          const T& get(void) const
          {
              return mX;
          }

    private:
        const T& mX;
        outputter_any& operator=(const outputter_any&);
    };

    // this is how outputter_any's get outputted,
    // found outside the detail namespace by ADL
    template <typename T>
    std::ostream& operator<<(std::ostream& pStream, const outputter_any<T>& pX)
    {
        // bring in the fall-back insertion operator
        using namespace detail;

        // either a specifically defined operator,
        // or the generic one via a conversion to any
        return pStream << pX.get();
    }
}

// construct an outputter_any
template <typename T>
outputter_any_detail::outputter_any<T> output_any(const T& pX)
{
    return outputter_any_detail::outputter_any<T>(pX);
}

Stick it in some header like "output_any.hpp". And you use it as such:

#include <iostream>
#include "output_any.hpp"    

struct foo {}; 
struct A {}; 
struct B : A {};

std::ostream& operator<<(std::ostream& o, const A&)
{
    return o << "A";
}

int main(void)
{
    foo f;
    int i = 5;
    B b;

    /*

    Expected output:
    unknown type at address: [address]
    5
    [address] 
    A
    */                                       // output via...  
    std::cout << output_any(f) << std::endl; // generic
    std::cout << output_any(i) << std::endl; // int
    std::cout << output_any(&i) << std::endl;// void*
    std::cout << output_any(b) << std::endl; // const A&
}

Let me know if something doesn't make sense.

Share:
10,393
gnathan
Author by

gnathan

I am a software design engineer for a networking a telecommunications company. I write a lot of C++, but I'd rather be writing Python. I also play around a lot with software development tools.

Updated on July 03, 2022

Comments

  • gnathan
    gnathan almost 2 years

    I have a templatized container class in C++ which is similar to a std::map (it's basically a thread-safe wrapper around the std::map). I'd like to write a member function which dumps information about the entries in the map. Obviously, however, I don't know the type of the objects in the map or their keys. The goal is to be able to handle the basic types (integers, strings) and also some specific class types that I am particularly interested in. For any other class, I'd like to at least compile, and preferably do something somewhat intelligent, such as print the address of the object. My approach so far is similar to the following (please note, I didn't actually compile this or anything...):

    template<typename Index, typename Entry>
    class ThreadSafeMap
    {
        std::map<Index, Entry> storageMap;
        ...
        dumpKeys()
        {
            for(std::map<Index, Entry>::iterator it = storageMap.begin();
                it != storageMap.end();
                ++it)
            {
                std::cout << it->first << " => " << it->second << endl;
            }
        }
        ...
    }
    

    This works for basic types. I can also write custom stream insertion functions to handle specific classes I'm interested in. However, I can't figure out a good way to handle the default case where Index and/or Entry is an unhandled arbitrary class type. Any suggestions?

    • GManNickG
      GManNickG almost 14 years
      Is nothing an option? If a class isn't made to be outputted, why try to output it?
    • gnathan
      gnathan almost 14 years
      Doing nothing for a class that doesn't support printing would be acceptable. Currently I get a compile error for template instantiations where one of the template parameters doesn't support printing. I'd like to at least compile and not crash for cases where a class doesn't support printing.
  • gnathan
    gnathan almost 14 years
    I like your answer. I accepted Staffan's because for my application the simpler implementation seemed sufficient. Unfortunately, I don't have any up-votes yet, or you'd definitely get one. Thanks for your time!
  • BetweenTwoTests
    BetweenTwoTests almost 14 years
    I stumbled upon your solution, and I found it doesn't work that well. See ideone.com/uRBKl, it doesn't correctly handle the int* and B.
  • BetweenTwoTests
    BetweenTwoTests almost 14 years
    AFAIK, this prints "unknown-type" even for types that are, in fact, known. See my comment on GMan's solution.
  • GManNickG
    GManNickG almost 14 years
    @jpalecek: While I think you've made an important point, I want to correct your terminology: you say "this prints "unknown-type" even for types that are, in fact, known" but neither B or int* have insertion operations; that is, they aren't "known". What you mean to say is "the solution, because it uses templates, prefers the generic solution over any conversions", which explains it much better. B can be converted to A, but the template insertion operation matches better, and int* can be converted to void*, but again the template insertion operation matches better. I will fix it. :)
  • GManNickG
    GManNickG almost 14 years
    @jpalecek: That should fix it. Making a template function into a template class + conversion makes the code a bit verbose, but it's not too hard and doesn't need to be looked at often anyway. :)
  • BetweenTwoTests
    BetweenTwoTests almost 14 years
    Oh yes, I could have realized it. I was trying to make this into something that could be used for SFINAE, and urged not to use the extra namespace - which was a bad idea. With the extra namespace, it is easy: ideone.com/xzf7S