c – operator->返回的指针的有效性

前端之家收集整理的这篇文章主要介绍了c – operator->返回的指针的有效性前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我正在实现一个二维数组容器(如boost :: multi_array< T,2>,主要用于练习).为了使用双索引表示法(a [i] [j]),我引入了一个代理类row_view(和const_row_view但我并不关心这里的constness),它保留了指向行的开头和结尾的指针.

我还希望能够分别遍历行和行内的元素:

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

现在,矩阵< T> :: iterator类(用于遍历行)保持私有row_view rv;在内部跟踪迭代器指向的行.当然,迭代器也实现了dereferenciation函数

>对于operator *(),通常需要返回引用.相反,这里正确的做法似乎是按值返回row_view(即返回私有row_view的副本).这可确保在迭代器前进时,row_view仍指向上一行. (在某种程度上,row_view就像引用一样).
>对于operator->(),我不太确定.我看到两个选择:

>返回指向迭代器的私有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将更改.但是,我能想到这个问题的唯一方法就是操作符 – >被它的全名调用并且返回的指针被绑定:

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的成员函数或访问成员,例如IT->总和().

我现在的问题是:鉴于 – >语法建议立即使用,是运算符返回的指针的有效性 – >被认为仅限于这种情况,或安全实施是否会导致上述“滥用”?

显然,解决方案2更昂贵,因为它需要堆分配.这当然是非常不受欢迎的,因为dereferenciation是一个非常常见的任务,并没有真正需要它:使用operator *代替避免这些问题,因为它返回row_view的堆栈分配副本.

解决方法

如您所知,operator->在函数返回类型上递归应用,直到遇到原始指针.唯一的例外是它在代码样本中按名称调用.

您可以使用它,并返回自定义代理对象.要避免上一个代码段中的场景,此对象需要满足以下几个要求:

>它的类型名称应该是矩阵<> :: 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;
  }
};

操作符的实施 – >返回一个命名对象,以避免由于C 17中保证的复制省略而导致的任何漏洞.使用运算符内联的代码(it-> mem)将像以前一样工作.但是,任何尝试按名称调用operator->()而不丢弃返回值的尝试都不会编译.

Live Example

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->()};
}

在进一步审查我建议的解决方案后,我意识到这并不像我想象的那么简单.无法在迭代器范围之外创建代理类的对象,但仍然可以绑定对它的引用:

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

因此允许再次应用operator->().

直接的解决方案是限定代理对象的运算符,并使其仅适用于rvalues.就像我的实例一样:

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

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

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

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

总而言之,没有对运算符的方向调用的保护 – >在迭代器对象上.最多,我们只能使API难以正确使用,而正确的使用仍然很容易.

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

我在这个答案中没有提到的另一个需要考虑的问题是代理可以用来实现写入时的复制.但是在我的回答中,这个类可能和代理一样容易受到攻击,除非非常小心并且使用了相当保守的设计.

猜你在找的C&C++相关文章