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

时间:2013-07-10 05:21:33

标签: c++ c++11 iterator containers argument-dependent-lookup

当创建按照通常规则播放的自定义容器类(即使用STL算法,使用行为良好的通用代码等)时,在C ++ 03中实现迭代器支持和成员begin /结束职能。

C ++ 11引入了两个新概念 - 基于范围的for循环和std :: begin / end。基于范围的for循环理解成员开始/结束函数,因此任何C ++ 03容器都支持基于范围的开箱即用。对于算法,推荐的方法(根据Herb Sutter的'Writing modern C ++ code')是使用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的方式是什么?容器库作者是否应该为其类编写额外的开始/结束函数,以便在没有使用namespace std的情况下支持不合格的开始/结束调用;或者使用std :: begin;?

1 个答案:

答案 0 :(得分:34)

有几种方法,每种方法各有利弊。以下三种方法进行了成本效益分析。

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风格的数组
  • 对于没有成员legacy::Container<T>的任何类模板 .begin()
  • 可以进行改装(也适用于 range-for循环!)和end(),无需修改源代码

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

  • std::beginstd::end必须被带入每个显式调用范围,作为C风格数组的后备选项(模板标题的潜在缺陷和一般滋扰)

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

第二种方法是通过提供非成员函数模板adladl_begin()将先前解决方案的using声明封装到单独的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()进行改装工作(也适用于 range-for循环!)和end()(并且没有源代码!)通过在begin()
  • 中提供非成员函数模板end()namespace std的显式特化 只能在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风格阵列。它失去了一些调用一致性,但节省了使用声明。