tags:

views:

248

answers:

4

This question might be more appropriately asked regarding C++ in general, but as I am using gcc on linux that's the context. Consider the following program:

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

using namespace std;

template <typename TKey, typename TValue>
class Dictionary{
    public:
    map<TKey, TValue> internal;

    TValue & operator[](TKey const & key)
    {
        cout << "operator[] with key " << key << " called " << endl;
        return internal[key];
    }

    TValue const & operator[](TKey const & key) const
    {
        cout << "operator[] const with key " << key << " called " << endl;
        return internal.at(key);
    }

};

int main(int argc, char* argv[])
{
    Dictionary<string, string> dict;
    dict["1"] = "one";
    cout << "first one: " << dict["1"] << endl;

    return 0;
}

When executing the program, the output is:

   operator[] with key 1 called 
   operator[] with key 1 called 
   first one: one

What I would like is to have the compiler choose the operator[]const method instead in the second call. The reason is that without having used dict["1"] before, the call to operator[] causes the internal map to create the data that does not exist, even if the only thing I wanted was to do some debugging output, which of course is a fatal application error.

The behaviour I am looking for would be something like the C# index operator which has a get and a set operation and where you could throw an exception if the getter tries to access something that doesn't exist:

class MyDictionary<TKey, TVal>
{
    private Dictionary<TKey, TVal> dict = new Dictionary<TKey, TVal>();
    public TVal this[TKey idx]
    {
        get
        {
            if(!dict.ContainsKey(idx))
                throw KeyNotFoundException("...");

            return dict[idx];
        }
        set
        {
            dict[idx] = value;
        }
    }
}

Thus, I wonder why the gcc prefers the non-const call over the const call when non-const access is not required.

A: 

It will use the const method in the case you instantiate a const object of that class.

Gab Royer
+1  A: 

You can't get the effect you want. When dict is non-const, it will call the non-const version of operator[].

C# is superior in this case because it can determine whether 'dict[n]' is part of an assignment. C++ cannot do this.

richb
It will call non-const unless the only version available is const and that doesn't result in the "discard qualifiers" error.Obviously, the compiler knows what versions exist so it could as well use the one that doesn't destroy my data. I personally think this is bad compiler behaviour, so enlighten me. Why did they do it this way when the other way around (to prefer non-const) is safer AFAICT?
JonasW
might seem safer, but its not too logical, the *best* match is the non const one as it is of the exact same type and constness. After that it can look for other matches. Generally this results in the least surprise. If you want const safety, you have to direct it to use it
Keith Nicholas
A: 

Any C++ compiler should work this way. You can't choose an overload based on whether your function will appear on the left or the right side of the assignment operator. The overload is selected based on whether the instance is const or not.

Properties in C# and overloading based on method const-ness in C++ simply happen to be different things that serve a different purpose.


I wonder if your question is related to http://stackoverflow.com/questions/2459755/why-cant-we-have-an-immutable-version-of-operator-for-map?

It is sort of possible to emulate distinguishing between the use on the left-hand side of assignment and in other contexts with a proxy class (I hope the term is right), but it doesn't really work too well and I wouldn't recommend it because of the implicit conversion operator (note that to output the result an explicit cast is required).

#include <iostream>
#include <map>
#include <string>
#include <stdexcept>

using namespace std;

template <typename KeyType, typename ValueType>
class DictProxy;

template <typename KeyType, typename ValueType>
class ConstDictProxy;

template <typename TKey, typename TValue>
class Dictionary{
    public:
    map<TKey, TValue> internal;

    DictProxy<TKey, TValue> operator[](TKey const & key);
    ConstDictProxy<TKey, TValue> operator[](TKey const & key) const;
};

template <typename KeyType, typename ValueType>
class DictProxy
{
    std::map<KeyType, ValueType>* p_map;
    const KeyType* key;
    DictProxy(std::map<KeyType, ValueType>* p_map, const KeyType* key): p_map(p_map), key(key) {}
    friend class Dictionary<KeyType, ValueType>;
public:
    void operator=(const ValueType& value) const {
        cout << "operator[] used on the left side of assignment with key " << *key << endl;
        (*p_map)[*key] = value;
    }
    operator ValueType&() const {
        cout << "operator[] used in a different context with " << *key << endl;

        //you used at here
        //it is in the C++0x standard, but generally not in online references?
        typename std::map<KeyType, ValueType>::iterator it = p_map->find(*key);
        if (it == p_map->end()) {
            throw std::range_error("Missing key in map");
        }
        return it->second;
    }
};

template <typename KeyType, typename ValueType>
class ConstDictProxy
{
    const std::map<KeyType, ValueType>* p_map;
    const KeyType* key;
    ConstDictProxy(const std::map<KeyType, ValueType>* p_map, const KeyType* key): p_map(p_map), key(key) {}
    friend class Dictionary<KeyType, ValueType>;
public:
    operator const ValueType&() const {
        cout << "operator[] const used in a different context with " << *key << endl;
        typename std::map<KeyType, ValueType>::const_iterator it = p_map->find(*key);
        if (it == p_map->end()) {
            throw std::range_error("Missing key in map");
        }
        return it->second;
    }
};

template <typename TKey, typename TValue>
DictProxy<TKey, TValue> Dictionary<TKey, TValue>::operator[](TKey const & key)
{
    return DictProxy<TKey, TValue>(&internal, &key);
}

template <typename TKey, typename TValue>
ConstDictProxy<TKey, TValue> Dictionary<TKey, TValue>::operator[](TKey const & key) const
{
    return ConstDictProxy<TKey, TValue>(&internal, &key);
}

int main(int argc, char* argv[])
{
    Dictionary<string, string> dict;
    dict["1"] = "one";
    cout << "first one: " << string(dict["1"]) << endl;

    const Dictionary<string, string>& r_dict = dict;
    cout << "first one: " << string(r_dict["1"]) << endl;
    return 0;
}

(Some code reuse should be possible to repect DRY when implementing DictProxy and ConstDictProxy.)


However, if your question is related to that, then the solution IMO is to use the at() method when you don't want default values added, and operator[] if you do. I suspect that the former is an addition with C++0x, though?

UncleBens
Thankyou for your time. I will take a look at this tonight.
JonasW
A: 

Just remember in C++ you are in charge and get to make the decisions.

consider in the non const operator you could change the object if so desired. It just happens that you don't. It's completely arbitrary choice by the coder whether to change the object or not. The compiler has no idea what your intent is. ie, there is no good rule for the compiler to know to use const.

so yes.... you tell the compiler what to treat as const.

Keith Nicholas