C++:为什么gcc在访问operator[]时更喜欢非const而不是const?

7
这个问题可能更适合一般的C++,但是由于我在Linux上使用gcc,因此这是上下文。 考虑以下程序:
#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;
}

当执行程序时,输出结果为:
   operator[] with key 1 called 
   operator[] with key 1 called 
   first one: one

我希望编译器在第二次调用时选择operator[]const方法。原因是,如果之前没有使用过dict["1"],调用operator[]会导致内部映射创建不存在的数据,即使我只想进行一些调试输出,这当然是致命的应用程序错误。
我期望的行为类似于C#索引运算符,它具有get和set操作,您可以在getter尝试访问不存在的内容时抛出异常。
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;
        }
    }
}

因此,我想知道为什么gcc在不需要非const访问时更喜欢非const调用而不是const调用。
4个回答

1

你无法获得想要的效果。当字典是非常量时,它将调用operator[]的非常量版本。

C#在这种情况下更优越,因为它可以确定'dict[n]'是否是赋值的一部分。C++无法做到这一点。


除非唯一可用的版本是const并且不会导致“丢弃限定符”错误,否则它将调用non-const。 显然,编译器知道存在哪些版本,因此它可以使用不破坏我的数据的版本。我个人认为这是糟糕的编译器行为,请给我指点。在我看来,为什么他们要这样做,而另一种方式(优先选择non-const)更安全呢? - JonasW
3
可能看起来更安全,但并不太合乎逻辑,最好的匹配是非 const 的,因为它与精确相同的类型和常量性。之后可以寻找其他匹配。通常这会产生最小的意外情况。如果想要 const 安全性,则必须引导它使用它。 - Keith Nicholas

0
任何C++编译器都应该按照这种方式工作。您不能根据函数出现在赋值运算符的左侧还是右侧来选择重载。重载是基于实例是否为const而选择的。
在C#中的属性和在C++中基于方法const-ness的重载只是不同的事情,它们有不同的目的。

我想知道你的问题是否与为什么我们不能为map创建一个不可变版本的operator[]有关?

通过使用代理类(希望这个术语是正确的),在左侧赋值和其他上下文中区分使用是有可能的,但它并不真正有效,并且由于隐式转换运算符,我不建议使用它(请注意,输出结果需要进行显式转换)。

#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;
}

在实现DictProxy和ConstDictProxy时,应该尽可能地重用代码以遵守DRY原则。


然而,如果你的问题与此相关,那么我认为解决方案是在不想添加默认值时使用at()方法,而在想要添加默认值时使用operator[]。我怀疑前者是C++0x的一个补充,不过?


似乎可以在代理中添加一个模板转换函数,代码如下: template<class T> Proxy::operator T() const noexcept(noexcept(T((*p_map)[*p_key]))) { return T((*p_map)[*p_key]); } ...或许可以加入一个enable_if,检查std::is_convertible或者检查static_cast/dynamic_cast是否可行。 - undefined

0

如果你实例化了该类的一个 const 对象,它将使用 const 方法。


0

只需记住,在 C++ 中您有主导权并可以做出决策。

在非 const 操作员中,如果需要,可以更改对象。 只是恰好你没有这样做。是否更改对象完全是编码人员的任意选择。编译器不知道您的意图。即,没有好的规则让编译器知道使用 const。

所以是的...您告诉编译器将其视为常量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接