【问题标题】:How to have const members in stl container values in C++?如何在 C++ 的 stl 容器值中有 const 成员?
【发布时间】:2013-04-20 18:27:51
【问题描述】:

我喜欢将我的 C++ 成员变量设置为 const,如果它们在构造对象后不应更改,但是,有时它们需要由 STL 修改。例如,如果我有一个包含 const 成员的类的向量,并且我尝试交换向量中的两个元素,STL 会尝试使用默认生成的 operator=(),但由于 const 成员变量而失败。

我觉得operator=() 就像一个构造函数,因为正在创建整个对象,因此希望通过某种方式允许operator=() 同时仍然拥有我的 const 成员变量。

在 C++03 中有没有办法做到这一点?如果不是,那么在 C++11 中,可能就地构造是为了这个?

class Foo {
  const int _id;

  static int _generate_unique_id();

public:
  Foo()
    : _id(_generate_unique_id()) {
  }
};

vector<Foo> foo_vector;

// Fill foo_vector with several entries:
//   [...]

// Try to swap the first and second elements of the vector:
swap(*foo_vector.begin(), *(foo_vector.begin() + 1));
// The above fails to compile due to const member variable _id
// prohibits us from using the default assignment operator.

【问题讨论】:

  • 如何在保留const 的同时交换两个元素?
  • IMO,尝试这样做会出现问题:一个 const 对象(顺便说一句,您可能会争辩说您不应该公开此数据成员)说“我不会改变我的可观察行为”。现在,如果有人拥有指向容器中某个元素的指针/引用或迭代器,而您替换了该元素,那么可观察到的行为就会发生变化。
  • @AlexChamberlain swap(T&amp; lhs, T&amp; rhs) { T tmp_lhs(lhs); lhs.~T(); new(&amp;lhs)(rhs); rhs.~T(); new(&amp;rhs)(tmp_lhs); } 在创建和销毁之间没有修改任何对象! ;)
  • @yakk 现在,让它异常安全!
  • @didierc 我们有一个T 的容器。我们正在四处移动。切片不是问题。

标签: c++ c++11 constants assignment-operator c++03


【解决方案1】:

在标准库容器中存储不可分配对象的解决方案是存储(智能)指向对象的指针。并不总是理想的,但可行。

【讨论】:

    【解决方案2】:

    例如,如果我有一个包含 const 成员的类的向量,并且我尝试交换向量中的两个元素,STL 会尝试使用默认生成的 operator=(),但由于 const 成员变量而失败。

    实现“三大半”(默认和复制构造函数、赋值运算符和交换),赋值运算符显式跳过 _id 的重新赋值。

    【讨论】:

      【解决方案3】:

      您想要的是 Java 不可变习语之类的东西。 这对于指针(以及垃圾收集语言)来说很棒,而对于像 C++ 这样的值语义语言来说则不那么棒了。

      您有两种解决方案:

      1 - 使您的对象在界面中不可变

      成员是私有的(或者应该是私有的),所以除了类本身(和它的朋友)之外没有人可以修改它。因此,您需要做的就是确保没有人在(您控制的)类中执行任何操作,并且在受保护/公共接口中没有提供任何方法让其他人有权这样做。

      TL;DR:让你的对象非 const。不要在类内修改它。添加一个 const getter。移除 setter(如果有的话)。

      2 - 使用 std::unique_ptr

      现在我们遵循 Java 习语。对象是 const,但指针可以重新归属,这正是你想要的。

      这实际上比 const Data * 成员替代方案更好,因为它具有异常安全性。

      奖励:不要手动调用析构函数来重新构造对象

      有一个答案建议这样做。

      正如sehe 首先提到的,不要那样做

      您的意思是提高代码的质量,这意味着您的代码需要在某一时刻保持异常安全。并且手动玩弄你的对象生命周期会使它在质量代码中无法使用。

      阅读 Herb Sutter 关于该主题的文章:http://www.gotw.ca/gotw/023.htm

      【讨论】:

      • 再次考虑这个问题(在将近两年半之后!),我认为你在正确的轨道上提到价值语义作为潜在问题之一。现在,我会说重生对象是一个不充分的解决方案。但一个合适的解决方案取决于这个_id 成员实际上是什么意思。如果它是对象值的一部分,由于分配/交换Foo 的保证,它一定不是不可变的;否则,它可以是一个 const 数据成员(例如,如果它是一个与地址非常相似的对象 id),并且必须手动实现特殊的成员函数。
      • 关于您的 Bonus 段落:我认为这对于 OP 的简单 int 案例来说不是问题,但如前所述,我认为这种技术是不足,因为它不必要地使代码更难理解和脆弱。 (所以我不认为它在质量代码中是不可用的,但可以默默地引入问题,例如在更改 _id 的类型时。)
      【解决方案4】:

      const 成员不仅可以防止程序员在其生命周期内修改成员的值;它还通过指定修改它的尝试是未定义的行为来启用编译器优化(请参阅const member and assignment operator. How to avoid the undefined behavior?)。

      做你想做的事情的一种方法是编写一个nonmodifiable容器,它提供语义const,同时让你作为程序员修改包含的值的可能性:

      template<typename T> class nonmodifiable {
         T t;
      public:
         nonmodifiable(T t): t{std::move(t)} {}
         operator const T &() const { return t; }
         nonmodifiable &operator=(const nonmodifiable &) = delete;
      };
      

      你现在可以写了:

      class Foo {
        nonmodifiable<int> _id;
        // etc.
      };
      

      因为_id 和它所包含的值都不是const,所以使用destruct-placement new dance 重新分配它的值:

      Foo &operator=(const Foo &foo) {
         if (this != &foo) {
            _id.~nonmodifiable<int>();
            new (&_id) nonmodifiable<int>(foo._id);
         }
         return this;
      }
      

      【讨论】:

      • 永远不要使用在复制构造方面通过显式析构函数然后放置 new 来实现复制分配的技巧,即使新闻组每三个月就会出现这种技巧 GotW #23
      • @sehe 那里出现的许多缺陷都与切片/继承有关。我不明白为什么要从nonmodifiable 继承,当然它不是在这里执行这个习语的继承类型。
      • @dyp :Sehe 是对的,Herb Sutter 不同意 (gotw.ca/gotw/023.htm) 这个成语是有充分理由的。主要的反对意见是:这段代码是异常不安全的设计,这有点讽刺意味,因为重点是增加某种安全性。实际上,这种防止风险仅限于类内部的病态案例的令人反感的设计选择具有阻止用户在类外设计异常安全代码(即所有代码)的后果。
      • @dyp : certainly it's not an inherited type on which this idiom is performed here :我看不到在那个“不可修改”模板中的什么地方有一个代码主动阻止将它与可遗传对象一起使用,所以你的论点是无效的。跨度>
      • @paercebal 首先,我同意这个成语的用法非常有限。我也同意异常安全通常是这种技术的一个问题,它很容易导致 UB。但是,在这个用例中,我们知道placement-new 不会抛出,并且我们没有使用从nonmodifiable 派生的类_id。所以这个用法是可以的。最好添加一些static_asserts,并使nonmodifiable 成为final 类(但即便如此,也不能断言它不包含const 或引用数据成员)。
      猜你喜欢
      • 2011-03-23
      • 2011-04-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多