为什么在C++ 11中使用非成员开始和结束函数?

Jon*_*vis 185 c++ iterator stl c++11 container-data-type

每个标准容器具有beginend方法,返回的迭代器是容器.然而,C++ 11显然已经引入自由函数调用std::beginstd::end该调用beginend成员函数.所以,而不是写作

auto i = v.begin();
auto e = v.end();
Run Code Online (Sandbox Code Playgroud)

你会写的

using std::begin;
using std::end;
auto i = begin(v);
auto e = end(v);
Run Code Online (Sandbox Code Playgroud)

在他的演讲中,编写现代C++,Herb Sutter说当你想要容器的开始或结束迭代器时,你应该总是使用自由函数.但是,他没有详细说明你想要的原因.查看代码,它可以为您节省一个字符.因此,就标准容器而言,自由函数似乎完全没用.Herb Sutter表示非标准容器有好处,但他再次没有详细说明.

因此,问题是除了调用相应的成员函数版本之外,自由函数版本究竟做了什么,std::begin并且std::end做了什么,为什么要使用它们呢?

Mat*_* M. 154

你如何称呼.begin().end()一个C-阵列上?

自由函数允许更通用的编程,因为它们可以在之后添加到您无法改变的数据结构上.

  • `template <typename T,size_t N> T*end(T(&a)[N]){return a + N; }` (32认同)
  • @JonathanMDavis:数组不是指针.对每个人来说:为了结束这种日益突出的混乱,请不要将(某些)指针称为"衰变数组".语言中没有这样的术语,实际上没有用处.指针是指针,数组是数组.可以隐式地将数组转换为指向其第一个元素的指针,但它仍然只是一个常规的旧指针,与其他元素没有区别.当然你不能得到一个指针的"结束",案件关闭. (31认同)
  • @JonathanMDavis:你可以使用模板编程技巧为静态声明的数组(`int foo [5]`)提供`end`.一旦它腐烂到指针,你当然不走运. (7认同)
  • @JonathanMDavis:正如其他人指出的那样,只要你还没有将它自己腐烂成指针,就可以在C数组上得到`begin`和`end` - @Huw将它拼出来.至于为什么你想要:想象你重构了使用数组使用向量的代码(反之亦然,无论出于何种原因).如果你一直在使用`begin`和`end`,也许是一些聪明的类型deffing,实现代码根本不需要改变(除了一些typedef). (6认同)
  • 好吧,除了数组之外,还有大量的API暴露容器之类的方面.显然,您无法修改第三方API,但您可以轻松编写这些独立的开始/结束功能. (5认同)

Gre*_*ape 35

考虑具有包含类的库的情况:

class SpecialArray;
Run Code Online (Sandbox Code Playgroud)

它有2种方法:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);
Run Code Online (Sandbox Code Playgroud)

迭代它的值你需要从这个类继承并定义begin()end()方法的情况

auto i = v.begin();
auto e = v.end();
Run Code Online (Sandbox Code Playgroud)

但如果你总是使用

auto i = begin(v);
auto e = end(v);
Run Code Online (Sandbox Code Playgroud)

你可以这样做:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}
Run Code Online (Sandbox Code Playgroud)

在哪里SpecialArrayIterator是这样的:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};
Run Code Online (Sandbox Code Playgroud)

现在i,e可以合法地用于迭代和访问SpecialArray的值

  • 这不应该包含`template <>`行.您正在声明一个新的函数重载,而不是专门化模板. (8认同)

def*_*ode 32

使用beginendfree函数添加一层间接.通常这样做是为了提供更大的灵活性.

在这种情况下,我可以想到一些用途.

最明显的用途是C数组(不是c指针).

另一种方法是尝试在不符合要求的容器上使用标准算法(即容器缺少.begin()方法).假设您不能只修复容器,那么下一个最佳选择是重载该begin功能.Herb建议您始终使用该begin功能来提高代码的一致性和一致性.而不是必须记住哪些容器支持方法begin和哪些需要功能begin.

顺便说一下,下一个C++ rev应该复制D的伪成员符号.如果a.foo(b,c,d)没有定义它反而尝试foo(a,b,c,d).它只是一点点语法糖来帮助我们那些喜欢主语然后动词排序的穷人.

  • _pseudo-member notation_看起来像C#/.Net _extension methods_.它们确实对各种情况有用 - 但是像所有功能一样 - 可能容易"滥用". (5认同)
  • 伪成员符号是用Intellisense编码的福音; 击中"a".显示相关的动词,从记忆列表中释放脑力,并帮助发现相关的API函数有助于防止重复功能,而无需将非成员函数强加到类中. (5认同)

