自定义容器是否应该具有免费的begin/end函数?

61

在创建按照通常规则(即与STL算法配合使用、与规范的通用代码配合使用等)运作的自定义容器类时,在C++03中只需实现迭代器支持和成员begin/end函数即可。

C++11引入了两个新概念-基于范围的for循环和std::begin/end。基于范围的for循环理解成员begin/end函数,因此任何C++03容器都支持开箱即用的基于范围的for循环。对于算法,推荐的方式(根据Herb Sutter的“编写现代C++代码”)是使用std::begin而不是成员函数。

然而,此时我不得不问一下-调用完全限定的begin()函数(即std::begin(c))是推荐的方式,还是依赖ADL并调用begin(c)?

在这种特殊情况下,ADL似乎毫无用处-因为如果可能,std::begin(c)将委托给c.begin(),所以通常的ADL优势似乎不适用。如果每个人都开始依赖ADL,那么所有自定义容器都必须在它们的所需名称空间中实现额外的begin()/end()自由函数。但是,一些来源似乎暗示未经限定的调用begin/end是推荐的方式(即https://svn.boost.org/trac/boost/ticket/6357)。

那么C++11的方式是什么?容器库作者应该为其类编写额外的begin/end函数,以支持在没有使用namespace std的情况下调用未经限定的begin/end,还是使用std::begin呢?


请注意,标准偏好在范围基于的for循环中使用成员函数;如果找不到它们或者范围初始化不是数组或类类型,则使用未限定的beginend函数。名称查找在[stmt.ranged]/1中明确提到:“beginend通过参数相关的查找(3.4.2)进行查找。对于此名称查找,命名空间std是一个关联的命名空间。” - dyp
是的,但如果类具有begin/end成员,则不会执行查找,对吗?因此-如果我错了,请纠正我-从标准的角度来看,ADL仅适用于不像标准容器的对象,因此它不能用作我的情况下的指导。 - zeuxcg
1
附录:请注意,基于范围的for语句的标准不使用未限定名称查找,而是显式地使用参数相关查找。我用clang++3.2测试了这一点以支持我的解释:如果range-init表达式的类型是在命名空间中声明的类类型,则基于范围的for语句不会找到全局的begin/end函数。我不知道你如何用自己的代码模拟这个。 - dyp
是的,只有当表达式不是类或数组类型,或者它是类类型但找不到begin/end成员函数时,才会执行ADL。 - dyp
2
另一个备注 ;) 如果您使用限定版本 std::begin(c),则隐含要求 c 要么是数组类型,要么具有 begin/end 成员函数。后者是由于 begin(c) 的声明具有返回类型 decltype(c.begin()):您无法通过函数模板特化更改返回类型,并且不允许在 std 命名空间中重载 std::begin - dyp
是的,没错。不过我不确定这是否是一个问题。这个问题的关键在于:是否存在一种情况,在这种情况下调用std::begin会导致问题,例如与某些自定义类型不兼容或缺乏有用的扩展点? - zeuxcg
1个回答

36

有几种方法可供选择,每种方法都有其优缺点。以下是三种方法及其成本效益分析。

通过自定义非成员 begin() / end() 实现 ADL

第一种替代方案提供了非成员 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";
}

优点:调用约定一致简洁,完全通用

  • 适用于任何标准容器和定义了成员.begin().end()的用户类型
  • 适用于C语言风格的数组
  • 可以通过后期修改来适应任何没有成员.begin().end()的类模板legacy::Container<T>(包括范围for循环!)而不需要修改源代码

缺点:需要在多个地方使用using-declarations。

  • 对于C语言风格的数组,必须将std::beginstd::end作为备选项带入每个显式调用范围内(对于模板头文件可能导致潜在的问题和麻烦)

通过自定义非成员函数adl_begin()adl_end()实现ADL

第二种替代方案是将前一个解决方案中using声明封装到一个单独的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";
}

优点: 一致的调用约定,完全通用

  • 与@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()的方式呢? 示例代码:

// 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()的用户类型
  • 适用于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。这会损失一些调用一致性,但可以节省使用声明。


5
我不太同意(事实上,我认为你是错的)—— 在C++中,使用“using std::swap; swap(a, b);”这个习惯用语已经深入人心(在泛型上下文中调用“swap”的正确方式),你没有解释为什么类似的操作不能用于“using std::begin”。 - Konrad Rudolph
2
@KonradRudolph 但是,正如我之前所写的那样,“swap”可以从用户定义的优化中受益,因此您确实希望调用未经限定的“swap”,并回退到“std :: swap”。但是像“begin”这样的getter中有哪些优化范围呢?相比之下,“using std :: begin;”仅用于节省打字,而不是提供具有回退到“std :: begin”的用户定义实现。 - TemplateRex
4
“我不建议采取这种方法” -- 我会建议采用这种方法。这是使用 std::beginstd::end 的正确方式,其他任何东西在通用代码中都是完全无用的,因为如果没有自由的 beginend 函数,将找不到容器成员 begin/end,因此没有成员 begin/end 的容器将无法与您的代码一起使用。 - Xeo
6
如果它不是你自己的容器怎么办?如果它没有适合的接口,但提供其他手段可以让你获得等效的行为,*只要你可以在接口中添加 begin/end*?Qt做得很好,除了自己的迭代功能外,还提供了这些功能,但并不是每个库都有这样的远见。 - Xeo
3
D编程语言具有统一函数调用语法,如果找不到成员函数c.fun(),它会自动查找非成员函数fun(c)(反之亦然)。这在C++中会非常棒。 - TemplateRex
显示剩余22条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接