Подтвердить что ты не робот

Должны ли пользовательские контейнеры иметь бесплатные функции начала/конца?

При создании пользовательского класса контейнера, который воспроизводится по обычным правилам (т.е. работает с алгоритмами STL, работает с хорошо выполненным общим кодом и т.д.), в С++ 03 было достаточно реализовать поддержку итератора и начать/конечных функций.

В С++ 11 представлены два новых понятия - диапазон для циклов и std:: begin/end. Диапазон, основанный на цикле, понимает функции начала/конца элемента, поэтому любые контейнеры С++ 03 поддерживают диапазон из-за коробки. Для алгоритмов рекомендуемый способ (согласно "Написание современного кода на С++" от Herb Sutter) заключается в использовании std:: begin вместо функции-члена.

Однако на данном этапе я должен спросить - это рекомендуемый способ вызвать полноценную функцию begin() (т.е. std:: begin (c)) или полагаться на ADL и начать вызов (c)?

ADL кажется бесполезным в данном конкретном случае - поскольку std:: begin (c) делегирует c.begin(), если это возможно, обычные преимущества ADL, похоже, не применяются. И если все начинают полагаться на ADL, все пользовательские контейнеры должны реализовывать дополнительные функции begin()/end() в своих требуемых пространствах имен. Тем не менее, несколько источников, по-видимому, подразумевают, что рекомендуются неквалифицированные призывы к началу/концу (т.е. https://svn.boost.org/trac/boost/ticket/6357).

Итак, что такое С++ 11? Если авторы контейнерной библиотеки пишут дополнительные функции begin/end для своих классов для поддержки неквалифицированных вызовов начала и конца в отсутствие использования пространства имен std; или используя std:: begin;?

4b9b3361

Ответ 1

Существует несколько подходов, каждый из которых имеет свои плюсы и минусы. Ниже три подхода с анализом затрат и результатов.

ADL через пользовательский не-член begin()/end()

Первая альтернатива предоставляет не-членные шаблоны функций begin() и end() внутри пространства имен legacy для модификации требуемой функциональности на любом классе или шаблоне класса, который может ее предоставить, но имеет, например, неправильные соглашения об именах. Затем код вызова может полагаться на ADL, чтобы найти эти новые функции. Пример кода (на основе комментариев от @Xeo):

// LegacyContainerBeginEnd.h
namespace legacy {

// retro-fitting begin() / end() interface on legacy 
// Container class template with incompatible names         
template<class C> 
auto begin(Container& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similarly for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // bring into scope to fall back on for types without their own namespace non-member begin()/end()
    using std::begin;
    using std::end;

    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(begin(c), end(c), std::ostream_iterator<decltype(*begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Преимущества: согласованное и краткое соглашение о вызове, которое работает полностью в целом

  • работает для любого стандартного контейнера и пользовательских типов, которые определяют члены .begin() и .end()
  • работает для массивов C-стиля
  • может быть модифицирован для работы (также для диапазонов циклов!) для любого шаблона класса legacy::Container<T>, у которого нет элементов .begin() и end() без изменения исходных кодов.

Минусы: требуется использование объявлений во многих местах

  • std::begin и std::end должны быть введены в каждую явную область вызова в качестве параметров возврата для массивов в стиле C (потенциальная ошибка для заголовков шаблонов и общая неприятность).

ADL через пользовательский не-член adl_begin() и adl_end()

Второй вариант заключается в том, чтобы инкапсулировать объявления-объявления предыдущего решения в отдельное пространство имен adl, предоставляя не-членные шаблоны функций adl_begin() и adl_end(), которые также могут быть найдены через ADL. Пример кода (на основе комментариев @Yakk):

// LegacyContainerBeginEnd.h 
// as before...

// ADLBeginEnd.h
namespace adl {

using std::begin; // <-- here, because otherwise decltype() will not find it 

template<class C> 
auto adl_begin(C && c) -> decltype(begin(std::forward<C>(c)))
{ 
    // using std::begin; // in C++14 this might work because decltype() is no longer needed
    return begin(std::forward<C>(c)); // try to find non-member, fall back on std::
}

// similary for cbegin(), end(), cend(), etc.

} // namespace adl

using adl::adl_begin; // will be visible in any compilation unit that includes this header

// print.h
# include "ADLBeginEnd.h" // brings adl_begin() and adl_end() into scope

template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(adl_begin(c), adl_end(c), std::ostream_iterator<decltype(*adl_begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    // does not need adl_begin() / adl_end(), but continues to work
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Преимущества: согласованное соглашение о вызове, которое работает полностью в целом

  • те же плюсы, что и для предложения @Xeo +
  • повторяющиеся объявления использования были инкапсулированы (DRY)

Минусы: немного подробный

  • adl_begin()/adl_end() не является столь же кратким, как begin()/end()
  • он, возможно, также не является идиоматическим (хотя и явным)
  • ожидающий вывод типа С++ 14, также будет загрязнять пространство имен с помощью std::begin/std::end

ПРИМЕЧАНИЕ. Не уверен, что это действительно улучшает предыдущий подход.

Явная квалификация std::begin() или std::end() везде

Как только дословность begin()/end() была отклонена, почему бы не вернуться к квалифицированным вызовам std::begin()/std::end()? Пример кода:

// LegacyIntContainerBeginEnd.h
namespace std {

// retro-fitting begin() / end() interface on legacy IntContainer class 
// with incompatible names         
template<> 
auto begin(legacy::IntContainer& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similary for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace std

// LegacyContainer.h
namespace legacy {

template<class T>
class Container
{
public:
    // YES, DOCUMENT REALLY WELL THAT THE EXISTING CODE IS BEING MODIFIED
    auto begin() -> decltype(legacy_begin()) { return legacy_begin(); }
    auto end() -> decltype(legacy_end()) { return legacy_end(); }

    // rest of existing interface
};

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays as well as 
    // legacy::IntContainer and legacy::Container<T>
    std::copy(std::begin(c), std::end(c), std::ostream_iterator<decltype(*std::begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and
    // legacy::IntContainer and legacy::Container<T>
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Преимущества: согласованное соглашение о вызове, которое работает почти в целом

  • работает для любого стандартного контейнера и пользовательских типов, которые определяют члены .begin() и .end()
  • работает для массивов C-стиля

Минусы: немного подробный и дооснащение не является общим и проблемой сохранения

  • std::begin()/std::end() является немного более подробным, чем begin()/end()
  • может быть обновлен только для работы (также для диапазонов циклов!) для любого класса LegacyContainer, у которого нет элемента .begin() и end() (и для которых нет исходного кода!), предоставляя явные специализации шаблонов функций, не являющихся членами begin() и end() в namespace std
  • можно обновить только на шаблонах классов LegacyContainer<T>, добавив в него исходные тексты LegacyContainer<T> (которые доступны для шаблонов) функции-члены begin()/end(). Тройка namespace std здесь не работает, потому что шаблоны функций не могут быть частично специализированы.

Что использовать?

Подход ADL через не-член begin()/end() в собственном пространстве имен контейнера является идиоматическим подходом С++ 11, особенно для универсальных функций, которые требуют дооснащения устаревших классов и шаблонов классов. Это та же самая идиома, что и для пользовательских функций, не являющихся членами swap().

Для кода, который использует только стандартные контейнеры или массивы C-стиля, std::begin() и std::end() можно было бы вызывать везде, не вводя объявления-объявления, за счет более подробных вызовов. Этот подход можно даже модернизировать, но для этого требуется задействовать namespace std (для типов классов) или исходных модификаций исходного кода (для шаблонов классов). Это можно сделать, но не стоит проблем с сохранением.

В неродном коде, где данный контейнер известен во время кодирования, можно даже полагаться только на ADL для стандартных контейнеров и явно квалифицировать std::begin/std::end для массивов в стиле C. Он теряет согласованность вызовов, но экономит на использовании объявлений.