【问题标题】:const vs non-const of container and its content容器及其内容的 const 与非 const
【发布时间】:2013-02-11 16:07:09
【问题描述】:

对于容器类,例如std::vector,有两个不同的常量概念:容器的概念(即它的大小)和元素的概念。似乎std::vector 混淆了这两者,以至于下面的简单代码无法编译:

struct A {
  A(size_t n) : X(n) {}
  int&x(int i) const { return X[i]; }    // error: X[i] is non-const.
private:
  std::vector<int> X;
};

请注意,即使std::vector数据成员(指向数据的开始和结束以及分配缓冲区结束的三个指针)没有被调用它的operator[],这个成员不是const——这不是一个奇怪的设计吗?

还要注意,对于原始指针,这两个常量性概念被巧妙地分开,因此相应的原始指针代码

struct B {
  B(size_t n) : X(new int[n]) {}
  ~B() { delete[] X; }
  void resize(size_t n);                 // non-const
  int&x(int i) const { return X[i]; }    // fine
private:
  int*X;
};

工作得很好。

那么当使用std::vector(不使用mutable)时,正确/推荐的处理方法是什么?

const_cast&lt;&gt;,如

int&A::x(int i) const { return const_cast<std::vector<int>&>(X)[i]; }

认为可以接受(X 已知不是const,所以这里没有 UB)?

EDIT 只是为了防止进一步混淆:我确实想修改元素,即 容器的内容,而不是容器本身(大小和/或内存位置)。

【问题讨论】:

  • std::vector 的数据可以通过调用operator[] 来更改。正如您所写的A::Xa.x(1)++; 是完全合法的,并且会修改向量的内容。
  • @DavidSchwartz 向量的 contents 不是它的实际 data(尽管您可以在逻辑上将它们关联起来)。如果您检查std::vector,它只有 3 个指针作为数据(数据的开头和结尾以及缓冲区的结尾)。这些保持不变。

标签: c++ constants containers


【解决方案1】:

C++ 只支持一级const。至于编译器 关心的是,它是按位常量:实际上在 对象(即计入sizeof)不能修改 玩游戏(const_cast 等),但其他一切都是公平的 游戏。在 C++ 的早期(1980 年代末,1990 年代初)有 关于按位的设计优势进行了很多讨论 const 与逻辑 const(也称为 Humpty-Dumpty const, 因为正如 Andy Koenig 曾经告诉我的,当程序员使用 const,这正是程序员想要的意思)。 最终达成共识,支持逻辑常量。

这确实意味着容器类的作者必须 一个选择。容器的元素是 容器,或者不是。如果它们是容器的一部分,那么它们 如果容器是 const,则无法修改。没有办法 提供选择;容器的作者必须选择一个或 另一个。在这里,似乎也有一个共识: 元素是容器的一部分,如果容器是 const,它们不能被修改。 (也许与 C 风格的数组在这里发挥了作用;如果 C 样式数组是 const, 那么你不能修改它的任何元素。)

和你一样,我也遇到过想要禁止的时候 修改向量的大小(也许是为了保护 迭代器),但不是它的元素。真的没有 满意的解决方案;我能想到的最好的就是创造 一个新类型,它包含一个mutable std::vector,并提供 对应const含义的转发函数 我需要在这种特定情况下。如果你想区分 三个级别(完全 const、部分 const 和非常量), 你需要推导。基类只公开 完全 const 和部分 const 函数(例如 const int operator[]( size_t index ) const;int operator[]( size_t index );,但不是 void push_back( int ););这 允许插入和删除元素的函数是 仅在派生类中公开。不应该的客户 插入或删除元素仅传递非常量引用 到基类。

