Quentin
Quentin

Reputation: 63124

Proxy to an object, proper const qualification and lack thereof

I just found something that looks like a quirk to me. Consider :

struct Tile {

    Tile(Map &map, int, int)
    : map(map) { }

    void destroy();

    void display() const;

    Map ↦
};

This (stripped down) class is an accessor object. It's constructed by the Map itself as such :

Tile Map::operator ()(int x, int y) {
    return Tile(*this, x, y);
}

Tile const Map::operator ()(int x, int y) const {
    return Tile(*this, x, y);
}

So a Map can return a Tile from which we can call destroy() (which updates the map), and a Map const can only return a Tile const, from which we can only call the non-modifying display() method.

So everything's good, right ? Well, not quite. Because even though it seemed pretty straightforward at first, I can't figure out how to construct a Tile from a Map const, because of the Map& constructor parameter.

I also tried removing Tile's constructor and aggregate-initializing it, to no avail :

Tile const Map::operator ()(int x, int y) const {
    return { *this, x, y };
}

... which looks even stranger to me, since I get an...

error: invalid initialization of reference of type ‘Map&’ from expression of type ‘const Map’

... even though a Tile const should only contain const fields, shouldn't it ?

Why does the compiler complain on that last one (the first is quite logical), and can I do something short of rewriting the whole Tile class specifically for const access ? Could it be one of the mythical places where const_cast is right ?

Thanks in advance.

Upvotes: 0

Views: 433

Answers (2)

Barry
Barry

Reputation: 303067

Daniel's on the right track - you definitely need a Tile and ConstTile class, which can be templated for simplicity, but you need to deal with when you can call destroy() and how you can construct them. To that end:

template<class MapT>
struct TileT {

    TileT(MapT &map, int, int)
    : map(map) { }

    // we want to be able to construct ConstTile from Tile
    template <typename M>
    TileT(const TileT<M>& tile)
    : map(tile.map) { } 

    void destroy() {
        static_assert(!std::is_const<MapT>::value, "Cannot call destory() from ConstTile");
        // rest of implementation
    }

    void display() const;

    MapT &map;
};

using Tile = TileT<Map>;
using ConstTile = TileT<const Map>;

That will give you the desired functionality and will work in a similar fashion to how iterator/const_iterator work. So you can do stuff like:

Map map;
...
ConstTile tile = map(4,3); // non-const map, ConstTile is ok

Upvotes: 3

DanielM
DanielM

Reputation: 1053

I see no way around creating two versions of the Tile class: One for const access and one for mutable access. Consider the following: What should the destroy function do, if the Map reference is const?

If you want to get around making a Tile and a ConstTile version of the class, you can use templates to achieve the same effect and still avoid code duplication.

template<class MapT>
struct Tile {

    Tile(MapT &map, int, int)
    : map(map) { }

    template<typename U = MapT>
    std::enable_if<std::is_const<U>::value> destroy();

    void display() const;

    MapT &map;
};

MapT can now be Map or const Map depending on the instantiation.

Upvotes: 2

Related Questions