Как написать шаблон С++ 11, который может принимать итератор const

Отвечая на этот вопрос на CodeReview, я думал о том, как можно написать функцию шаблона, чтобы указать const -ness содержащегося объекта.

Чтобы быть конкретным, рассмотрим эту шаблонную функцию

#include <iostream>
#include <numeric>
#include <vector>

template <class It>
typename std::iterator_traits<It>::value_type average(It begin, It end) {
    typedef typename std::iterator_traits<It>::value_type real;
    real sum = real();
    unsigned count = 0;
    for ( ; begin != end; ++begin, ++count)
        sum += *begin;
    return sum/count;
}

int main()
{
    std::vector<double> v(1000);
    std::iota(v.begin(), v.end(), 42);
    double avg = average(v.cbegin(), v.cend());
    std::cout << "avg = " << avg << '\n';
}

Он принимает итератор и вычисляет среднее значение на основе содержащихся чисел, но гарантированно не изменять вектор через прошедшие итераторы. Как передать это пользователю шаблона?

Обратите внимание, что объявление этого типа:

template <class It>
typename std::iterator_traits<It>::value_type average(const It begin,
    const It end)

не работает, потому что это не итератор, а то, на что указывает итератор, что const. Должен ли я ждать, пока стандарты будут стандартизированы?

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

Ответы

Ответ 1

template <class ConstIt>

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

Это приводит к вопросу о принудительном исполнении на стороне вызываемого абонента/функции - поэтому он не может притворяться, что использует только итератор для доступа const, а затем модифицирует элементы. Если вас это волнует, вы можете принять параметр, используя какой-то идентификатор, дающий понять, что он не предназначен для использования повсюду в функции, а затем создайте версию const_iterator с более удобным идентификатором. Это может быть сложно, поскольку, как правило, вы не знаете, является ли тип итератора членом контейнера, не говоря уже о том, что это тип контейнера, и имеет ли он const_iterator тоже, поэтому какой-то концепт действительно будет идеальным - пальцы скрещены для С++ 14. В то же время:

  • сообщите своему абоненту тип контейнера,
  • напишите свои собственные черты, ИЛИ
  • напишите простой класс-оболочку, который содержит итератор и гарантирует, что только const доступ к указанным данным ускоряет интерфейс

Этот последний подход к оболочке показан ниже (не все API-интерфейс итератора реализован так, как требуется):

template <typename Iterator>
class const_iterator
{
  public:
    typedef Iterator                                                 iterator_type;
    typedef typename std::iterator_traits<Iterator>::difference_type difference_type;
    // note: trying to add const to ...:reference or ..:pointer doesn't work,
    //       as it like saying T* const rather than T const* aka const T*.
    typedef const typename std::iterator_traits<Iterator>::value_type& reference;
    typedef const typename std::iterator_traits<Iterator>::value_type* pointer;

    const_iterator(const Iterator& i) : i_(i) { }
    reference operator*() const { return *i_; }
    pointer operator->() const { return i_; }    
    bool operator==(const const_iterator& rhs) const { return i_ == rhs.i_; }
    bool operator!=(const const_iterator& rhs) const { return i_ != rhs.i_; }    
    const_iterator& operator++() { ++i_; return *this; }
    const_iterator operator++(int) const { Iterator i = i_; ++i_; return i; }
  private:
    Iterator i_;
};

Использование образца:

template <typename Const_Iterator>
void f(const Const_Iterator& b__, const Const_Iterator& e__)
{
    const_iterator<Const_Iterator> b{b__}, e{e__}; // make a really-const iterator
    // *b = 2;  // if uncommented, compile-time error....
    for ( ; b != e; ++b)
        std::cout << *b << '\n';
}

Посмотрите на ideone.com здесь.

Ответ 2

Вы можете добавить некоторые черты, чтобы увидеть, является ли итератор const_iterator:

template <typename IT>
using is_const_iterator = 
        std::is_const<typename std::remove_reference<typename std::iterator_traits<IT>::reference>::type>;

И затем используйте что-то вроде:

template <typename IT>
typename
std::enable_if<is_const_iterator<IT>::value,
               typename std::iterator_traits<It>::value_type
>::type average(It begin, It end);

Но это позволит избежать использования итератора, которые конвертируются в const_iterator. Поэтому лучше ограничивать итератор, когда const запрещен (как в std::sort)

Ответ 3

Он принимает итератор и вычисляет среднее значение на основе содержащихся чисел, но гарантированно не изменять вектор через прошедшие итераторы. Как передать это пользователю шаблона?

Вы можете использовать SFINAE, чтобы отключить шаблон при передаче не-const-итераторов, но это было бы ненужным ограничением.


Другой способ - принять диапазоны вместо итераторов. Таким образом, вы можете написать:

template <class Range>
typename Range::value_type average(Range const& range);

Пользователь может передавать там контейнер или итератор.

Ответ 4

Вы можете попробовать разыменовать итератор с помощью некоторой функции deref().

template <typename It>
typename ::std::remove_reference<typename ::std::iterator_traits<It>::reference>::type const&
deref(It it)
{
  return *it;
}

Что бы гарантировать, что базовое значение не будет изменено.