Should custom containers have free begin/end functions?
When creating a custom container class that plays by the usual rules (ie works with STL algorithms, works with well-behaved generic code, etc.), in C++03 it was sufficient to implement iterator support and member begin/end functions.
C++11 introduces two new concepts - range-based for loop and std::begin/end. Range-based for loop understands member begin/end functions, so any C++03 containers support range-based for out of the box. For algorithms the recommended way (according to 'Writing modern C++ code' by Herb Sutter) is to use std::begin instead of member function.
However, at this point I have to ask - is the recommended way to call a fully qualified begin() function (ie std::begin(c)) or to rely on ADL and call begin(c)?
ADL seems useless in this particular case - since std::begin(c) delegates to c.begin() if possible, usual ADL benefits do not seem to apply. And if everybody starts to rely on ADL, all custom containers have to implement extra begin()/end() free functions in their requisite namespaces. However, several sources seem to imply that unqualified calls to begin/end are the recommended way (ie https://svn.boost.org/trac/boost/ticket/6357).
So what is the C++11 way? Should container library authors write extra begin/end functions for their classes to support unqualified begin/end calls in absence of using namespace std; or using std::begin;?
There are several approaches, each with their own pros and cons. Below three approaches with a cost-benefit analysis.
ADL through custom non-member begin()
/ end()
The first alternative provides non-member begin()
and end()
function templates inside a legacy
namespace to retrofit the required functionality onto any class or class template that can provide it, but has eg the wrong naming conventions. Calling code can then rely on ADL to find these new functions. Example code (based on comments by @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 : consistent and terse calling convention that works completely generically
.begin()
and .end()
legacy::Container<T>
that does not have member .begin()
and end()
without requiring source code modifications Cons : requires using-declarations in many places
std::begin
and std::end
are required to have been brought into every explicit calling scope as fall back options for C-style arrays (potential pitfall for template headers and general nuisance) ADL through custom non-member adl_begin()
and adl_end()
A second alternative is to encapsulate the using-declarations of the previous solution into a separate adl
namespace by providing non-member function templates adl_begin()
and adl_end()
, which can then also be found through ADL. Example code (based on comments by @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 : consistent calling convention that works completely generically
Cons : a little verbose
adl_begin()
/ adl_end()
is not as terse as begin()
/ end()
std::begin
/ std::end
NOTE : Not sure if this really improves upon the previous approach.
Explicitly qualifying std::begin()
or std::end()
everywhere
Once the verbosity of begin()
/ end()
has been given up anyway, why not go back to the qualified calls of std::begin()
/ std::end()
? Example code:
// 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 : consistent calling convention that works almost generically
.begin()
and .end()
Cons : a little verbose and retrofitting is not generic and a maintainence problem
std::begin()
/ std::end()
is a little more verbose than begin()
/ end()
LegacyContainer
that does not have member .begin()
and end()
(and for which there is no source code!) by providing explicit specializations of the non-member function templates begin()
and end()
in namespace std
LegacyContainer<T>
by directly adding member functions begin()
/ end()
inside the source code of LegacyContainer<T>
(which for templates is available). The namespace std
trick does not work here because function templates cannot be partially specialized. What to use?
The ADL approach through non-member begin()
/ end()
in aa container's own namespace is the idiomatic C++11 approach, especially for generic functions that require retrofitting on legacy classes and class templates. It is the same idiom as for user-providing non-member swap()
functions.
For code that only uses Standard Containers or C-style arrays, std::begin()
and std::end()
could be called everywhere without introducing using-declarations, at the expense of more verbose calls. This approach can even be retrofitted but it requires fiddling with namespace std
(for class types) or in-place source modifcations (for class templates). It can be done, but is not worth the maintainence trouble.
In non-generic code, where the container in question is known at coding-time, one could even rely on ADL for Standard Containers only, and explicitly qualify std::begin
/ std::end
for C-style arrays. It loses some calling consistency but saves on using-declarations.
上一篇: 用容器的保护继承来推动foreach
下一篇: 自定义容器应该有免费的开始/结束功能吗?