60

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 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 (i.e. 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 (i.e. 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;?

4

1 回答 1

36

有几种方法,每种方法都有自己的优点和缺点。以下三种方法进行了成本效益分析。

ADL通过自定义非会员begin()/end()

第一个替代方案在命名空间内提供非成员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 风格的数组
  • 可以对任何没有成员且不需要修改源代码的类模板进行改造(也适用于范围 for 循环!) legacy::Container<T>.begin()end()

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

  • std::begin并且std::end需要作为 C 样式数组的后备选项被引入每个显式调用范围(模板头的潜在陷阱和一般的麻烦)

ADL 通过自定义非会员adl_begin()adl_end()

第二种选择是adl通过提供非成员函数模板adl_begin()and将先前解决方案的 using-declarations 封装到单独的命名空间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 的建议相同的优点 +
  • 重复的使用声明已被封装(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()
  • 只能通过提供非成员函数模板 式特化和LegacyContainer.begin()end()begin()end()namespace std
  • 只能通过在源代码中直接添加成员函数/来改装到类模板 上(对于模板可用)。这个技巧在这里不起作用,因为函数模板不能部分特化。 LegacyContainer<T>begin()end()LegacyContainer<T>namespace std

用什么?

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

对于只使用标准容器或 C 样式数组的代码,std::begin()并且std::end()可以在不引入 using 声明的情况下在任何地方调用,但代价是更冗长的调用。这种方法甚至可以进行改造,但它需要摆弄namespace std(对于类类型)或就地源代码修改(对于类模板)。可以做到,但不值得维护麻烦。

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

于 2013-07-10T07:09:14.280 回答