运算符返回指针的有效性
我正在实现一个二维数组容器(如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