有几种方法可供选择,每种方法都有其优缺点。以下是三种方法及其成本效益分析。
通过自定义非成员 begin()
/ end()
实现 ADL
第一种替代方案提供了非成员 begin()
和 end()
函数模板,放置在一个名为 legacy
的命名空间中,可以将所需功能添加到任何可以提供它的类或类模板中(例如具有错误命名约定的类)。然后,调用代码可以依靠 ADL 找到这些新函数。示例代码(基于 @Xeo 的评论):
namespace legacy {
template<class C>
auto begin(Container& c) -> decltype(c.legacy_begin())
{
return c.legacy_begin();
}
}
template<class C>
void print(C const& c)
{
using std::begin;
using std::end;
std::copy(begin(c), end(c), std::ostream_iterator<decltype(*begin(c))>(std::cout, " ")); std::cout << "\n";
for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
优点:调用约定一致简洁,完全通用
- 适用于任何标准容器和定义了成员
.begin()
和.end()
的用户类型
- 适用于C语言风格的数组
- 可以通过后期修改来适应任何没有成员
.begin()
和.end()
的类模板legacy::Container<T>
(包括范围for循环!)而不需要修改源代码
缺点:需要在多个地方使用using-declarations。
- 对于C语言风格的数组,必须将
std::begin
和std::end
作为备选项带入每个显式调用范围内(对于模板头文件可能导致潜在的问题和麻烦)
通过自定义非成员函数adl_begin()
和 adl_end()
实现ADL
第二种替代方案是将前一个解决方案中using声明封装到一个单独的adl
命名空间中,通过提供非成员函数模板adl_begin()
和adl_end()
,也可以通过ADL查找。示例代码(基于@Yakk的评论):
namespace adl {
using std::begin;
template<class C>
auto adl_begin(C && c) -> decltype(begin(std::forward<C>(c)))
{
return begin(std::forward<C>(c));
}
}
using adl::adl_begin;
# include "ADLBeginEnd.h"
template<class C>
void print(C const& c)
{
std::copy(adl_begin(c), adl_end(c), std::ostream_iterator<decltype(*adl_begin(c))>(std::cout, " ")); std::cout << "\n";
for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
优点: 一致的调用约定,完全通用
- 与@Xeo的建议相同的优点+
- 重复的using声明已封装(DRY)
缺点: 有点冗长
adl_begin()
/ adl_end()
不如 begin()
/ end()
简洁
- 也许它不太惯用(尽管它是明确的)
- 在等待C++14返回类型推断时,还将污染命名空间,包括
std::begin
/ std::end
注意: 不确定这是否真的改进了之前的方法。
明确限定std::begin()
或std::end()
既然已经放弃了begin()
/ end()
的简洁性,为什么不回到限定调用std::begin()
/ std::end()
的方式呢? 示例代码:
namespace std {
template<>
auto begin(legacy::IntContainer& c) -> decltype(c.legacy_begin())
{
return c.legacy_begin();
}
}
namespace legacy {
template<class T>
class Container
{
public:
auto begin() -> decltype(legacy_begin()) { return legacy_begin(); }
auto end() -> decltype(legacy_end()) { return legacy_end(); }
};
}
template<class C>
void print(C const& c)
{
std::copy(std::begin(c), std::end(c), std::ostream_iterator<decltype(*std::begin(c))>(std::cout, " ")); std::cout << "\n";
for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
优点: 一致的调用约定几乎可以通用
- 适用于任何标准容器和定义了成员
.begin()
和.end()
的用户类型
- 适用于C风格数组
缺点: 有点啰嗦,后期转换不够通用,是维护问题
std::begin()
/ std::end()
比begin()
/ end()
更加冗长
- 只能通过在
namespace std
中提供非成员函数模板begin()
和end()
的显式特化来使其适用于没有成员.begin()
和end()
的类LegacyContainer
(并且没有源代码!)(也适用于范围for循环!)。
- 只能通过直接在
LegacyContainer<T>
的源代码中添加成员函数begin()
/ end()
将其后期转换为类模板LegacyContainer<T>
上。这里的namespace std
技巧不起作用,因为函数模板不能被部分特化。
使用什么?
通过容器自己的命名空间中的非成员begin()
/ end()
的ADL方法是惯用的C++11方法,特别是对于需要在遗留类和类模板上进行后期转换的通用函数。这与用户提供的非成员swap()
函数的惯用法相同。
对于仅使用标准容器或C风格数组的代码,可以在不引入使用声明的情况下随处调用std::begin()
和std::end()
,但代价是更冗长的调用。这种方法甚至可以进行后期转换,但需要涉及到namespace std
(针对类类型)或原地源代码修改(针对类模板)。它可以做到,但不值得维护麻烦。
在非通用代码中,在编写时已知所涉及的容器的情况下,甚至可以仅依赖ADL来处理标准容器,并显式限定C风格数组的std::begin
/ std::end
。这会损失一些调用一致性,但可以节省使用声明。
begin
和end
函数。名称查找在[stmt.ranged]/1中明确提到:“begin
和end
通过参数相关的查找(3.4.2)进行查找。对于此名称查找,命名空间std
是一个关联的命名空间。” - dypbegin
/end
函数。我不知道你如何用自己的代码模拟这个。 - dypbegin
/end
成员函数时,才会执行ADL。 - dypstd::begin(c)
,则隐含要求c
要么是数组类型,要么具有begin
/end
成员函数。后者是由于begin(c)
的声明具有返回类型decltype(c.begin())
:您无法通过函数模板特化更改返回类型,并且不允许在std
命名空间中重载std::begin
。 - dyp