Patrizio Bertoni
Patrizio Bertoni

Reputation: 2732

Autodeduction of return type

After reading C++ auto deduction of return type and C++ : Vector of template class, I'm still wondering how to do generic operations (e.g. operator << overload) on object. My code looks like

#include <map>
#include <memory>
#include <string>
#include <iostream>

/**
 * Abstract placeholder for Cache polymorphism
 */
class ICache
{
public:

    virtual void update() {};

    friend std::ostream & operator << (std::ostream & out, const ICache & IC)
    {
        out << "you should never print this";
    }
};

/**
 * Concrete. Coupling a name with some cached value
 */
template<typename T>
class Cache :
    public ICache
{
    const std::string m_name;
    T m_cached;

public:

    Cache(const std::string & name) :
        m_name(name),
        m_cached(0)
    {}

    void update() override
    {
        // m_cached is synced with remote resource; action depends both from T and m_name
    }

    void set(const T t)
    {
        std::cout << m_name << " setting " << +m_cached << " -> " << +t << std::endl;
        m_cached = t;
    }

    inline T get() const noexcept { return m_cached; }

    friend std::ostream & operator << (std::ostream & out, const Cache & O)
    {
        out << "Cache<" << O.m_name << ", " << O.m_cached << ">";
    }
};

class CacheMap
{
    std::map<std::string, std::unique_ptr<ICache>> m_map;

    template<typename T>
    Cache<T>* _get_ptr(const std::string & name) const
    {
        return reinterpret_cast<Cache<T>*>(m_map.at(name).get());
    }

public:

    template<typename T>
    T get(const std::string & name) const
    {
        return _get_ptr<T>(name)->get();
    }

    template <typename T>
    void set(const std::string & name, T t)
    {
        _get_ptr<T>(name)->set(t);
    }

    template <typename T>
    void insert(const std::string & name, T def = 0)
    {
        std::unique_ptr<ICache> up = std::make_unique<Cache<T>>(name);
        m_map.insert({name, std::move(up)});
        set<T>(name, def);
    }

    friend std::ostream & operator << (std::ostream & out, const CacheMap & OM)
    {
        out << "OM{";
        for (const auto & IO : OM.m_map)
            out << IO.first << ": " << *(IO.second.get()) << ", ";     // ver1
            // out << IO.first << ": " << (IO.second->get()) << ", ";  // ver2
        out << "}";
        return out;
    }
};


int main()
{
    CacheMap m;
    int i= 70000;

    m.insert<int>("i", 69999);
    m.insert<short>("s", 699);
    m.insert<char>("c", 69);
    m.set("i", i);

    std::cout << m << std::endl;
}

Line marked with trailing //ver1 shows you should never print this which makes sense; I'm dealing with std::unique_ptr<ICache> objects.

Line marked with trailing //ver2 won't compile at all, and this makes sense too.

What I'd like to do inside CacheMap is to auto-detect, at runtime (hmm sounds bad) the correct T, given a map key, in order to fire the right reinterpret_cast<> and to retrieve m_cached value.

EDIT 1

If compiling with g++ -O3, ver1 line causes a segmentation violation.

Upvotes: 0

Views: 114

Answers (3)

n314159
n314159

Reputation: 5095

First, reinterpret_cast is a dangerous thing. In contexts of polymorphism you mostly want to use dynamic_cast.

You problem is not about templates etc at all, but it is more about that you want to make the operator<< virtual, which is not possible, since it is a friend function. An easy workaround is the following:

class ICache {
  virtual void print(std::ostream &out) const { // Prefer pure virtual 
    out << "Never print this\n";
  }
  friend std::ostream &operator<<(std::ostream &out, const ICache& c) {
    c.print(out);
    return out;
  }
};

template<class T>
class Cache: ICache {
  void print(std::ostream &out) const override {
    out << "Print this instead\n";
  }
};

will do what you want without any casting.

Upvotes: 1

