Неоднозначность при использовании reverse_iterator и const_reverse_iterator

Итераторы reverse_iterator и const_reverse_iterator предназначены для перечисления элементов контейнера в обратном порядке (от конца к началу). Об этом написано в любой более или менее приличной книге по С++. Но есть ряд моментов, о которых там умалчивают. Один из них связан с неоднозначностью, возникающей при работе с обратными итераторами.

Чтобы понять суть проблемы рассмотрим класс:

#include <vector>

template <typename T> class MyClass
{
public:
  typedef typename std::vector<T>::iterator Iterator;
  typedef typename std::vector<T>::const_iterator ConstIterator;
  typedef typename std::vector<T>::reverse_iterator ReverseIterator;
  typedef typename std::vector<T>::const_reverse_iterator ConstReverseIterator;

  Iterator begin() {return data_.begin();};
  Iterator end()   {return data_.end();};

  ConstIterator begin() const {return data_.begin();};
  ConstIterator end() const {return data_.end();};

  ReverseIterator rbegin() {return data_.rbegin();};
  ReverseIterator rend() {return data_.rend();};

  ConstReverseIterator rbegin() const {return data_.rbegin();};
  ConstReverseIterator rend()   const {return data_.rend();};

  T& at(unsigned int i) {return data_.at(i);};
  const T& at(unsigned int i) const {return data_.at(i);};
  //////////
  Iterator find(const T &symbol, Iterator i)
  {	
    while(i != end())
    {
      if(*i == symbol)
        break;

      ++i;	
    }
  
    return i;
  };
  //////////
  Iterator find(const T &symbol) { return find(symbol, begin()); };
  //////////
  ConstIterator find(const T &symbol, ConstIterator i) const
  {	
    while(i != end())
    {
      if(*i == symbol)
        break;

      ++i;	
    }
  
    return i;
  };
  //////////
  ConstIterator find(const T &symbol) const
  {
    return find(symbol, begin());
  };
  //////////
  ReverseIterator rfind(const T &symbol, ReverseIterator i)
  {	
    while(i != rend())
    {
      if(*i == symbol)
        break;

      ++i;	
    }
  
    return i;
  };
  //////////
  ReverseIterator rfind(const T &symbol)
  {
    return rfind(symbol, rbegin());
  };
  //////////
  ConstReverseIterator rfind(const T &symbol,
                             ConstReverseIterator i) const
  {	
    while(i != rend())
    {
      if(*i == symbol)
        break;

      ++i;	
    }
  
    return i;
  };
  //////////
  ConstReverseIterator rfind(const T &symbol) const
  {
    return rfind(symbol, rbegin());
  };
  //////////
  friend MyClass<T>& operator << (MyClass<T> &left, const T &right)
  {
    left.data_.push_back(right);
    
    return left;
  };

 
  MyClass() {};
  ~MyClass() {};

private:
  std::vector<T> data_;	
};

Он выглядит сложным, но на деле представляет собой лишь надстройку над стандартным контейнером std::vector. В нем переопределяются типы для 4 итераторов: iterator, const_iterator, reverse_iterator и const_reverse_iterator. Для каждого из них реализуется метод поиска find (для обычных итераторов) или rfind (для обратных итераторов). Вот их прототипы:

Iterator find(const T&, Iterator);
Iterator find(const T&);

ConstIterator find(const T&, ConstIterator) const;
ConstIterator find(const T&) const;

ReverseIterator rfind(const T&, ReverseIterator);
ReverseIterator rfind(const T&);

ConstReverseIterator find(const T&, ConstReverseIterator) const;
ConstReverseIterator find(const T&) const;

Реализация этих методов нам сейчас не интересна. Рассмотрим пример их использования:

MyClass<int> values;
values << 1 << 4 << 7 << 8 << 9 << 6 << 7;

int val = 7;

MyClass<int>::Iterator i = values.find(val);
int count = 0;

while (i != values.end())
{
  ++count;
  ++i;
  i = values.find(val, i);
}

Здесь мы подсчитываем сколько раз число 7 входит в состав массива values. Ничего сложного. Данный пример исправно работает, в переменной count, как мы того и ожидаем, окажется число 2. Все хорошо.

Изменим тип итератора с Iterator на ConstIterator. Такая замена ни на что не повлияла. Наш пример по-прежнему нормально работает.

