自定义容器应该有免费的开始/结束功能吗?
当创建一个自定义的容器类时,它遵循通常的规则(即与STL算法一起工作,使用行为良好的通用代码等),在C ++ 03中,实现迭代器支持和成员开始/结束函数就足够了。
C ++ 11引入了两个新概念 - 基于范围的循环和std :: begin / end。 基于范围的for循环理解成员开始/结束函数,所以任何C ++ 03容器都支持基于范围的开箱即用。 对于算法,推荐的方式(根据Herb Sutter编写的“现代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 :: begin;?
有几种方法,每种方法都有各自的优缺点。 以下三种方法进行成本效益分析。
ADL通过自定义非成员begin()
/ end()
第一种选择是在legacy
名称空间内提供非成员的begin()
和end()
函数模板,以将所需的功能改进到任何可以提供它的类或类模板上,但却有例如错误的命名约定。 调用代码可以依靠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()
任何标准容器和用户类型 .begin()
和end()
类模板 legacy::Container<T>
进行加装(对于范围for循环 !),无需修改源代码 缺点 :需要在许多地方使用声明
std::begin
和std::end
需要被引入到每个显式调用范围中作为C样式数组的回退选项(模板头和潜在的麻烦潜在缺陷) ADL通过自定义非成员adl_begin()
和adl_end()
第二种方法是通过提供非成员函数模板adl_begin()
和adl_end()
将先前解决方案的使用声明封装到单独的adl
名称空间中,然后可以通过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";
}
优点 :完全一致的调用约定
缺点 :有点冗长
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";
}
优点 :几乎一般工作的一致调用约定
.begin()
和.end()
任何标准容器和用户类型 缺点 :一些细节和改造不是通用和维护问题
std::begin()
/ std::end()
比begin()
/ end()
有点冗长 .begin()
和end()
(并且没有源代码!)的类 LegacyContainer
进行改进以适用于工作(也适用于范围for循环 ! - namespace std
成员函数模板begin()
和end()
LegacyContainer<T>
(对于模板可用)的源代码中直接添加成员函数begin()
/ end()
来将其改进到类模板 LegacyContainer<T>
。 namespace std
技巧在这里不起作用,因为函数模板不能部分专用。 要使用什么?
在容器自己的命名空间中通过非成员begin()
/ end()
方法的ADL方法是惯用的C ++ 11方法,尤其是对于需要在旧类和类模板上进行改进的泛型函数。 这与用户提供非成员swap()
函数的习惯是一样的。
对于只使用标准容器或C风格数组的代码,可以在不引入使用声明的情况下调用std::begin()
和std::end()
,而以更多详细调用为代价。 这种方法甚至可以改进,但它需要摆弄namespace std
(用于类类型)或就地源修改(用于类模板)。 它可以完成,但不值得维护麻烦。
在非泛型代码中,在编码时已知问题的容器,人们甚至可以仅依赖于标准容器的ADL,并明确限定std::begin
/ std::end
为C风格的数组。 它失去了一些调用一致性,但节省了使用声明。
上一篇: Should custom containers have free begin/end functions?