Home > front end >  Templates and type erasure - Why does this program compile?
Templates and type erasure - Why does this program compile?

Time:09-03

I have written an example type erasure program, but noticed something that seems strange. The code compiles - and I believe it shouldn't. More likely, it does something that I don't understand which enables it to compile.

Here is a MWE.

#include <iostream>
#include <memory>
#include <string>
#include <vector>


class Object
{
public:

    template<typename T>
    Object(T value)
        : p_data(std::make_unique<Model<T>>(std::move(value)))
    {

    }

    Object(const Object& object)
        : p_data(object.p_data->copy())
    {

    }

    Object(Object&& other) noexcept = default;

    Object& operator=(const Object& object)
    {
        Object tmp(object);
        *this = std::move(tmp);
        return *this;
    }

    Object& operator=(Object&& object) noexcept = default;

   friend std::ostream& operator<<(std::ostream&, const Object&);

private:

    struct Concept;
    std::unique_ptr<Concept> p_data;

    struct Concept
    {
        virtual
        ~Concept() = default;

        virtual
        std::unique_ptr<Concept> copy() const = 0;

        virtual
        void draw_internal(std::ostream&) const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {

        Model(T value)
            : data(std::move(value))
        {

        }

        std::unique_ptr<Concept> copy() const override
        {
            return std::make_unique<Model<T>>(data);
        }

        void draw_internal(std::ostream& os) const override
        {
            std::cout << "Model<T>::draw_internal(std::ostream& os)" << std::endl;
            os << data;
        }

        T data;
    };
    
};


//  This enables us to do
//      std::cout << Object
//  We forward the call to unique_ptr<Concept>::draw_internal using virtual function
//  dispatch  
std::ostream& operator<<(std::ostream& os, const Object& object)
{
    std::cout << "operator<<(std::ostream& os, const Object& object" << std::endl;

    object.p_data->draw_internal(os);
    return os;
}


int main()
{
    std::vector<Object> document;

    document.push_back(0);
    document.push_back(std::string("Hello World"));

    std::cout << "Drawing now!" << std::endl;
    std::cout << document << std::endl;
    std::cout << std::endl;

    return 0;
}

Actually this is a broken example - at runtime the code enters an infinite loop. No doubt for reasons related to the fact that it compiles when it (maybe) shouldn't.

Here's why I think it shouldn't compile:

  • There is no operator<< defined for the type std::vector<Object>.

That said, I suspect this compiles because the compiler is able to generate a compatiable operator from something within this code. I just don't understand exactly how it has done this. My guess would be there is an implicit conversion from std::vector<Object> to Object<T> with T=std::vector<Object>? But this is really a guess.

  • At runtime there is an infinite loop caused by operator<< calling itself. I don't understand exactly why this happens either.

Interestingly here are two things which do not compile:

std::vector<int> a;
std::cout << a << std::endl;

std::vector<std::string> b;
std::cout << b << std::endl;

So the compiler clearly can't generate operator<< for any type. This makes me think my guess about what is happening is wrong.

Compiled with gcc-12 (mingw64) on Windows 10.

Godbolt

CodePudding user response:

You could have stripped your example down bit by bit to get to a MWE like this:

#include <iostream>
#include <vector>

struct Object {
    template<typename T> Object(T) {}                              // #1
    friend std::ostream& operator<<(std::ostream&, const Object&); // #2
};

int main()
{
    std::vector<Object> document;
    std::cout << document << std::endl;
}

I truly just removed pieces as long as the program would compile, stopping only when I couldn't reasonably remove anything else.

Once got to this point it's easy to experiment and, in this specific situation, find out that commenting either #1 or #2 makes the program fail to compile.

The step is short to connect that to the solution to the question.

By they way, the suggestion in one of the comments to the question of using explicit really pays off.

CodePudding user response:

There is no operator<< defined for the type std::vector.

Nearly any type is convertible to Object thanks to the constructor template here:

template<typename T>
Object(T value)
   : p_data(std::make_unique<Model<T>>(std::move(value)))
{
}

Thus this operator overload:

std::ostream& operator<<(std::ostream& os, const Object& object)

Can take almost any argument

CodePudding user response:

I believe I have figured it out. Someone may correct me if this is wrong.

Starting from

cout << document

this is, in terms of the types involved

cout << vector<Object<T>>

This is implicitly converted to

cout << Object<vector<Object<T>>>

I'll just add this line, which we will refer back to later. Since cout is a type of ostream, we more precicely have this:

ostream << Object<vector<Object<T>>> // call this (A)

Looking at the function call for operator<<(ostream&, Object<T>& object) we can see that it does this:

[ operator<<(ostream&, Object<T>& object) ] :
    object.p_data->draw_internal(...)

Converting that to the type, we have this:

object.[unique_ptr<W>]->draw_internal(...)

with W = Model<T>, T = vector<Object<U>>.

So this is a function call to

Model<T>::draw_internal(ostream& os)

and draw_internal does this:

[ Model<T>::draw_internal(ostream& os) ] :
    os << data

where data is of type T = vector<Object<U>>. Therefore it is a function call like this:

os << vector<Object<U>>

Becuase of the implicit conversion we saw previously, the compiler actually converts this into

os << Object<vector<Object<U>>

in other words it does this:

os << Object(data) // instead of just os << data

and hence we have a loop. operator<< is calling itself via draw_internal. See (A) above.

  • Related