При создании пользовательского класса-контейнера, который воспроизводится по обычным правилам (то есть работает с алгоритмами 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 ;?
Есть несколько подходов, каждый со своими плюсами и минусами. Ниже три подхода с анализом затрат и выгод.
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()
legacy::Container<T>
у которого нет члена .begin()
а также end()
без необходимости изменения исходного кодаCons: требует использования объявлений во многих местах
std::begin
а также std::end
должны быть включены в каждую явную область вызова как запасные варианты для массивов в стиле C (потенциальная ловушка для заголовков шаблонов и общая неприятность)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: согласованное соглашение о вызовах, которое работает полностью в общем
Cons: немного многословно
adl_begin()
/ adl_end()
не так кратко, как begin()
/ end()
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()
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 Он теряет некоторую согласованность вызовов, но экономит на использовании-объявлений.
Других решений пока нет …