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

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

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

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

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

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

55

Решение

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

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";
}

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

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

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

  • 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";
}

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

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

Cons: немного многословно

  • 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";
}

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

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

Cons: немного многословно и дооснащение не является общим и проблема сопровождения

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

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

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

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

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

34

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]