Jon*_*ann 7 c++ containers iterator unique-ptr
我正在实现一个二维数组容器(比如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
}
}
Run Code Online (Sandbox Code Playgroud)
现在,matrix<T>::iterator类(用于遍历行)在row_view rv;内部保持私有以跟踪迭代器指向的行.当然,iterator也实现了dereferenciation功能:
operator*(),人们通常想要返回一个参考.相反,这里正确的做法似乎返回一个row_view值(即返回私有的副本row_view).这确保了当迭代器前进时,row_view静止指向前一行.(在某种程度上,row_view行为就像参考一样).因为operator->(),我不太确定.我看到两个选择:
返回指向row_view迭代器私有的指针:
row_view* operator->() const { return &rv; }
Run Code Online (Sandbox Code Playgroud)返回指向新的指针row_view(私有指针的副本).由于存储生命周期,必须在堆上分配.为了确保清理,我将其包装成unique_ptr:
std::unique_ptr<row_view> operator->() const {
return std::unique_ptr<row_view>(new row_view(rv));
}
Run Code Online (Sandbox Code Playgroud)显然,2更正确.如果迭代器在 operator->调用后被row_view提升,那么1中指向的迭代器将会改变.但是,我能想到这个问题的唯一方法是,如果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)
Run Code Online (Sandbox Code Playgroud)
但是,这不是您通常使用的方式operator->.在这种用例中,您可能会调用operator*并保留对第一行的引用.通常,人们会立即使用指针调用成员的成员函数row_view或访问成员,例如it->sum().
我现在的问题是:鉴于->语法建议立即使用,被operator->认为仅限于该情况返回的指针的有效性,还是安全实现会导致上述"滥用"?
显然,解决方案2更昂贵,因为它需要堆分配.这当然是非常不受欢迎的,因为dereferenciation是一个非常常见的任务,并没有真正需要它:使用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;
}
};
Run Code Online (Sandbox Code Playgroud)
的实现operator->返回一个命名对象,以避免由于 C++17 中保证复制省略而导致的任何漏洞。使用内联运算符 ( 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->()};
}
Run Code Online (Sandbox Code Playgroud)
在进一步审查我建议的解决方案后,我意识到它并不像我想象的那么万无一失。无法在 的范围之外创建代理类的对象iterator,但仍然可以绑定对其的引用:
auto &&r = i.operator->();
auto *d = r.operator->();
Run Code Online (Sandbox Code Playgroud)
从而允许再次申请operator->()。
直接的解决方案是限定代理对象的运算符,并使其仅适用于右值。就像我的例子一样:
data* operator->() && { return d_; }
Run Code Online (Sandbox Code Playgroud)
这将导致上面的两行再次发出错误,而正确使用迭代器仍然有效。不幸的是,由于转换的可用性,这仍然不能保护 API 免受滥用,主要是:
auto &&r = i.operator->();
auto *d = std::move(r).operator->();
Run Code Online (Sandbox Code Playgroud)
这对整个努力来说是致命的打击。这是无法阻止的。
operator->因此总而言之,对迭代器对象的方向调用没有任何保护。我们最多只能让 API 很难被错误地使用,而正确的使用仍然很容易。
如果row_view副本的创建范围很广,这可能就足够了。但这是你要考虑的。
另一个需要考虑的点(我在这个答案中没有提到)是代理可以用来实现写时复制。但是,除非非常小心并使用相当保守的设计,否则该类可能与我的答案中的代理一样容易受到攻击。
| 归档时间: |
|
| 查看次数: |
242 次 |
| 最近记录: |