Зачем использовать функции, не являющиеся членами начала и конца, в C++11?

Каждый стандартный контейнер имеет begin а также end метод для возврата итераторов для этого контейнера. Тем не менее, C++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);

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

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

6 ответов

Решение

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

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

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

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

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

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

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

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

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

Чтобы ответить на ваш вопрос, бесплатные функции 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; тогда не имеет значения, используете ли вы функции free или member, они делают то же самое. Если ваш объект 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); }

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

Одно преимущество std::begin а также std::end заключается в том, что они служат точками расширения для реализации стандартного интерфейса для внешних классов.

Если вы хотите использовать CustomContainer класс с диапазоном для цикла или функции шаблона, который ожидает .begin() а также .end() методы, вам, очевидно, придется реализовать эти методы.

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

Это не всегда возможно, например, при использовании внешней библиотеки, особенно коммерческой и закрытой.

В таких ситуациях std::begin а также std::end пригодится, так как можно предоставить API-интерфейс итератора без изменения самого класса, а с перегрузкой свободных функций.

Пример: предположим, что вы хотите реализовать count_if функция, которая принимает контейнер вместо пары итераторов. Такой код может выглядеть так:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

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

Теперь в C++ есть механизм, называемый Argument Dependent Lookup (ADL), что делает такой подход еще более гибким.

Короче говоря, ADL означает, что когда компилятор разрешает неквалифицированную функцию (то есть функцию без пространства имен, например, begin вместо std::begin), он также будет рассматривать функции, объявленные в пространствах имен своих аргументов. Например:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

В этом случае не имеет значения, что квалифицированные имена some_lib::begin а также some_lib::end - поскольку CustomContainer в some_lib:: также компилятор будет использовать эти перегрузки в count_if,

Это также причина того, что using std::begin; а также using std::end; в count_if, Это позволяет нам использовать неквалифицированную begin а также end следовательно, позволяя для ADL и позволяя компилятору выбирать std::begin а также std::end когда других альтернатив не найдено.

Мы можем съесть куки и иметь куки - то есть способ обеспечить пользовательскую реализацию begin / end в то время как компилятор может вернуться к стандартным.

Некоторые заметки:

  • По той же причине есть и другие похожие функции: std::rbegin / rend, std::size а также std::data,

  • Как говорится в других ответах, std:: версии имеют перегрузки для голых массивов. Это полезно, но это просто частный случай того, что я описал выше.

  • С помощью std::begin и друзья особенно хорошая идея при написании кода шаблона, потому что это делает эти шаблоны более общими. Для не шаблонов вы можете также использовать методы, когда это применимо.

PS Я в курсе, что этому посту почти 7 лет. Я столкнулся с этим, потому что я хотел ответить на вопрос, который был отмечен как дубликат, и обнаружил, что ни один ответ здесь не упоминает ADL.

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

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

В конечном итоге преимущество заключается в коде, который обобщен так, что он не зависит от контейнера. Он может работать наstd::vector, массив или диапазон без изменений самого кода.

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

Подробнее см. Здесь.

Другие вопросы по тегам