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

Why use non-member begin and end functions in C++11?

本文关键字:成员 开始 函数 结束 c++ 为什么      更新时间:2023-10-16

每个标准容器都有一个beginend方法,用于返回该容器的迭代器。然而,c++ 11显然引入了名为std::beginstd::end的自由函数,它们调用beginend成员函数。所以,不要写

auto i = v.begin();
auto e = v.end();

你会写

auto i = std::begin(v);
auto e = std::end(v);

在他的演讲《编写现代c++》中,Herb Sutter说,当你想要容器的开始或结束迭代器时,你应该现在就使用自由函数。然而,他并没有详细说明为什么要。看看代码,它为您节省了一个字符。因此,就标准容器而言,自由函数似乎是完全无用的。Herb Sutter指出了非标准容器的好处,但是他没有详细说明。

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

如何调用c数组上的.begin().end() ?

Free-functions允许更多的泛型编程,因为它们可以在之后添加到不能更改的数据结构上。

使用beginend自由函数增加了一层间接。通常这样做是为了更灵活。

在这种情况下,我可以想到几个用途。

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

另一个是当试图在不符合标准的容器上使用标准算法时(即容器缺少.begin()方法)。假设您不能修复容器,那么下一个最佳选择是重载begin函数。Herb建议您始终使用begin函数来促进代码的统一性和一致性。而不是必须记住哪些容器支持方法begin,哪些需要函数begin

作为题外话,下一个c++版本应该复制D的伪成员符号。如果没有定义a.foo(b,c,d),则尝试使用foo(a,b,c,d)。这只是一个小小的语法糖来帮助我们这些可怜的人,他们更喜欢主语而不是动词的顺序。

考虑一个包含class:

的库的情况
class SpecialArray;

它有两个方法:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

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

auto i = v.begin();
auto e = v.end();

但是如果你总是使用

auto i = begin(v);
auto e = end(v);

你可以这样做:

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

其中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
};

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

回答您的问题,默认情况下,自由函数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()); 

你问题的第二部分是,如果自由函数所做的只是调用成员函数,为什么更喜欢自由函数?这实际上取决于示例代码中v是哪种对象。如果v的类型是标准容器类型,比如vector<T> v;,那么使用自由函数还是成员函数都无关紧要,它们的作用是一样的。如果您的对象v更通用,如以下代码所示:

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

那么使用成员函数会破坏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 .. */ } 
}

代码现在可以处理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); }

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

std::beginstd::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));
}

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

现在,c++有一种叫做参数依赖查找的机制(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);

在本例中,限定名是some_lib::begin还是some_lib::end并不重要-由于CustomContainer也在some_lib::中,编译器将使用count_if中的那些重载。

这也是count_ifusing std::begin;using std::end;的原因。这允许我们使用非限定的beginend,因此允许ADL

允许编译器在没有其他选择时选择std::beginstd::end

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

一些注意事项:

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

  • 正如其他答案所提到的,std::版本对裸数组有过载。这是有用的,

  • 在编写模板代码时,使用std::begin和朋友是一个特别好的主意,因为这使得这些模板更加通用。对于非模板,您只需

p。我知道这篇文章是近7年前写的。我偶然发现它是因为我想这么做回答一个被标记为重复的问题,发现这里没有提到ADL的答案。

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

但是当然,这总是需要适当地加权,过度抽象也不好。尽管使用自由函数并不是一种过度抽象,但它仍然破坏了与c++ 03代码的兼容性,这在c++ 11的年轻时代可能仍然是一个问题。

最终的好处是在一般化的代码中,它与容器无关。它可以对std::vector、数组或范围进行操作,而无需更改代码本身。

此外,容器,甚至是非自有的容器都可以被改造,这样它们也可以被使用非成员范围访问器的代码不可知地使用。