Зачем использовать функции, не являющиеся членами начала и конца в C ++ 11? - PullRequest
182 голосов
/ 29 сентября 2011

В каждом стандартном контейнере есть метод 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);

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

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

Ответы [ 6 ]

153 голосов
/ 29 сентября 2011

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

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

34 голосов
/ 29 сентября 2011

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

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

31 голосов
/ 29 сентября 2011

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

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

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

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

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

17 голосов
/ 11 февраля 2013

Чтобы ответить на ваш вопрос, бесплатные функции 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!), с низкой стоимостью для других разработчиков.

5 голосов
/ 14 июня 2018

Одним из преимуществ 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, когда других альтернатив не найдено.

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

Некоторые примечания:

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

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

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

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

5 голосов
/ 30 сентября 2011

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

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

...