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

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

每个标准容器都有一个返回该容器迭代器的beginend方法。然而,C++ 11显然引入了称为EDCOX1、2和EDCOX1 3的自由函数,称为EDCOX1×0和EDCOX1,1个成员函数。所以,不是写作

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

你会写

1
2
3
4
using std::begin;
using std::end;
auto i = begin(v);
auto e = end(v);

在他的谈话中,编写现代C++时,Herb Sutter说,当你想要一个容器的开始或结束迭代器时,你现在应该总是使用自由函数。然而,他并没有详细说明你为什么要这样做。查看代码,它可以保存所有字符。因此,就标准容器而言,自由功能似乎完全无用。Herb Sutter表示,对于非标准容器有好处,但他再次没有详细说明。

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


如何在C数组上调用.begin().end()

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


当您有包含类的库时,请考虑这种情况:

1
class SpecialArray;

它有两种方法:

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

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

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

但如果你总是用

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

您可以这样做:

1
2
3
4
5
6
7
8
9
10
11
template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

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

其中,SpecialArrayIterator类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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可以合法地用于特殊数组的值的迭代和访问。


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

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

最明显的用途是用于C数组(而不是C指针)。

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

另一方面,下一个C++ Rev应该复制D的伪成员表示法。如果没有定义a.foo(b,c,d),则尝试foo(a,b,c,d)。这只是一点点的句法糖分,帮助我们穷人谁更喜欢主语,而不是动词排序。


要回答您的问题,默认情况下,自由函数begin()和end()只调用容器的member.begin()和.end()函数。从中,当您使用任何标准容器(如等)时自动包含,您可以得到:

1
2
3
4
template< class C >
auto begin( C& c ) -> decltype(c.begin());
template< class C >
auto begin( const C& c ) -> decltype(c.begin());

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

1
2
3
4
5
6
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字符串、枚举等的代码。通过使用非成员函数,可以宣传一个更通用的接口,人们可以轻松地扩展它。通过使用自由功能界面:

1
2
3
4
5
6
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字符串。现在编写少量适配器代码:

1
2
3
4
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); }

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


std::beginstd::end的一个好处是它们用作扩展点。用于实现外部类的标准接口。

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

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

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

在这种情况下,std::beginstd::end很有用,因为我们可以提供迭代器API不修改类本身,而是重载自由函数。

示例:假设您希望实现接受容器的count_if函数而不是一对迭代器。此类代码可能如下所示:

1
2
3
4
5
6
7
8
9
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,它还将考虑在其参数的命名空间中声明的函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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::beginsome_lib::end并不重要。-由于CustomContainer也在some_lib::中,编译器将在count_if中使用这些重载。

这也是using std::begin;using std::end;进入count_if的原因。这使得我们可以使用不合格的beginend,因此允许adl和允许编译器在找不到其他选项时选择std::beginstd::end

我们可以吃曲奇饼和曲奇饼-也就是说,有一种方法来提供自定义实现在编译程序可以恢复到标准的情况下。

一些注意事项:

    百万千克1

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

    百万千克1百万千克1

    正如其他答案所提到的,std::版本对裸数组有过载。这很有用,但这只是我上面描述的一个特例。

    百万千克1百万千克1

    在编写模板代码时,使用std::begin和friends是一个特别好的主意,因为这使得这些模板更加通用。对于非模板,您可能只是以及使用方法(如适用)。

    百万千克1

另外,我知道这篇文章已经快7岁了。我遇到它是因为我想回答一个标记为重复的问题,发现这里没有答案提到ADL。


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

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