Зачем использовать начальные и конечные функции не-членов в С++ 11?

Каждый стандартный контейнер имеет метод begin и end для возврата итераторов для этого контейнера. Однако С++ 11, по-видимому, ввел бесплатные функции, называемые std::begin и std::end, которые вызывают функции-члены begin и end. Итак, вместо написания

auto i = v.begin();
auto e = v.end();

вы пишете

using std::begin;
using std::end;
auto i = begin(v);
auto e = end(v);

В своем выступлении Написание Modern С++, Herb Sutter говорит, что вы всегда должны использовать бесплатные функции сейчас, когда вы хотите, чтобы начальный или конечный итератор для контейнера. Тем не менее, он не задумывается о том, почему вы хотите. Глядя на код, он сохраняет вас всем одним персонажем. Итак, что касается стандартных контейнеров, бесплатные функции кажутся совершенно бесполезными. Трава Саттер указала, что есть преимущества для нестандартных контейнеров, но опять же он не вдавался в подробности.

Итак, вопрос в том, что именно делают версии бесплатных функций std::begin и std::end за пределами вызова их соответствующих версий функций-членов и почему вы хотите их использовать?

Ответ 1

Как вы называете .begin() и .end() в C-массиве?

Свободные функции позволяют использовать более общее программирование, потому что они могут быть добавлены впоследствии, на структуру данных, которую вы не можете изменить.

Ответ 2

Рассмотрим случай, когда у вас есть библиотека, содержащая класс:

class SpecialArray;

он имеет 2 метода:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

чтобы перебрать значения, которые вам нужно наследовать от этого класса, и определить методы begin() и end() для случаев, когда

auto i = v.begin();
auto e = v.end();

Но если вы всегда используете

auto i = begin(v);
auto e = end(v);

вы можете сделать это:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

где SpecialArrayIterator есть что-то вроде:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

теперь i и e могут быть юридически использованы для итерации и доступа к значениям SpecialArray

Ответ 3

Использование свободных функций begin и end добавляет один слой косвенности. Обычно это делается для большей гибкости.

В этом случае я могу придумать несколько вариантов использования.

Наиболее очевидное использование - для C-массивов (не c-указателей).

Другой - при попытке использовать стандартный алгоритм на несоответствующем контейнере (т.е. в контейнере отсутствует метод .begin()). Предполагая, что вы не можете просто исправить контейнер, следующим лучшим вариантом является перегрузка функции begin. Трава предполагает, что вы всегда используете функцию begin для обеспечения единообразия и согласованности кода. Вместо того, чтобы помнить, какие методы поддержки контейнеров begin и которым нужна функция begin.

В стороне, следующий С++ rev должен скопировать D псевдо-членную нотацию. Если a.foo(b,c,d) не определено, он вместо этого пытается foo(a,b,c,d). Это всего лишь небольшой синтаксический сахар, который поможет нам бедным людям, которые предпочитают подчинение, а затем упорядочивание глаголов.

Ответ 4

Чтобы ответить на ваш вопрос, свободные функции begin() и end() по умолчанию не более чем вызовут функции .begin() и .end() в контейнере. Из <iterator>, автоматически включаемого при использовании любого из стандартных контейнеров, таких как <vector>, <list> и т.д., Вы получаете:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

Вторая часть вашего вопроса заключается в том, почему предпочитают бесплатные функции, если все, что они делают, все равно вызывает функции-члены. Это действительно зависит от того, какой объект v находится в вашем примере кода. Если тип v является стандартным типом контейнера, например vector<T> v;, то не имеет значения, используете ли вы свободные или членские функции, они делают то же самое. Если ваш объект v более общий, как в следующем коде:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Затем использование функций-членов разбивает ваш код на массивы T = C, строки C, перечисления и т.д. Используя не-членские функции, вы рекламируете более общий интерфейс, который люди могут легко расширить. Используя интерфейс свободной функции:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Теперь код работает с массивами T = C и строками C. Теперь записываем небольшой код адаптера:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Мы можем получить ваш код для совместимости с итерабельными перечислениями. Я полагаю, что основное внимание Herb заключается в том, что использование бесплатных функций так же просто, как использование функций-членов, и дает вам обратную совместимость кода с типами последовательностей C и прямой совместимостью с типами не-stl-последовательностей (и типов будущего-stl!), с низкой стоимостью для других разработчиков.

Ответ 5

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

Но, конечно, это всегда нужно взвешивать правильно, и абстракция тоже не хороша. Хотя использование бесплатных функций не является чрезмерной абстракцией, тем не менее она нарушает совместимость с кодом С++ 03, который в этот молодой возраст С++ 11 может все еще быть проблемой для вас.