运算符返回指针的有效性

我正在实现一个二维数组容器(如boost::multi_array<T,2> ,主要用于练习)。 为了使用双索引表示法( a[i][j] ),我引入了一个代理类row_view (和const_row_view但我不担心const_row_view在这里),它保持指向行的开始和结束的指针。

我也想能够遍历行和单独的行内的元素:

matrix<double> m;
// fill m
for (row_view row : m) {
    for (double& elem : row) {
        // do something with elem
    }
}

现在, matrix<T>::iterator类(它是为了遍历行)保留一个专用的row_view rv; 在内部跟踪迭代器指向的行。 自然, iterator还实现取消引用功能:

  • 对于operator*() ,通常需要返回一个引用。 相反,这里正确的做法似乎是通过值返回row_view (即返回专用row_view的副本)。 这可以确保当迭代器高级时, row_view仍然指向前一行。 (在某种程度上, row_view作用就像一个引用)。
  • 对于operator->() ,我不太确定。 我看到两个选项:

  • 返回指向迭代器的private row_view的指针:

    row_view operator->() const { return &rv; }
    
  • 返回一个指向新的row_view (私有副本)的指针。 由于存储寿命,这将不得不在堆上分配。 为了确保清理,我将它包装在unique_ptr

    std::unique_ptr<row_view> operator->() const {
        return std::unique_ptr<row_view>(new row_view(rv));
    }
    
  • 显然,2更正确。 如果在调用operator->之后迭代器被提前,则在1中指向的row_view将会改变。 然而,我能想到这个问题的唯一途径是如果operator->被其全名调用并且返回的指针被绑定:

    matrix<double>::iterator it = m.begin();
    row_view* row_ptr = it.operator->();
    // row_ptr points to view to first row
    ++it;
    // in version 1: row_ptr points to second row (unintended)
    // in version 2: row_ptr still points to first row (intended)
    

    但是,这不是你通常使用operator-> 。 在这种用例中,您可能会调用operator*并保留对第一行的引用。 通常,可以立即使用指针调用row_view的成员函数或访问成员,例如row_view it->sum()

    我现在的问题是这样的:鉴于->语法建议立即使用, operator->返回的指针的有效性被认为仅限于这种情况,或者上述“滥用”的安全实现可以解决吗?

    显然,解决方案2的成本更高,因为它需要堆分配。 这当然是非常不理想的,因为解除引用是一项很常见的任务,并且没有真正的需要:使用operator*可以避免这些问题,因为它会返回row_view的堆栈分配副本。


    如你所知, operator->递归地应用于函数返回类型,直到遇到一个原始指针。 唯一的例外是当它像名称中的名称一样被调用时。

    您可以使用它来获得优势并返回自定义代理对象。 为了避免在上一段代码中出现这种情况,这个对象需要满足几个要求:

  • 它的类型名称应该是matrix<>::iterator私有名称,所以外部代码无法引用它。

  • 其建设/复制/分配应该是私人的。 matrix<>::iterator将有机会成为朋友。

  • 一个实现看起来像这样:

    template <...>
    class matrix<...>::iterator {
    private:
      class row_proxy {
        row_view *rv_;
        friend class iterator;
        row_proxy(row_view *rv) : rv_(rv) {}
        row_proxy(row_proxy const&) = default;
        row_proxy& operator=(row_proxy const&) = default;
      public:
        row_view* operator->() { return rv_; }
      };
    public:
      row_proxy operator->() {
        row_proxy ret(/*some row view*/);
        return ret;
      }
    };
    

    operator->的实现返回一个命名对象,以避免由于C ++ 17中的保证副本精简而导致的任何漏洞。 使用内it->mem算符( it->mem )的代码将像以前一样工作。 但是,如果试图在不放弃返回值的情况下通过名称调用operator->() ,则不会进行编译。

    现场示例

    struct data {
        int a;
        int b;
    } stat;
    
    class iterator {
        private:
          class proxy {
            data *d_;
            friend class iterator;
            proxy(data *d) : d_(d) {}
            proxy(proxy const&) = default;
            proxy& operator=(proxy const&) = default;
          public:
            data* operator->() { return d_; }
          };
        public:
          proxy operator->() {
            proxy ret(&stat);
            return ret;
          }
    };
    
    
    int main()
    {
      iterator i;
      i->a = 3;
    
      // All the following will not compile
      // iterator::proxy p = i.operator->();
      // auto p = i.operator->();
      // auto p{i.operator->()};
    }
    

    在进一步审查我提出的解决方案后,我意识到这不像我想象的那样简单。 不能在iterator的作用域外创建代理类的对象,但仍然可以绑定对它的引用:

    auto &&r = i.operator->();
    auto *d  = r.operator->();
    

    从而允许再次应用operator->()

    直接的解决方案是限定代理对象的运算符,并使其仅适用于右值。 就像我现场的例子一样:

    data* operator->() && { return d_; }
    

    这将导致上述两行再次发出错误,而正确使用迭代器仍然有效。 不幸的是,由于铸造的可用性,这仍然不能保护API免受滥用,主要是:

    auto &&r = i.operator->();
    auto *d  = std::move(r).operator->();
    

    这是对整个努力的致命打击。 这没有阻止。

    因此,总而言之,对于迭代器对象上的operator->方向调用没有保护。 至多,我们只能使API非常难以正确使用,而正确的用法仍然很容易。

    如果row_view副本的创建是广泛的,这可能就足够了。 但这是你要考虑的。

    另一个值得考虑的问题,我在这个答案中没有涉及到的是,代理可以用来在写入时实现复制。 但是,除非采取了非常谨慎的态度,并且采用了相当保守的设计,否则该班级可能与我的答案中的代理人一样脆弱。

    链接地址: http://www.djcxy.com/p/39815.html

    上一篇: Validity of pointer returned by operator

    下一篇: Slightly different result from exp function on Mac and Linux