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


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

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

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

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

Так что же такое C++11 путь? Должны ли авторы библиотеки контейнеров писать дополнительные функции begin/end для своих классов для поддержки неквалифицированных вызовов begin/end в отсутствие использования пространства имен std; или с помощью std::begin;?

1 55

1 ответ:

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

АДЛ через пользовательские не-членов 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";
}

плюсы:последовательное и лаконичное соглашение о вызовах, которое работает полностью обобщенно

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

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

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

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

второй альтернативой является инкапсуляция using-объявления предыдущего решения в отдельный 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 предложение +
  • повторное использование-объявления были инкапсулированы (сухой)

минусы: немного многословный

  • adl_begin()/adl_end() не так лаконично, как begin()/end()
  • возможно, это также не так идиоматично (хотя и явно)
  • ожидание вычета типа возврата C++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()
  • может быть переоборудован только для работы (также для и end() (и для которого нет исходного кода!) путем предоставления явных специализаций шаблонов функций, не являющихся членами begin() и end() на namespace std
  • может быть переоборудован только на шаблоны классLegacyContainer<T> путем непосредственного добавления функции-члена begin()/end() внутри исходного кода LegacyContainer<T> (который для шаблоны предоставляются). Элемент namespace std трюк здесь не работает, потому что шаблоны функций не могут быть частично специализированный. 

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

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

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

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