Validity of the pointer returned by the operator->

I implement a two-dimensional array container (e.g. boost::multi_array<T,2> , mainly for practice). To use the double index notation ( a[i][j] ), I introduced the row_view proxy class (and const_row_view , but I'm not interested in the constant here), which holds the pointer to the beginning and end of the line.

I would also like to be able to iterate through the rows and over the elements inside the string separately:

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

Now the matrix<T>::iterator class matrix<T>::iterator (which is designed to iterate through the rows) stores the private row_view rv; inside to keep track of the line the iterator points to. Naturally, iterator also implements separation functions:

  • for operator*() , I would usually like to return a link. Instead, the right thing here seems to return the row_view value by value (i.e., it returns a copy of the private row_view ). This ensures that when the iterator is advanced, row_view still points to the previous row. (In a sense, row_view acts as a link).
  • for operator->() , I'm not sure. I see two options:

    • Returns a pointer to the private row_view iterator:

       row_view operator->() const { return &rv; } 
    • Return the pointer to the new row_view (copy private). Because of the shelf life that needs to be allocated to the heap. To provide cleanup, I wrapped it in unique_ptr :

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

Obviously 2 is more correct. If the iterator advances after calling operator-> , then the row_view that is listed in 1 will be changed. However, the only way I can think about where it matters is that operator-> was called by its full by name, and the returned pointer was bound:

 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) 

However, this is not how you usually use operator-> . In this case, you will probably call operator* and save the link to the first line. As a rule, you can immediately use a pointer to call a member function row_view or access an element, for example. it->sum() .

Now my question is this: given that the syntax -> implies immediate use, is the fidelity of the pointer returned by operator-> considered limited by this situation, or will there be a secure account to implement the above "abuse" "?

Obviously, solution 2 is much more expensive since it requires heap allocation. This, of course, is very undesirable, as separating names is a fairly common task, and there is no real need for it: using operator* instead fixes these problems, since it returns an instance of row_view placed on the stack.

+7
c ++ iterator containers unique-ptr
source share
1 answer

As you know, operator-> is applied recursively to the return type of functions until an unhandled pointer is found. The only exception is when it is called by name, as in your code example.

You can take advantage of this and return your own proxy object. To avoid the script in your last code snippet, this object must satisfy several requirements:

  • Its type name must be closed to matrix<>::iterator , so external code cannot reference it.

  • Its construction / copying / assignment should be closed. matrix<>::iterator will have access to them due to the fact that he is a friend.

The implementation will look something like this:

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

The operator-> implementation returns a named object to avoid any loopholes due to guaranteed copying in C ++ 17. Code that uses the inline operator ( it->mem ) will continue to work. However, any attempt to call operator->() by name without discarding the return value will not compile.

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

After further consideration of my proposed solution, I realized that this is not as flawless as I thought. You cannot create an object of the proxy class outside the scope of iterator , but you can still bind a link to it:

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

Thus, you can again apply operator->() .

The immediate solution is to qualify the proxy operator and apply it only to r values. For example, for my live example:

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

This will cause the two lines above to throw an error again, while using the iterator correctly still works. Unfortunately, this still does not protect the API from abuse, due to the availability of casting, mainly:

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

This is a fatal blow to the whole case. This does not interfere.

So, in conclusion, there is no protection against calling directions to operator-> iterator object. In the best case, we can make the API very difficult to use incorrectly, while proper use remains easy.

If making row_view copies is expansive, that might be good enough. But you should consider this.

Another point to consider that I did not touch on in this answer is that a proxy server can be used to implement copy-on-write. But this class can be as vulnerable as the proxy server in my answer, unless great care is taken and a rather conservative design is used.

+3
source share

All Articles