【问题标题】:is using const_cast regularly as a design tool acceptable?是否可以定期使用 const_cast 作为设计工具?
【发布时间】:2014-10-03 14:32:11
【问题描述】:

我查看了下面的代码类型,虽然我对问题 (*) 有个人答案,但我想请 C++/设计专家的 cmets。

由于某种原因,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 对象上合法地调用此方法而不会引发未定义的行为)。

但是在 C++ 中使用 const_cast 修改其他 const 成员变量是一个不错的类设计选择吗?

我想强调以下几点:

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

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

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

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

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

【问题讨论】:

  • 那个赋值运算符不安全。它确实调用了未定义的行为。对象本身不是 const 没关系,它的成员仍然是。
  • 设计原理听起来很疯狂:“为了避免从类成员函数内部对其进行意外修改。”课堂由你掌控!只是不要修改成员。如果您不能相信您的开发人员不会破坏课程,请解雇开发人员。
  • 我想知道const_cast 的哪些用法不调用UB?如我所见,它总是用于从对象中删除cv。看来,它的所有用法都调用了 UB。如果是这样,那为什么语言首先要有它。
  • @Nawaz:当您对非 const 对象进行 const 引用时,可以将 const 从引用中丢弃。您只需要确定引用的对象确实是非常量的。
  • @BenjaminLindley:为什么?规范是这样说的吗?

标签: c++ const-cast


【解决方案1】:

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

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

Data 对象的“值”是IDValue 的组合,IdValue 的“keyability”决定了(Id, Value) 对。 换句话说,正是Id->Value对应来表征自己为常数,但在univocal的意义上。

此外,如果 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;

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

      【讨论】:

        【解决方案4】:

        引用cppreference:

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

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

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

        【讨论】:

        • 它咬人的一个例子可能很好:构造,打印id,分配,再次打印id可能会这样做。
        猜你喜欢
        • 2019-12-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-08
        • 1970-01-01
        相关资源
        最近更新 更多