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

时间:2011-09-29 06:00:09

标签: c++ stl iterator containers c++11

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

auto i = v.begin();
auto e = v.end();
你会写

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

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

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

7 个答案:

答案 0 :(得分:153)

如何在C阵列上拨打.begin().end()

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

答案 1 :(得分:34)

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

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的值

答案 2 :(得分:31)

使用beginend免费函数可以添加一层间接。通常这样做是为了提供更大的灵活性。

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

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

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

顺便说一下,下一个C ++ rev应该复制D的 pseudo-member notation 。如果未定义a.foo(b,c,d),则会尝试foo(a,b,c,d)。这只是一点点语法糖,可以帮助那些喜欢主语然后动词排序的穷人。

答案 3 :(得分: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()); 

你问的第二部分是为什么更喜欢免费功能,如果他们所做的就是调用成员函数。这实际上取决于示例代码中的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 .. */ } 
}

然后使用成员函数打破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序列类型(以及future-stl类型!),与其他开发人员的低成本。

答案 4 :(得分:5)

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

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

答案 5 :(得分:5)

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 ++有一个名为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);

在这种情况下,合格的名称some_lib::beginsome_lib::end无关紧要 - 由于CustomContainer也位于some_lib::,编译器将使用count_if中的那些重载。

这也是using std::begin;using std::end;count_if的原因。 这允许我们使用不合格的beginend,因此允许ADL 当没有找到其他替代方案时,允许编译器选择std::beginstd::end

我们可以吃饼干并吃饼干 - 我。即有办法提供自定义实现 begin / end,而编译器可以回退到标准的。{/ p>

一些注意事项:

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

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

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

P上。 S.我知道这篇文章已有近7年的历史了。我遇到它是因为我想 回答一个标记为重复的问题,发现此处没有答案提到ADL。

答案 6 :(得分:0)

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

此外,还可以对容器(甚至是非自有容器)进行改造,以使它们也可以通过使用基于非成员范围的访问器的代码来不可知地使用。

有关更多详细信息,请参见here