自定义容器应该有免费的开始/结束功能吗?

当创建一个按常规规则运行的自定义容器类(即使用 STL 算法、使用行为良好的通用代码等)时,在 C++03 中实现迭代器支持和成员开始/结束函数.

When creating a custom container class that plays by the usual rules (i.e. 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 引入了两个新概念――基于范围的 for 循环和 std::begin/end.基于范围的 for 循环理解成员开始/结束函数,因此任何 C++03 容器都支持开箱即用的基于范围的 for.对于算法,推荐的方法(根据 Herb Sutter 的编写现代 C++ 代码")是使用 std::begin 而不是成员函数.

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.

然而,在这一点上我不得不问 - 是调用完全限定的 begin() 函数(即 std::begin(c))还是依赖 ADL 并调用 begin(c) 的推荐方法?

However, at this point I have to ask - is the recommended way to call a fully qualified begin() function (i.e. std::begin(c)) or to rely on ADL and call begin(c)?

ADL 在这种特殊情况下似乎没用 - 因为 std::begin(c) 如果可能的话委托给 c.begin() ,通常的 ADL 好处似乎并不适用.如果每个人都开始依赖 ADL,那么所有自定义容器都必须在其必需的命名空间中实现额外的 begin()/end() 自由函数.然而,一些消息来源似乎暗示对开始/结束的不合格调用是推荐的方式(即 https://svn.boost.org/trac/boost/ticket/6357).

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 (i.e. https://svn.boost.org/trac/boost/ticket/6357).

那么 C++11 的方式是什么?容器库作者是否应该为他们的类编写额外的开始/结束函数,以在没有使用命名空间 std 的情况下支持不合格的开始/结束调用;或使用 std::begin;?

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.

第一个替代方案在 legacy 命名空间内提供非成员 begin()end() 函数模板,以将所需的功能改型到任何可以提供它的类或类模板,但具有例如错误的命名约定.然后调用代码可以依赖 ADL 来查找这些新函数.示例代码(基于@Xeo 的评论):

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 e.g. 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 << "
";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    for (auto elem: c) std::cout << elem << " "; std::cout << "
";
}

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

  • 适用于定义成员 .begin().end()
  • 的任何标准容器和用户类型
  • 适用于 C 风格的数组
  • 可以为任何 类模板 legacy::Container<T> 进行改造(也适用于range-for循环!)没有成员 .begin()end() 无需修改源代码
  • works for any Standard Container and user-types that define member .begin() and .end()
  • works for C-style arrays
  • can be retrofitted to work (also for range-for loops!) for any class template legacy::Container<T> that does not have member .begin() and end() without requiring source code modifications

缺点:在很多地方都需要使用声明

Cons: requires using-declarations in many places

  • std::beginstd::end 必须作为 C 样式数组的后备选项被引入每个显式调用范围(对于模板标题和一般的麻烦)
  • 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_begin() 将先前解决方案的 using-declarations 封装到单独的 adl 命名空间中adl_end(),然后也可以通过ADL找到.示例代码(基于@Yakk 的评论):

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 << "
";

    // 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 << "
";
}

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

Pros: consistent calling convention that works completely generically

  • 与@Xeo 的建议相同的优点 +
  • 重复的 using-declarations 已被封装 (DRY)

缺点:有点冗长

  • adl_begin()/adl_end() 不如 begin()/end() 简洁
  • 它可能也不是惯用的(虽然它是明确的)
  • 等待 C++14 返回类型推导,也会用 std::begin/std::end
  • 污染命名空间
  • adl_begin() / adl_end() is not as terse as begin() / end()
  • it is perhaps also not as idiomatic (although it is explicit)
  • pending C++14 return type deduction, will also pollute namespace with std::begin / std::end

注意:不确定这是否真的比以前的方法有所改进.

NOTE: Not sure if this really improves upon the previous approach.

一旦 begin()/end() 的冗长已经被放弃,为什么不回到 std::begin 的合格调用()/std::end()?示例代码:

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 << "
";

    // 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 << "
";
}

优点:一致的调用约定,几乎通用

Pros: consistent calling convention that works almost generically

  • 适用于定义成员 .begin().end()
  • 的任何标准容器和用户类型
  • 适用于 C 风格的数组

缺点:有点冗长和改造不是通用的,并且是维护问题

Cons: a little verbose and retrofitting is not generic and a maintainence problem

  • std::begin()/std::end()begin()/ 稍微冗长一些end()
  • 只能对没有成员 的任何 class LegacyContainer 进行改造(也适用于 range-for 循环!)>.begin()end()(并且没有源代码!)通过提供非成员函数模板的显式特化 begin()namespace std
  • 中的 code> 和 end()
  • 只能通过直接添加成员函数 begin()/end() 改装到 类模板 LegacyContainerLegacyContainer 的源代码中(模板可用).namespace std 技巧在这里不起作用,因为函数模板不能部分特化.
  • std::begin() / std::end() is a little more verbose than begin() / end()
  • can only be retrofitted to work (also for range-for loops!) for any class 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
  • can only be retrofitted onto class templates 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.?

在容器自己的命名空间中通过非成员 begin()/end() 的 ADL 方法是惯用的 C++11 方法,尤其是对于那些需要对遗留类和类模板进行改造.它与用户提供非成员 swap() 函数的习惯用法相同.

The ADL approach through non-member begin() / end() in a a 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.

对于只使用标准容器或 C 样式数组的代码,std::begin()std::end() 可以在任何地方调用,而无需引入 using-声明,以更详细的调用为代价.这种方法甚至可以进行改造,但它需要摆弄 namespace std(对于类类型)或就地源代码修改(对于类模板).可以,但不值得维护.

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.

在非通用代码中,所讨论的容器在编码时是已知的,甚至可以仅将 ADL 用于标准容器,并明确限定 std::begin/std::end 用于 C 风格的数组.它失去了一些调用一致性,但节省了使用声明.

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.

相关文章