KamilCuk
KamilCuk

Reputation: 141768

Just use a virtual function. The time you cast your variable from Cache<int> pointer type to ICache pointer you lose compile time information about it. That information is lost. You can use dynamic_cast in your friend ICache::operator<< to handle all the different types... To properly resolve type information use virtual functions - ie. unique data tied to each of the class.

#include <map>
#include <memory>
#include <string>
#include <iostream>

class ICache
{
public:
    virtual ~ICache() {};
    virtual void update() {};

    // -------- HERE --------------
    virtual std::ostream& printme(std::ostream & out) const = 0;
    friend std::ostream& operator << (std::ostream & out, const ICache & IC) {
        return IC.printme(out);
    }

};

template<typename T>
class Cache : public ICache {
    const std::string m_name;
    T m_cached;
public:
    Cache(const std::string & name): m_name(name), m_cached(0) {}
    void update() override {}
    void set(const T t) {
        std::cout << m_name << " setting " << +m_cached << " -> " << +t << std::endl;
        m_cached = t;
    }
    inline T get() const noexcept { return m_cached; }
    std::ostream& printme(std::ostream & out) const override {
        out << "Cache<" << m_name << ", " << m_cached << ">";
        return out;
    }
};

class CacheMap {
    std::map<std::string, std::unique_ptr<ICache>> m_map;
    template<typename T>
    Cache<T>* _get_ptr(const std::string & name) const {
        return dynamic_cast<Cache<T>*>(m_map.at(name).get());
    }
public:
    template<typename T>
    T get(const std::string & name) const {
        return _get_ptr<T>(name)->get();
    }

    template <typename T>
    void set(const std::string & name, T t) {
        _get_ptr<T>(name)->set(t);
    }

    template <typename T>
    void insert(const std::string & name, T def = 0) {
        std::unique_ptr<ICache> up = std::make_unique<Cache<T>>(name);
        m_map.insert({name, std::move(up)});
        set<T>(name, def);
    }

    friend std::ostream& operator << (std::ostream & out, const CacheMap & OM) {
        out << "OM{";
        for (const auto & IO : OM.m_map)
            out << IO.first << ": " << *(IO.second.get()) << ", ";     // ver1
            // out << IO.first << ": " << (IO.second->get()) << ", ";  // ver2
        out << "}";
        return out;
    }
};


int main()
{
    CacheMap m;
    int i= 70000;

    m.insert<int>("i", 69999);
    m.insert<short>("s", 699);
    m.insert<char>("c", 69);
    m.set("i", i);

    std::cout << m << std::endl;
}

will output on godbolt:

i setting 0 -> 69999
s setting 0 -> 699
c setting 0 -> 69
i setting 69999 -> 70000
OM{c: Cache<c, E>, i: Cache<i, 70000>, s: Cache<s, 699>, }

I just spotted, to protect against very bad and hard to debug errors, I remind you to use dynamic_cast instead of reintepret_cast in CacheMap::_get_ptr().

Upvotes: 2

Peter
Peter

Reputation: 36617

There are various ways, but generally I would implement a single operator<<() that calls a virtual function.

class ICache {
   protected:      //  so the function is only accessible to derived classes

    virtual std::ostream print(std::ostream &out) const = 0;    // force derived classes to override

  friend std::ostream &operator<<(std::ostream &out, const ICache& c);
};

Then place a single definition of the operator<< in a single compilation unit

// definition of class ICache needs to be visible here

std::ostream &operator<<(std::ostream &out, const ICache& c)
{
     return c.print(out);
}   

And to implement the derived class

// definition of ICache here

template<class T>
class Cache: ICache
{
   protected:
    std::ostream print(std::ostream &out) const override
    {
        // output a Cache<T> 
        return out;
    }
};

The advantage of this is that each class takes responsibility for outputting itself, rather than a container class having to work out which output function to call (and the opportunity for a programmer to forget to do that).

Upvotes: 2

Related Questions