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

zeu*_*xcg 58 c++ containers iterator argument-dependent-lookup c++11

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

C++ 11引入了两个新概念 - 基于范围的for循环和std :: begin/end.基于范围的for循环理解成员开始/结束函数,因此任何C++ 03容器都支持基于范围的开箱即用.对于算法,推荐的方法(根据Herb Sutter的'Writing modern C++ code')是使用std :: begin而不是member function.

但是,此时我不得不问 - 是否建议调用完全限定的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;?

Tem*_*Rex 35

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

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";
}
Run Code Online (Sandbox Code Playgroud)

优点:一致且简洁的调用约定完全有效

  • 适用于定义成员.begin()和的任何标准容器和用户类型.end()
  • 适用于C风格的数组
  • 可以改装为没有成员且不需要修改源代码的任何类模板工作(也适用于range-for循环!) legacy::Container<T>.begin()end()

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

  • std::beginstd::end要求已纳入每一个明确的通话范围的回落对于C风格的数组选项(潜在的缺陷模板头和普通滋扰)

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

第二种替代方案是封装先前的溶液到一个单独的使用,声明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";
}
Run Code Online (Sandbox Code Playgroud)

优点:一致的调用约定完全有效

  • 与@ 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";
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • std::begin()/ std::end()begin()/ 更冗长一点end()
  • 只能改装工作(也可用于范围-for循环!)对于任何 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()随处叫不引入使用申述,在更详细通话的费用.这种方法甚至可以改装,但它需要摆弄namespace std(对于类类型)或就地源修改(对于类模板).它可以做到,但不值得维护麻烦.

在非通用代码中,有问题的容器在编码时是已知的,人们甚至可以仅依赖于标准容器的ADL,并明确限定std::begin/ std::end用于C样式的数组.它失去了一些调用一致性,但节省了使用声明.

  • 如果它不是你自己的容器怎么办?如果它没有合适的接口,但提供其他可以让你获得等效行为的方法,*如果只有一个可以在接口上添加`begin` /`end`,那该怎么办?*Qt做对了并提供了它旁边的那些自己的迭代设施,但不是每个图书馆都有这样的先见之明. (6认同)
  • 我不相信(事实上,我认为你错了) - 成语`使用std :: swap; swap(a,b);`在C++中是牢固建立的(它是*在通用上下文中调用`swap`的正确方法)...你没有解释为什么`使用std :: begin不一样;`. (5认同)
  • "我不会推荐这种做法" - 我愿意.这是使用`std :: begin`和`std :: end`的正确方法,其他一切在**泛型**代码中完全*无用*因为无法找到自由的`begin`和`end`函数,因为没有成员开始/结束的容器将无法使用您的代码. (4认同)
  • @TemplateRex谢谢,这说服了我. (3认同)
  • @Xeo D编程语言具有统一函数调用语法,它将自动查找非成员函数`fun(c)`如果找不到成员函数`c.fun()`(反之亦然).在C++中这真的很酷 (3认同)
  • @KonradRudolph但是`swap`可以从用户定义的优化中受益,正如我上面所写,所以你真的想调用swap不合格并回退到`std :: swap`.但是像"begin"这样的getter中优化的范围在哪里?相比之下,这里只使用`using std :: begin;`来保存类型,而不是提供回退到`std :: begin`的用户定义的实现. (2认同)
  • 我不推荐这种方法.相反,我会`命名空间adl {使用std :: begin; template <typename C> auto adl_begin(C && c) - > decltype(begin(std :: forward <C>(c))){return begin(std :: forward <C>(c)); 使用adl :: adl_begin;`和`end`同样如此.通过使用这种技术,我可以使用`std :: begin`来调用符合adl的`begin`而不必总是在我需要的每个位置使用std :: begin`,这在某些情况下是不可能的.上下文(如`auto->`return type deduction).现在我可以在我想要的时候做ADL,而且我正在做ADL的事实很清楚. (2认同)
  • @Xeo经过进一步的反思,我不得不再次同意你的意见(我今天已经改变了我的想法,我知道,现在很有希望),ADL方法就是惯用的解决方案.Tnx坚持:-) (2认同)