Действительность указателя, возвращаемого оператором-›

Я реализую контейнер двумерного массива (например, boost::multi_array<T,2>, в основном для практики). Чтобы использовать нотацию с двойным индексом (a[i][j]), я ввел прокси-класс row_viewconst_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->() я не уверен. Я вижу два варианта:

    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->, 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)

Однако это не то, как вы обычно используете operator->. В таком случае вы, вероятно, вызовете operator* и сохраните ссылку на первую строку. Обычно можно было бы немедленно использовать указатель для вызова функции-члена row_view или доступа к члену, например. it->sum().

Теперь мой вопрос заключается в следующем: учитывая, что синтаксис -> предполагает немедленное использование, считается ли допустимость указателя, возвращаемого operator->, ограниченной этой ситуацией, или будет ли безопасная учетная запись реализации для вышеуказанного «злоупотребления "?

Очевидно, что решение 2 намного дороже, так как требует выделения кучи. Это, конечно, очень нежелательно, так как разыменование является довольно распространенной задачей, и в ней нет реальной необходимости: вместо этого использование operator* позволяет избежать этих проблем, поскольку оно возвращает выделенную в стеке копию row_view.


person Jonas Greitemann    schedule 26.06.2017    source источник
comment
Я почти уверен, что вам нужно вернуть ссылку для operator * и указатель для operator ->: stackoverflow.com/questions/37191290/   -  person NathanOliver    schedule 26.06.2017
comment
Согласно cppreference: перегрузка оператора -› должна либо возвращать необработанный указатель или вернуть объект (по ссылке или по значению), для чего оператор -› в свою очередь перегружен.   -  person Jonas Greitemann    schedule 26.06.2017
comment
Что касается operator*, я не нашел никаких ограничений. Компилятор точно не жалуется.   -  person Jonas Greitemann    schedule 26.06.2017
comment
Он не будет жаловаться, но стандартное ожидание — получить ссылку на элемент, содержащийся в контейнере.   -  person NathanOliver    schedule 26.06.2017
comment
Я думаю, что row_view действует как умная ссылка. Я согласен с тем, что следует злоупотреблять перегрузкой оператора вопреки ожиданиям пользователей, но в этом случае, похоже, это соответствует ожиданиям пользователя.   -  person Jonas Greitemann    schedule 26.06.2017


Ответы (1)


Как вы знаете, operator-> применяется рекурсивно к возвращаемому типу функций до тех пор, пока не встретится необработанный указатель. Единственное исключение - это когда он вызывается по имени, как в вашем примере кода.

Вы можете использовать это в своих интересах и вернуть собственный прокси-объект. Чтобы избежать сценария в вашем последнем фрагменте кода, этот объект должен удовлетворять нескольким требованиям:

  1. Имя его типа должно быть приватным для matrix<>::iterator, чтобы внешний код не мог ссылаться на него.

  2. Его построение/копирование/присвоение должно быть частным. 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), будет работать, как и раньше. Однако любая попытка вызвать 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->().

Немедленное решение состоит в том, чтобы квалифицировать оператор прокси-объекта и сделать его применимым только к rvalue. Например, для моего живого примера:

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

Это приведет к тому, что две приведенные выше строки снова выдадут ошибку, в то время как правильное использование итератора все еще работает. К сожалению, это все еще не защищает API от злоупотреблений, в основном из-за доступности кастинга:

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

Что является смертельным ударом для всего начинания. Этому не помешает.

Итак, в заключение, нет никакой защиты от вызова направления operator-> на объекте итератора. В лучшем случае мы можем только сделать API очень сложным для неправильного использования, в то время как правильное использование остается простым.

Если создание row_view копий является дорогостоящим, этого может быть достаточно. Но это вам на рассмотрение.

Еще один момент для рассмотрения, который я не затронул в этом ответе, заключается в том, что прокси-сервер можно использовать для реализации копирования при записи. Но этот класс может быть так же уязвим, как и прокси в моем ответе, если не соблюдать осторожность и не использовать довольно консервативный дизайн.

person StoryTeller - Unslander Monica    schedule 26.06.2017
comment
Просто я правильно понимаю: вызов operator-> без отбрасывания возвращаемого значения приведет к ошибке компилятора, поскольку тип возвращаемого значения (row_proxy) является закрытым? - person Jonas Greitemann; 26.06.2017
comment
@ Джонас - Не только. Приватность возвращаемого типа предотвращает только одну атаку. Скрытие конструктора и оператора присваивания предотвращает его захват с выводом типа auto p = .... - person StoryTeller - Unslander Monica; 26.06.2017
comment
Спасибо за подробное описание и дополнительную информацию. Полагаю, я доволен вашим ответом и, вероятно, скоро соглашусь. Это не совсем то, что я ожидал. Обойти проблему и гарантировать невозможность злонамеренного использования operator-> (с разной степенью успеха) мне даже не приходило в голову. Мой первоначальный вопрос был больше направлен на то, что можно было бы считать идиоматическим. - person Jonas Greitemann; 26.06.2017
comment
Я думаю, что даже в оригинальной версии 1 было бы довольно сложно случайно злоупотребить этой функцией, поскольку operator-> нужно явно вызывать по полному имени, что я не думаю, что кто-то в здравом уме предпочел бы просто использовать operator*. Когда есть злой умысел, я полагаю, всегда есть какой-то способ выкинуть все к черту из чего бы то ни было. - person Jonas Greitemann; 26.06.2017
comment
Я не знал, что можно rvalue-квалифицировать функции-члены. Это довольно круто, хотя я изо всех сил пытаюсь придумать другой сценарий, в котором это было бы полезно. Во всяком случае, очень ценится. - person Jonas Greitemann; 26.06.2017
comment
@Jonas - Ну, если вам нужен идиоматический код, ничто не сравнится с оператором стрелки. Я бы даже сказал, что ожидается, что класс, подобный указателю, будет поддерживать его вместе с operator*. Что касается ценностной категории участников, да, я тоже не использовал ее, пока не ответил на ваш вопрос, так что спасибо за это. Вы также можете квалифицировать l-value. Я, как и вы, в данный момент изо всех сил пытаюсь найти вариант использования. - person StoryTeller - Unslander Monica; 27.06.2017
comment
Если вам интересно, std::optional кажется подходящим примером для определения ссылки: akrzemi1.wordpress.com/2014/06/02/ref-qualifiers/ - person Jonas Greitemann; 27.06.2017
comment
@ Джонас - Аккуратно. Я никогда не вникал в детали реализации optional, но в этом есть смысл. - person StoryTeller - Unslander Monica; 27.06.2017
comment
Я бы, наверное, не стал заморачиваться со всем этим, просто сделал пометку в документации к operator->, что сохранять результат необычными способами не рекомендуется. Как говорится, в C++ часто можно защититься от Мерфи, но не от Макиавелли. - person aschepler; 12.06.2018