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


197

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

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

ты бы написал

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

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

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


29
Это на один символ меньше, сохраните эти точки для своих детей: xkcd.com/297
HostileFork говорит, что не доверяйте SE

Я как-то ненавижу их использовать, потому что мне приходится std::все время повторять .
Майкл Чурдакис

Ответы:


162

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

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


7
@JonathanMDavis: вы можете использовать endстатически объявленные массивы ( int foo[5]), используя приемы программирования шаблонов. Как только он распался на указатель, вам, конечно, не повезло.
Матье М.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Хью

6
@JonathanMDavis: Как другие указали, что, конечно , можно получить beginи endв массиве C до тех пор , пока вы не уже распались его к указателю сами - @Huw выговаривать это. Что касается того, почему вы хотите: представьте, что вы реорганизовали код, который использовал массив, чтобы использовать вектор (или наоборот, по любой причине). Если вы использовали beginи end, и, возможно, некоторую умную дешифровку, код реализации вообще не должен будет изменяться (за исключением, возможно, некоторых из typedefs).
Карл Кнехтель

31
@JonathanMDavis: массивы не указатели. И для всех: ради прекращения этой постоянно растущей путаницы, прекратите называть (некоторые) указатели «разложившимися массивами». В языке нет такой терминологии, и ее на самом деле нет. Указатели - это указатели, массивы - это массивы. Массивы могут быть неявно преобразованы в указатель на их первый элемент, но он все еще является обычным старым указателем, без различия с другими. Конечно, вы не можете получить «конец» указателя, дело закрыто.
GManNickG

5
Ну, кроме массивов, есть большое количество API, которые представляют аспекты, похожие на контейнеры. Очевидно, что вы не можете изменить сторонний API, но вы можете легко написать эти автономные функции начала / конца.
edA-qa mort-ora-y

35

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

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


8
Это не должно включать в себя template<>линии. Вы объявляете перегрузку новой функции, не специализируя шаблон.
Дэвид Стоун

33

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

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

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

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

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


5
Запись псевдо-члена выглядит как методы расширения C # /. Net . Они полезны для различных ситуаций, хотя, как и все функции, могут быть подвержены «злоупотреблениям».
Гарет Уилсон

5
Псевдо-член нотации - благо для кодирования с Intellisense; удар "а" показывает соответствующие глаголы, освобождая умственные способности от запоминания списков и помогая находить соответствующие функции API, может помочь предотвратить дублирование функций без необходимости вставлять функции, не являющиеся членами, в классы.
Мэтт Кертис

Есть предложения перенести это в C ++, в котором используется термин Unified Function Call Syntax (UFCS).
underscore_d

17

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


Хорошие примеры. Я бы не стал использовать enumкакой-либо другой фундаментальный тип по ссылке; они будут дешевле копировать, чем косвенные.
underscore_d

6

Одним из преимуществ , 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::beginand friends особенно полезно при написании кода шаблона, потому что это делает эти шаблоны более общими. Для не шаблонов вы можете также использовать методы, когда это применимо.

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


Хороший ответ, в частности, объясняющий ADL открыто, вместо того, чтобы оставить это воображению, как все остальные - даже когда они демонстрировали это в действии!
underscore_d

5

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

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


3
В C ++ 03 вы можете просто использовать boost::begin()/ end(), так что нет никакой реальной несовместимости :)
Марк Мутц - mmutz

1
@ MarcMutz-mmutz Что ж, зависимость от наддува не всегда возможна (и является излишним, если используется только для begin/end). Так что я бы посчитал это несовместимостью с чистым C ++ 03 тоже. Но, как уже говорилось, это довольно маленькая (и становится все меньше) несовместимость, так как C ++ 11 (по крайней мере, begin/endв частности) получает все большее и большее признание, в любом случае.
Кристиан Рау

0

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

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

Смотрите здесь для более подробной информации.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.