【问题标题】:Validity of pointer returned by operator->operator-> 返回的指针的有效性
【发布时间】:2017-11-29 05:19:23
【问题描述】:

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

我还希望能够分别迭代行和行中的元素:

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

现在,matrix&lt;T&gt;::iterator 类(用于迭代行)在内部保留了一个私有 row_view rv;,以跟踪迭代器指向的行。当然,iterator 也实现了解引用功能:

  • 对于operator*(),通常希望返回一个引用。相反,这里正确的做法似乎是按值返回 row_view(即返回私有 row_view 的副本)。这确保了当迭代器前进时,row_view 仍然指向前一行。 (在某种程度上,row_view 的行为类似于引用)。
  • 对于operator-&gt;(),我不太确定。我看到两个选项:

    1. 返回指向迭代器私有row_view的指针:

      row_view* operator->() const { return &rv; }
      
    2. 返回指向新row_view 的指针(私有的副本)。由于存储寿命,必须在堆上分配。为了确保清理,我将其包装在unique_ptr

      std::unique_ptr<row_view> operator->() const {
          return std::unique_ptr<row_view>(new row_view(rv));
      }
      

显然,2 更正确。如果迭代器在调用 operator-&gt; 之后 是高级的,则 1 中指向的 row_view 将发生变化。但是,我能想到的唯一方法是,operator-&gt; 是否按其全名调用并且返回的指针已绑定:

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-&gt; 的方式。在这种用例中,您可能会调用operator* 并保留对第一行的引用。通常,人们会立即使用指针来调用row_view 的成员函数或访问成员,例如it-&gt;sum().

我现在的问题是:鉴于-&gt; 语法建议立即使用,operator-&gt; 返回的指针的有效性是否被认为仅限于这种情况,或者 安全上述“滥用”的实施原因?

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

【问题讨论】:

  • 我很确定你必须返回 operator * 的引用和 operator -&gt; 的指针:stackoverflow.com/questions/37191290/…
  • 根据cppreference:“运算符 -> 的重载必须返回一个原始指针或返回一个对象(通过引用或按值),而运算符 -> 反过来又被重载。”
  • 至于operator*,我没有发现任何限制。编译器肯定不会抱怨。
  • 它不会抱怨,但标准的期望是获取对容器包含的元素的引用。
  • 我认为row_view 有点像“智能参考”。我同意应该滥用运算符重载违背用户的期望,但在这种情况下,它似乎满足了用户的期望

标签: c++ iterator containers unique-ptr


【解决方案1】:

如您所知,operator-&gt; 递归地应用于函数返回类型,直到遇到原始指针。唯一的例外是在您的代码示例中按名称调用它。

您可以利用它并返回自定义代理对象。为避免您上一个代码 sn-p 中的情况,此对象需要满足几个要求:

  1. 它的类型名应该是matrix&lt;&gt;::iterator私有的,所以外部代码不能引用它。

  2. 它的构造/复制/分配应该是私有的。 matrix&lt;&gt;::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-&gt; 的实现返回一个命名对象,以避免由于 C++17 中保证复制省略而导致的任何漏洞。使用内联运算符 (it-&gt;mem) 的代码将像以前一样工作。但是,任何通过名称调用 operator-&gt;() 而不丢弃返回值的尝试都不会编译。

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

在进一步查看我建议的解决方案后,我意识到它并不像我想象的那样万无一失。不能在iterator 范围之外创建代理类的对象,但仍然可以绑定对它的引用:

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

因此允许再次申请operator-&gt;()

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

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

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

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

这对整个工作来说是致命的打击。没有办法阻止这一点。

因此,总而言之,对迭代器对象上的operator-&gt; 的方向调用没有任何保护。我们最多只能让 API 真的很难用错,而正确的使用仍然很容易。

如果row_view 副本的创建范围很广,这可能就足够了。但这需要你考虑。

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

【讨论】:

  • 所以我做对了:调用 operator-&gt; 而不丢弃返回值会导致编译器错误,因为返回类型 (row_proxy) 是私有的?
  • @Jonas - 不只是。将返回类型设为私有只能防止一次“攻击”。隐藏构造函数和赋值运算符可防止使用类型推导auto p = ... 捕获它。
  • 感谢您提供详细信息和其他信息。我想,我对你的回答很满意,可能很快就会接受。这不是我所期望的。解决这个问题并确保operator-&gt; 不能被“恶意”使用(以不同程度的成功)我什至没有想到。我最初的问题是更多地针对什么被认为是惯用的。
  • 即使在最初的版本 1 中,我认为也很难意外滥用此功能,因为 operator-&gt; 必须明确地用全名调用,我认为他们中没有任何人正确的头脑宁愿只使用operator*。当有恶意时,我想总有一些方法可以解决任何问题。
  • 我不知道对成员函数进行右值限定是可能的。这很酷,尽管我正在努力想出另一个有用的场景。无论如何,非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-10
相关资源
最近更新 更多