【问题标题】:Const-correctness semantics in C++C++ 中的 const 正确性语义
【发布时间】:2013-10-31 21:24:54
【问题描述】:

为了乐趣和利润™,我正在用 C++ 编写一个 trie 类(使用 C++11 标准。)

我的trie<T> 有一个迭代器trie<T>::iterator。 (它们实际上都是功能const_iterators,因为你不能修改trie 的value_type。)迭代器的类声明部分看起来像这样:

template<typename T>
class trie<T>::iterator : public std::iterator<std::bidirectional_iterator_tag, T> {
    friend class trie<T>;
    struct state {
        state(const trie<T>* const node, const typename std::vector<std::pair<typename T::value_type, std::unique_ptr<trie<T>>>>::const_iterator& node_map_it ) :
            node{node}, node_map_it{node_map_it} {}
// This pointer is to const data:
        const trie<T>* node;
        typename std::vector<std::pair<typename T::value_type, std::unique_ptr<trie<T>>>>::const_iterator node_map_it;
    };
public:
    typedef const T value_type;
    iterator() =default;
    iterator(const trie<T>* node) {
        parents.emplace(node, node->children.cbegin());
            // ...
    }
    // ...
private:
    std::stack<state> parents;
    // ...
};

注意node 指针声明为const。这是因为(在我看来)迭代器不应该修改它指向的节点;它只是一个迭代器。

现在,在我的主要 trie&lt;T&gt; 类的其他地方,我有一个具有通用 STL 签名的擦除函数——它需要一个 iterator 来擦除数据(并返回一个 iterator 到下一个对象)。

template<typename T>
typename trie<T>::iterator trie<T>::erase(const_iterator it)
{
    // ...

    // Cannot modify a const object!
    it.parents.top().node->is_leaf = false;

    // ...
}

编译器报错是因为node 指针是只读的! erase 函数肯定应该修改迭代器指向的 trie,即使迭代器不应该这样做。

所以,我有两个问题:

  1. iterator 的构造函数应该公开吗? trie&lt;T&gt; 有必要的begin()end() 成员,当然trie&lt;T&gt;::iteratortrie&lt;T&gt; 是共同的朋友,但我不'不知道约定是什么。将它们设为私有将解决我对从迭代器的构造函数中删除 const“承诺”的许多焦虑。
  2. 关于迭代器及其node 指针的正确const 语义/约定是什么? 从来没有人向我解释过这一点,我在网上找不到任何教程或文章。这可能是更重要的问题,但它确实需要大量的计划和适当的实施。我想它可以通过实现 1 来规避,但这是事情的原理

【问题讨论】:

  • 你实现的是const_iterator
  • 是的。 trie 将类似数组的对象拆分为树,因此“horse”和“horses”(例如)共享基本字符串“horse”。在这样的结构中,数据不是直接可变的。
  • 这是friends 无法解决的问题:给用户一个常量,给你的friend class trie一个非常量。
  • @rici:问题是,您可以使用const_iterator 来修改容器,但只能通过容器上的非 const 函数间接修改,例如 @987654347 @。因此,提问者使用指向 const 的指针来防止迭代器直接在其自己的成员函数中进行修改(毕竟这正是 const 的用途)。那么,问题是容器应该如何使用const_iterator 来获得一个非常量指针,指向容器中它应该进行修改的位置。
  • @SteveJessop:好点。我不知何故完全错过了erase 采用const_iterator,而不是iterator 的事实。谢谢。

标签: c++ c++11 iterator


【解决方案1】:

1) 所有迭代器都必须是可复制构造的。您的迭代器是双向的,因此也需要默认构造(http://en.cppreference.com/w/cpp/concept/ForwardIterator),尽管我不知道为什么。所以默认构造函数需要公开,但你可以用const trie&lt;T&gt;* 做你喜欢的事情。我认为它应该是私有的,因为此类的目的是为用户提供对 trie 的迭代器,因此它的公共接口应该只是适当类别的迭代器的接口。不需要任何额外的公共构造函数。

2) erase 是一个非常量函数。您可以有效传递给它的唯一迭代器是引用与调用该函数相同的 trie 的迭代器,这意味着(我认为,尽管我不太确定我是否遵循了您的设计)父级的整个层次结构是非常量对象。所以我怀疑这是你可以const_cast&lt;trie&lt;T&gt;*&gt;(it.parents.top().node) 的情况之一。 iterator 不允许使用它来修改 trie,这就是为什么你希望它保存一个指向 const 的指针。但是当你持有一个指向 trie 的非 const 指针时,即this,你可以修改它的任何你喜欢的部分,而迭代器只是给你一个开始修改的位置。

您可以在这里绘制一些更一般的 const-safety 原则。 container::erase(const_iterator) 函数中的一种可能情况是,您从迭代器中获得的 const container* 等于 this。在那种情况下,const_cast 肯定既安全又合法(也是不必要的,因为您可以只使用this,但这与它是否是 const 正确无关)。在您的容器中,它(通常)不等于this,它指向几个trie 对象之一,这些对象共同构成this 所属的分层容器。好消息是,整个容器在逻辑上是 const 或逻辑上非常量,因此 const_cast 就像它是一个对象一样安全和合法。但是要证明正确有点困难,因为您必须确保在您的设计中,整个分层容器确实像我假设的那样共享非常量。