【讨论】:

  • +1 很好的讨论。不过,我不相信容器类的设计者别无选择。可以实现一个容器类,该类将其元素的 const-ness 作为模板参数,并允许在 const 和非 const 容器之间进行移动转换(使用智能指针)。不过,您的基础派生设计看起来更简单。
  • @Walter 容器类的设计者有各种各样的选择。在这种情况下,std::vector 的设计者制作了一个似乎已达成普遍共识的容器——我所知道的大多数预标准容器都是相同的。在全球范围内,对于提供三个级别的const(无、部分或完整)的容器似乎没有很大的需求,除非在特殊情况下(例如std::array)。 (在标准之前的日子里,我的一个数组类将大小作为构造函数参数,并且以后不可能更改它。)
  • @JamesKanze:解释很好,但是最顶部的前两个开头句给人的印象是按位 const 是 C++ 支持的。 (希望 C++ 程序员没有太多的 TL;DR。) 这句话在编译器后端是正确的,但前端只认为const 是一个句法辅助 - 它用于类型检查,仅此而已。 (顺便说一句,C++11 引入了编译时constexpr。)逻辑 const-ness 只存在于程序员的大脑中。当一个 C++ 程序员开始多线程编程时,就会意识到这些差异。
  • @rwong 语言(及其编译器)在形式上只支持按位常量。但是 const 是类型系统的一部分,当你重载它时,你可以将它定义为你想要的任何含义。今天的共识是程序员应该实现逻辑常量(语言或编译器不能这样做,因为它不知道在任何给定上下文中什么是“逻辑”)。
【解决方案2】:

不幸的是,与指针不同,你不能做类似的事情

std::vector<int> i;
std::vector<const int>& ref = i;

这就是为什么std::vector 无法消除可能适用的两种const 之间的歧义,而且它必须是保守的。我个人会选择做类似的事情

const_cast<int&>(X[i]);

编辑:正如另一位评论者准确指出的那样,迭代器确实模拟了这种二分法。如果您在开头存储了一个 vector&lt;int&gt;::iterator,那么您可以在 const 方法中取消引用它并取回一个非 const int&amp;。我认为。但是你必须小心失效。

【讨论】:

  • 迭代器的使用提出了一个有趣的解决方案:一个只包含开始和结束迭代器的view 类,并且只提供他想要的有限功能。 (就此而言,带有指向向量的指针的视图类也可以。)
【解决方案3】:

这不是一个奇怪的设计,这是一个非常慎重的选择,而且是正确的恕我直言。

您的 B 示例不能很好地类比 std::vector,更好的类比是:

struct C {
   int& get(int i) const { return X[i]; }
   int X[N];
};

但有一个非常有用的区别是可以调整数组的大小。上面的代码由于与原始代码相同的原因无效,数组(或vector)元素在概念上是包含类型的“成员”(技术上的子对象),因此您不应该能够通过const 成员函数。

我会说const_cast 是不可接受的,除非作为最后的手段,否则也不能使用mutable。您应该询问为什么要更改 const 对象的数据,并考虑将成员函数设为非 const。

【讨论】:

  • 这是一个解释问题。如果您将 vector 视为可重新调整大小的 C 数组,那么我同意。但是,那么 resize() 等函数在哪里适合这个类比呢?它们必须不仅仅是常量:您希望能够对元素进行非常量访问,但不能对大小进行访问。 std::vector 无法做到这一点。我认为解决这个问题的常用方法是提供迭代器,它通常允许更改内容,但不能更改容器。
  • 这只是一个比喻,不要太从字面上理解,反正resize不是const,所以允许对对象进行变异。不知道你对迭代器的意思是什么,但没有一个标准容器会给你一个非常量的迭代器到一个 const 容器。
  • 我对迭代器的意思是,如果你有一个(非常量)迭代器,你就不能修改容器。所以容器保持不变,而元素可以被修改。
【解决方案4】:

我建议使用std::vector::at() 方法而不是const_cast

【讨论】:

  • vector::at() 的 const 重载返回一个 const 引用,所以这无济于事
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-13
  • 2015-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-09
  • 1970-01-01
相关资源
最近更新 更多