定期使用const_cast作为设计工具是否可以接受

is using const_cast regularly as a design tool acceptable?

本文关键字:是否 工具 const cast      更新时间:2023-10-16

我回顾了下面的代码,虽然我对这个问题有一个个人的答案(*),但我希望能得到c++/设计专家的评论。

由于某些原因,Data是一个具有不可修改标识符和可修改值的对象:

class Data
{
   const Id    m_id ;        // <== note that m_id is a const member variable
   Value       m_value ;
   Data(const Id & id, const Value & value) ;
   Data(const Data & data) ;
   Data & operator = (const Data & data) ;
   // etc.
} ;

设计选择变成了语言选择,因为标识符在类级别(**)被声明为const,以避免它的(意外)修改,甚至从类成员函数内部…

…但是正如您所看到的,这里有一个复制赋值操作符,实现为:

Data & Data::operator = (const Data & that)
{
   if(this != &that)
   {
      const_cast<Id &>(this->m_id) = that.m_id ;
      this->m_value                = that->m_value ;
   }
   return *this ;
}

复制赋值操作符不是const限定的这一事实使得这段代码是安全的(用户只能合法地在非const对象上调用该方法,而不会引发未定义行为)。

但是使用const_cast来修改其他const成员变量在c++中是一个很好的类设计选择吗?

我想强调以下几点:

  • 数据显然是一个值类型(它有一个operator =成员函数)
  • 在这种模式中,其他一些函数也可能合法地需要const_cast(例如移动构造函数/赋值和/或交换函数),但不是很多。

注意,这可能是一个代码审查问题,但这不是一个"日常"的代码。这是一个通用的c++类型设计问题,需要平衡语言的需求/能力和模式/习惯用法的代码解释。

还请注意,mutable(如c++ 98)并不是解决这个问题的方法,因为其目的是使成员变量尽可能不可修改。当然,mutable(就像Herb Sutter在c++ 11 post-"你不知道constmutable ")更不是一个解决方案。

(*)我可以私下把我对那个问题的答案转发给任何问我的人。

(**)另一个解决方案是使对象非const,并在接口级别使其为const(即不提供可以改变它的函数)

引用自cppreference:

尽管const_cast可以从任何指针或引用中删除constconstness或volatile,但是使用生成的指针或引用对声明为const的对象进行写操作或访问声明为volatile的对象会调用未定义的行为。

这意味着你的拷贝赋值是不安全的,而是完全不正确的。如果您声明某个东西为const,则无法安全地更改它。这与设计无关。

const_cast的唯一有效用途是从const引用或指向非const对象的指针中删除constness(或者指向const对象但不修改它,但这样你就可以不使用const_cast)。

我将在实现私有成员的唯一访问器时使用它,当它返回const引用时,即该类的客户端只看到const引用。

然而,当派生类需要"修改"私有成员时,我可以实现一个非const保护的访问器,但我宁愿将派生类的访问器调用限制为const引用,因为大多数情况下它只需要const引用。

因此,在我确实需要在派生类中"调整"它的少数情况下,const_cast<>像疼痛的拇指一样突出,但这是我的选择。我喜欢它的突出。我可以很容易地搜索它(谁是const_cast<>-ing这个类?)。

另一种选择——提供一个受保护的非常量访问器,可能在语法上更"正确",但我宁愿让非常量访问变得突兀,而不是"普通"。

通常情况下,类应该完全控制并了解自己的成员。保护成员在其自己的类中不被滥用的要求违背了一些基本的面向对象设计原则。

当然可以将私有变量声明为常量,如果它确实是常量。但在您的情况下,您只想保护它不受一些方法的影响。在这种情况下,保持它为非常量,或者拆分类。您可以使用类似Private类数据模式的东西来更好地控制变量的可访问性。

即使我不是设计专家,更不是c++专家,我也认为这是一个"设计陷阱"的例子(我允许自己这么说,因为这个陷阱确实很巧妙)。

在我看来,争论从"数据显然是一种值类型"的错误假设开始,然后变成了一些"constness"问题。

Data对象的"value"是IDValue的组合,IdValue的"keyability"决定了(Id, Value)对的唯一性。换句话说,是Id-> Value对应关系将自身表征为常数,但在意义上,是惟一的。此外,如果Data对象出生时是Id->值对应,由于某种原因不再有效(在必须修改的意义上),那么Data本身已经结束了它的生命周期,因此它不会更改。从这个角度出发,我们来讨论不可变对象的特征。

我将用下面的代码实现它,其中KeyedValue类模板通过从引用返回的对象池中绘制来封装上面概述的需求:
template <class K, class V>
class KeyedValue {
public:
    typedef K key_type;
    typedef V value_type;
    const K& key() const { return _key; }
    const V& value() const { return _value; }
    operator K() const { return _key; }
    //bool operator == (const Keyable& other) { return _key == other.key(); }
    /**************************/
    /* _value doesn't take part to hash calculation */
    /* with this design choice we have unique KeyedValue(s) */
    struct hash {
        size_t operator()(const KeyedValue& d) const noexcept {
            return std::hash<K>()(d.key());
        }
    };
    /**************************/
    static KeyedValue getValue(const K& key, const V& val));
private:
    KeyedValue& operator = (const KeyedValue&); // Don't implement
    K _key;
    V _value;
protected:
    KeyedValue(const K& key_val, const V& val):  _key(key_val), _value(val) {}
    static std::unordered_set<KeyedValue<K, V>, typename KeyedValue<K, V>::hash> value_pool;
    };
template <class K, class V>
std::unordered_set<KeyedValue<K, V>, typename KeyedValue<K, V>::hash> 
KeyedValue<K, V>::value_pool;
template <class K, class V>
KeyedValue<K, V> KeyedValue<K, V>::getValue(const K& key, const V& val) {
    KeyedValue to_find(key, val);
    auto got = value_pool.find (to_find);
    if (got == value_pool.end()) {
        value_pool.insert(to_find);
        return to_find;
    }
    else
        return *got;
}
typedef size_t Id;
typedef int Value;
typedef KeyedValue<Id, Value> Data;