Теперь попробуем с обратным итератором:

MyClass<int> values;
values << 1 << 4 << 7 << 8 << 9 << 6 << 7;

int val = 7;

MyClass<int>::ReverseIterator ri = values.rfind(val);
int count = 0;

while (ri != values.rend())
{
  ++count;
  ++ri;
  ri = values.rfind(val, ri);
}

Все нормально работает. Изменим теперь тип итератора с ReverseIterator на ConstReverseIterator. Попытавшись скомпилировать измененный пример, мы получим длинное сообщение:

Интересно: компилятор g++ (проверялось на версиях 4.8.1 и 5.4.0) выдает предупреждение (warning) вне зависимости от ключа -Wall. При этом исполняемый модуль собирается и нормально работает. Компилятор, входящий в состав C++ Builder 6 выдает 2 ошибки. Первая аналогична предупреждению g++ (код E2015), вторая является следствием первой. Компилятор, входящий в состав Microsoft Visual Studio Community 2017 также выдает сообщение об ошибке неоднозначности (код E0308). Это позволяет отмести версию об ошибке в используемом компиляторе.

Можно конечно отмахнуться от этого предупреждения. Ведь в итоге всё компилируется. Но это не наш путь. Да, компилируется но только в g++ (из тех, что я проверял). Уже одно это намертво привязывает весь наш проект к одному компилятору. Если кто-то решит попробовать собрать наш проект в другом компиляторе, то у него это не получится. Это очень плохая практика и ее следует избегать.

Если убрать все лишнее, то текст ошибки (предупреждения) будет следующим:

warning: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second

Или в моем переводе:

Внимание: стандарт С++ говорит, что существует неопределенность даже несмотря на то, что первый вариант лучше второго

Из дальнейших пояснений к этому сообщению становится ясно, что компилятор не может выбрать из двух функций:

ReverseIterator rfind(const T&, ReverseIterator);

ConstReverseIterator rfind(const T&, ConstReverseIterator) const;

И это несколько странно: мало того, что вторые параметры имеют разный тип, так и только один из этих методов имеет атрибут const.

Обратите внимание: методы find для обычных (не реверсных) итераторов отличаются друг от друга тем же (типом второго параметра и атрибутом const), но там такой ошибки не возникает. В случае работы с обычными итераторами компилятор «понимает» какой метод нужно вызывать. А здесь — нет. Более того он «понимает» что нужно вызывать даже при работе с обычным обратным итератором (reverse_iterator). Проблема проявляется только при работе с const_reverse_iterator.

Как водится в таких случаях, я полез в Интернет, надеясь найти там объяснение. С ошибкой E2015 в С++ Builder сталкивались многие. И обычно она вызывалась конфликтом имен, когда, например, создавались два класса с одним и тем же именем. Обычно она решалась переименовываем типов, введением и уточнением пространств имён. Но это не наш вариант. В нашем случае конфликтующие методы должны находиться в одном пространстве имен. Да и потом, это не объясняет, почему конфликт имен происходит только при работе с const_reverse_iterator?

К сожалению, ответа на свой вопрос я так и не нашел. Поэтому, уважаемые читатели моего блога, если кто из вас знает ответ на этот вопрос, напишите мне (в комментариях к этой статье или через форму обратной связи), буду очень признателен.

Хорошо. Но как нам быть с ошибкой? Совсем отказаться от const_reverse_iterator? Необязательно, ошибку можно обойти вот так:

void foo(const MyClass<int> &values)
{
  int val = 7;
  MyClass<int>::ConstReverseIterator ri = values.rfind(val);
  int count = 0;
  
  while (ri != values.rend())
  {
    ++count;
    ++ri;
    ri = values.rfind(val, ri);
  }
  printf("%u\r\n", count);  	 
}

Здесь мы получаем объект по константной ссылке, благодаря чему компилятор «знает», что вызывать можно только константный метод. Поэтому неоднозначности не возникает. Стоит нам убрать квалификатор const, как мы сразу получим нашу ошибку.

Таким образом, будьте предельно осторожными при работе с константными обратными итераторами.

P.S.  По ссылке ниже вы можете бесплатно скачать архив со всеми файла из данного примера.

 

Добавить комментарий

Ваш адрес email не будет опубликован.