5

我查看了下面的代码类型,虽然我对问题 (*) 有个人答案,但我想听听 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,在 C++ 中使用修改其他 const 成员变量是一个不错的类设计选择吗?

我想强调以下几点:

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

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

还要注意,这mutable(如在 C++98 中)不是问题的解决方案,因为其目的是使成员变量尽可能地不可修改。当然,mutable(如在 C++11 中,Herb Sutter 的“你不知道constmutable”)更不是一个解决方案。

(*) 我可以私下将我对该问题的回答转发给任何提问的人。

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

4

4 回答 4

6

来自cppreference的引用:

即使 const_cast 可以从任何指针或引用中删除 const 或易失性,使用生成的指针或引用来写入声明为 const 的对象或访问声明为 volatile 的对象会调用未定义的行为。

这意味着您的副本分配不安全,但完全不正确。如果您声明某些东西const,您将永远无法安全地更改它。这与设计无关。

的唯一有效用途const_cast是从 const 引用或指向非 const 对象的指针(或指向 const 对象,然后不修改它,但你不能const_cast改为)中删除 constness。

于 2014-08-10T18:47:53.230 回答
0

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

但是,当派生类需要“修改”私有成员时,我可以实现非 const 受保护访问器,但我宁愿将派生类的访问器调用限制为 const-reference,在大多数情况下,它只需要无论如何 const 参考。

因此,在我确实需要在派生类中“调整”它的少数情况下, const_cast<> 会像大拇指一样突出,但这是自愿选择的。我喜欢它突出。我可以轻松搜索它(谁是 const_cast<>-ing 这个类?)。

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

于 2014-08-10T21:10:28.007 回答
0

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

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

于 2014-08-11T11:32:30.923 回答
0

即使我不是设计专家,更不用说 C++ 专家,我认为这是一个“设计陷阱”的案例(我允许自己这么说,因为可以肯定陷阱是巧妙地完成的)。

在我看来,争论始于“数据显然是一种值类型”的错误假设,然后变成了一些“常量”问题。

对象的“值”是andData的组合,而an的“ keyability ”决定了 ( , ) 对的唯一性。换句话说,将自身表征为常数是Id- > Value对应,但在单义的意义上。IDValueValueIdIdValue

此外,如果一个Data对象作为Id-> Value对应而诞生,由于某种原因不再有效(在必须修改的意义上),那么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;
于 2020-03-23T20:19:02.673 回答