【讨论】:

  • 当您可以定义一个没有初始值的迭代器变量时,许多标准算法会更加自然。这需要默认初始化或其他更丑陋的解决方案,如假值、动态分配或 boost::optional-like hacks。
  • 您对设计的看法是正确的...我非常不愿意 const_cast&lt;&gt; 离开 const-ness。但是,如果采用指针的构造函数是私有的,那么我不介意(相当多)使迭代器的指针非常量。由于这个特殊的设计,堆栈对于在特里树上上下移动是必需的。
  • @BatchyX:是的,这就是我在默默地猜测自己一定是这样的,但我暂时想不出一个标准算法,其明显的实现需要一个未初始化的迭代器。也就是说,如果这些概念是为了支持旧的“在函数顶部定义所有内容”编码风格,那么你说的当然是对的,如果你不能默认构造,那么你必须分配任何任意你周围的类型的值(很可能是startfinish 参数),知道你永远不会使用该值。
  • @thirtythreeforty:假设您继续并将其设为指向非常量的指针。很快您将准备好实施trie&lt;T&gt;::const_iterator begin() consttrie&lt;T&gt;::const_iterator 有一个数据成员是 trie&lt;T&gt;*。考虑到在begin() const 内,this 的类型为const trie&lt;T&gt;*,您将在哪里获得该数据成员的值?
  • @thirtythreeforty:你可以做那个模板,但它对你没有帮助,因为trie::erase 需要一个const_iterator。因此,如果它要从迭代器中读取一个指针而不强制转换它,那么该指针需要在const_iteratoriterator 中指向非常量。在 C++03 中,容器 erase 函数确实只接受 iterator,可能是因为在过去的某个时候 Stepanov 或其他参与设计和实现容器接口的人,恰好遇到了这个问题,并决定避免它而不是解决它;-)
【解决方案2】:

trie 的非const 方法希望修改const_iterator 指向的内容(它指向的内容应该在triethis 实例内)应该根据需要只使用const_cast。您知道这是一个安全且已定义的强制转换,因为如果有人设法调用此trie 的非const 实例,那么此trie 实例本身就不是const。*

或者,您可以做相反的事情并在const_iterator 中保留指向trie 的非const 指针,这将在构造时需要const_cast。出于同样的原因,这是安全的,如上所述。 const_iterator 方法将仅提供 const 访问权限,因此用户无法更改迭代器指向的 trie 的部分。如果const_iterator 需要在非const trie 方法中进行突变,那没关系,因为trie 首先一定不是const。*

这里const_casting 安全的前提是,只要您不改变该对象,就可以安全地持有和使用指向const 对象的非const 指针。并且可以安全地将指针的 constness 丢弃,该指针指向最初未声明为 const 的东西。

*是的,非const trie 方法的调用者可能以未定义的方式拥有const_cast;但在这种情况下,导致未定义行为的责任在他们头上,而不是tries。

【讨论】:

  • 这意味着您不能在 const trie&lt;T&gt; 上创建迭代器。
  • @Adam,我认为我可以进一步模板化 trie 以允许这样做。
  • @thirtythreeforty 模板无济于事。您不能将 const 指针分配给非 const 指针(显然,没有 const_cast,但您在这里所拥有的不需要)。
  • @Adam 你可以像你说的那样const_cast,这应该是安全的,因为你永远不会在const trie上调用非const方法,你只能得到@ 987654356@ 通过迭代器访问,即使它在内部将其保存为非const。我理解你的担心,这听起来很脏。
  • @Adam 它会允许什么错误?我在这里关于安全的想法的前提是,只要你不改变它,非const 访问const 的东西是安全的。迭代器的用户不能改变迭代器指向的内容,因为他们只有const 访问权限,而trie 本身可以在您调用非const 方法时改变iterator 指向的内容,因为如果您可以trie 上调用非const 方法,那么trie 本身就不是const
【解决方案3】:
  1. iterator 的构造函数应该公开吗?

至少是复制构造函数,是的。看看这张图表,它描述了每种类型的迭代器应该具有的特征:

http://www.cplusplus.com/reference/iterator/

所有迭代器类型都应该是可复制构造、可复制赋值和可破坏的,这意味着它们需要公共的复制构造函数。一些迭代器,例如RandomAccessIterator 也应该是默认可构造的,因此默认构造函数也应该是公共的。

  1. 关于迭代器及其node 指针的正确const 语义/约定是什么?

如果你想拥有一个erase,那么你实际上并没有const_iterator,你有一个普通的iterator。两者的区别在于,如果你有一个consttrie 对象,那么你只能从中得到一个const_iterator,因为你不能以任何方式修改它。

您可以在 STL 容器中注意到这一点。他们往往同时拥有:

iterator begin();
const_iterator begin() const;

通常发生的情况是你实现了const_iterator 然后:

class iterator : public const_iterator {...};

它实现了一个或两个非常量函数。这对你来说可能只意味着erase,因为你的operator* 将保持不变。

【讨论】:

  • 为什么RandomAccessIterator 必须是默认可构造的?这只是课程规范之一吗?
  • @thirtythreeforty 是的,它已被指定(RandomAccessIterator 文本实际上是规范的链接)。
  • @thirtythreeforty 我认为逻辑是 RandomAccessIterator 基本上应该像指针一样工作,并且可以默认构造指针。
  • @Adam:这不是指向“规范”的链接,而是指向某个解释标准的网站的链接。当然,我的链接也是如此。
  • @Adam:我有一个更具建设性的评论,关于“如果你想要擦除,那么你实际上没有 const_iterator,你有一个常规迭代器”。这在 C++11 中有所改变,容器的 erase() 函数现在采用 const_iterator。我相信逻辑是你可能想要执行一些不允许修改容器的基于迭代器的算法(因此需要const_iterators),以便计算出容器中的位置。然后,如果您自己拥有对容器的非常量引用,则您有权使用它来修改容器而无需进行讨厌的强制转换。
猜你喜欢
  • 1970-01-01
  • 2012-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多