Навіщо використовувати функції початку та закінчення, які не є членами, у 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, можливо, якісь розумні виправлення помилок, код реалізації не доведеться взагалі змінювати (крім, можливо, деяких типів).
Карл Кнечтел

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

Використання функцій beginі endвільних додає один шар непрямості. Зазвичай це робиться для забезпечення більшої гнучкості.

У цьому випадку я можу придумати кілька застосувань.

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

Інша справа при спробі використання стандартного алгоритму на невідповідному контейнері (тобто у контейнері відсутній .begin()метод). Якщо припустити, що ви не можете просто виправити контейнер, наступний найкращий варіант - перевантажити beginфункцію. Herb пропонує вам завжди використовувати цю beginфункцію, щоб сприяти рівномірності та послідовності коду. Замість того, щоб пам’ятати, які контейнери підтримують метод, beginа які потрібні функції begin.

Як і в стороні, на наступному C ++ обороти повинні копіювати двійки позначення псевдо-член . Якщо a.foo(b,c,d)не визначено, він замість цього намагається foo(a,b,c,d). Це лише трохи синтаксичного цукру, щоб допомогти нам бідним людям, які віддають перевагу предмету, а не впорядкуванню дієслів.


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

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

Є пропозиції ввести це в C ++, в якому використовується термін Синтаксис виклику уніфікованої функції (UFCS).
підкреслюйте_d

17

Щоб відповісти на ваше запитання, вільні функції за замовчуванням () і кінець () не виконують нічого іншого, як викликати функції контейнера .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); }

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


Приємні приклади. Я б не брав enumбудь-який інший фундаментальний тип за посиланням; їх копіювання буде дешевше, ніж до непрямого.
підкреслюйте_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коли інших альтернатив не знайдено.

Ми можемо з’їсти печиво та мати печиво - тобто мати спосіб забезпечити власну реалізацію begin/ в endтой час як компілятор може повернутися до стандартних.

Деякі примітки:

  • З цієї ж причини існують і інші подібні функції: std::rbegin/ rend, std::sizeі std::data.

  • Як згадується в інших відповідях, std::версії мають перевантаження для голих масивів. Це корисно, але це просто окремий випадок того, що я описав вище.

  • Використання std::beginта друзів є особливо хорошою ідеєю при написанні коду шаблону, оскільки це робить ці шаблони більш загальними. Для інших шаблонів ви можете також добре використовувати методи, коли це застосовується.

PS Я знаю, що цій посаді майже 7 років. Я натрапив на це, тому що хотів відповісти на запитання, яке було позначене як дублікат, і виявив, що жодна відповідь тут не згадує ADL.


Хороша відповідь, особливо відверто пояснюючи ADL, а не залишаючи це уявою, як всі інші - навіть коли вони демонстрували це в дії!
підкреслюйте_d

5

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

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


3
У C ++ 03 ви можете просто використовувати boost::begin()/ end(), тому справжньої несумісності немає:
Marc Mutz - mmutz

1
@ MarcMutz-mmutz Добре, що залежність від підвищення не завжди є варіантом (і є надмірним набором, якщо використовується лише для begin/end). Тому я вважаю, що несумісність із чистим C ++ 03 теж. Але, як було сказано, це досить невелика (і дедалі менша) несумісність, оскільки C ++ 11 (принаймні, begin/endзокрема) набуває все більше і більше прийняття.
Крістіан Рау

0

Зрештою, користь полягає в коді, який узагальнений таким чином, що це контейнерний агностик. Він може працювати на astd::vector , масиві або діапазоні без змін у самому коді.

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

Дивіться тут для більш детальної інформації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.