小智 17

要回答你的问题,默认情况下,自由函数begin()和end()除了调用容器的成员.begin()和.end()函数之外别无其他.从<iterator>当你使用任何标准的容器,如的,自动列入<vector>,<list>等等,您可以:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 
Run Code Online (Sandbox Code Playgroud)

你问的第二部分是为什么更喜欢自由函数,如果他们所做的只是调用成员函数.这实际上取决于v示例代码中的对象类型.如果v的类型是标准容器类型,vector<T> v;那么如果使用free或member函数并不重要,它们会做同样的事情.如果您的对象v更通用,如下面的代码:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}
Run Code Online (Sandbox Code Playgroud)

然后使用成员函数来破坏T = C数组,C字符串,枚举等的代码.通过使用非成员函数,您可以宣传人们可以轻松扩展的更通用的界面.通过使用免费功能界面:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}
Run Code Online (Sandbox Code Playgroud)

该代码现在适用于T = C数组和C字符串.现在编写少量的适配器代码:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }
Run Code Online (Sandbox Code Playgroud)

我们也可以使您的代码与可迭代枚举兼容.我认为Herb的主要观点是使用自由函数就像使用成员函数一样简单,它使代码向后兼容C序列类型,并向前兼容非stl序列类型(以及future-stl类型!),与其他开发人员的低成本.


Chr*_*ica 5

虽然非成员函数不为标准容器提供任何好处,但使用它们会强制执行更一致和灵活的样式.如果您有时想扩展现有的非std容器类,则宁可定义自由函数的重载,而不是更改现有类的定义.因此,对于非std容器,它们非常有用,并且始终使用自由函数使您的代码更加灵活,因为您可以更轻松地用非std容器替换std容器,并且底层容器类型对代码更透明,因为它支持更广泛的容器实现.

但当然,这总是必须加权,而抽象也不好.虽然使用自由函数并不是一个过度抽象的东西,但它仍然破坏了与C++ 03代码的兼容性,在这个年轻的C++ 11代码可能仍然是一个问题.

  • 在C++ 03中,你可以使用`boost :: begin()`/`end()`,所以没有真正的不兼容性:) (3认同)

joe*_*hip 5

的一个好处std::begin,并std::end为他们充当扩展点实现的标准接口,外部类.

如果你想使用CustomContainer带有基于范围的for循环或模板函数的类,它们需要.begin().end()方法,你显然必须实现这些方法.

如果类确实提供了这些方法,那不是问题.如果没有,你必须修改它*.

这并不总是可行的,例如在使用外部库时,特别是商业和闭源库.

在这种情况下,std::beginstd::end派上用场,因为可以提供迭代器API而无需修改类本身,而是重载自由函数.

示例:假设您要实现count_if接受容器而不是一对迭代器的函数.这样的代码可能如下所示:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}
Run Code Online (Sandbox Code Playgroud)

现在,对于您要使用此自定义的任何类count_if,您只需添加两个自由函数,而不是修改这些类.

现在,C++有一个名为Argument Dependent Lookup (ADL)的机制,这使得这种方法更加灵活.

简而言之,ADL意味着,当编译器解析非限定函数(即没有命名空间的函数,begin而不是代替std::begin)时,它还将考虑在其参数的名称空间中声明的函数.例如:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);
Run Code Online (Sandbox Code Playgroud)

在这种情况下,不要紧,合格的名称是some_lib::beginsome_lib::end -因为CustomContainer是在some_lib::太,编译器将使用这些重载count_if.

这也是因为拥有using std::begin;using std::end;进入的原因count_if.这使我们能够使用不合格beginend,因此允许ADL 允许编译器来接std::beginstd::end当发现没有其他的替代品.

我们可以吃cookie并拥有cookie - 即有一种方法可以提供begin/的自定义实现,end而编译器可以回退到标准的.

一些说明:

  • 出于同样的原因,还有其他类似的功能:std::rbegin/ rend, std::sizestd::data.

  • 正如其他答案所提到的,std::版本对裸阵列有重载.这很有用,但只是我上面所描述的特例.

  • std::begin在编写模板代码时,使用和朋友特别好,因为这会使这些模板更通用.对于非模板,您也可以在适用时使用方法.

PS我知道这篇文章已有近7年的历史了.我遇到它是因为我想回答一个被标记为重复的问题并发现这里没有回答提到ADL.