Search code examples
c++templatesostream

`operator<<` on a container which works for both values and references


There is a container class container which is templated, so it can contain anything.

I want to add ability to print its contents to std::ostream so I've overriden operator<<.

However this has a drawback: if the container contains references (or pointers), the method only prints addresses instead of real information.

Please consider this simple test code which demonstrates the problem:

#include <iostream>
#include <deque>

template<typename T, bool is_reference = false>
class container {
public:
    container() {}
    void add(T a) { queue.push_back(a); }
    
    friend std::ostream& operator<<(std::ostream& out, const container<T, is_reference>& c) {
        for (const T& item : c.queue) {
            if (is_reference) out << *item; else out << item;
            out << ",";
        }
        return out;
    }
    
private:
std::deque<T> queue;
};


int main() {
    //Containers
    container<int*, true> myContainer1;
    container<int> myContainer2;
    
    int myA1(1);
    int myA2(10);
    
    myContainer1.add(&myA1);
    myContainer1.add(&myA2);
    
    myContainer2.add(myA1);
    myContainer2.add(myA2);
    
    std::cout << myA1 << std::endl;
    std::cout << myA2 << std::endl;
    
    std::cout << myContainer1 << std::endl;
    std::cout << myContainer2 << std::endl;
    
    return 0;
}

I had an idea to provide an extra is_reference boolean in the template for adjusting the operator<<.

However this causes an early compiler error, in case I have value type container(s).

How can I make this work?

If I change the printer line to

out << item << ",";

The code compiles and prints this:

1
10
0x7ffd77357a90,0x7ffd77357a94,
1,10,

Obviously my goal is to have this result:

1
10
1,10,
1,10,

(How) can I achieve this easily?


Solution

  • You can use SFINAE to select an overload based on whether std::is_pointer<T>::value is true or false. This works already with C++11:

    #include <iostream>
    #include <deque>
    
    template<typename T>
    class container {
    public:
        // container() {} // dont define empty constructor when not needed
        //                   or declare it as = default
        void add(const T& a) { queue.push_back(a); } // should take const&
        
        template <typename U = T, typename std::enable_if< std::is_pointer<U>::value,bool>::type=true> 
        friend std::ostream& operator<<(std::ostream& out, const container<T>& c) {
            std::cout << "is pointer\n";
        }
        template <typename U = T, typename std::enable_if< ! std::is_pointer<U>::value,bool>::type=true>
        friend std::ostream& operator<<(std::ostream& out, const container<T>& c) {
            std::cout << "is not pointer\n";
        }
        
    private:
        std::deque<T> queue;
    };
    
    
    int main() {
        //Containers
        container<int*> myContainer1;
        container<int> myContainer2;
        std::cout << myContainer1 << std::endl;
        std::cout << myContainer2 << std::endl;
    }
    

    Output:

    is pointer
    
    is not pointer
    

    Since C++17 you can use constexpr if. Also since C++17 there is std::is_pointer_v and since C++14 there is std::enable_if_t, both would make the code a little less verbose.