为什么在C ++ 11中使用非成员的begin和end函数?


197

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

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

你会写

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

Herb Sutter 在他的写现代C ++的演讲中说,当您想要容器的开始或结束迭代器时,应该始终使用自由函数。但是,他没有详细说明为什么要这么做。查看代码,可以节省一个字符。因此,就标准容器而言,自由功能似乎完全没有用。赫伯·萨特(Herb Sutter)表示,使用非标准容器也有好处,但是他没有详细介绍。

那么,问题到底是什么做的免费功能的版本std::begin,并std::end做超出调用它们相应的成员函数的版本,你为什么要使用它们?


29
它减少了一个字符,为您的孩子保存了这些点:xkcd.com/297
HostileFork说不要相信

我不愿意使用它们,因为我必须一直重复std::
Michael Chourdakis

Answers:


162

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

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


7
@JonathanMDavis:您可以使用模板编程技巧来end获取用于静态声明的数组(int foo[5])。一旦衰减到指针,您当然就不走运了。
Matthieu M.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }

6
@JonathanMDavis:当别人指出的,那肯定是有可能得到beginendC数组,只要在你还没有它腐烂的指针自己- @Huw拼出来。至于为什么要:假设您重构了使用数组使用向量的代码(或者由于任何原因,反之亦然)。如果您一直在使用beginend,也许还使用了一些巧妙的typedeffing,则实现代码根本不需要更改(也许某些typedef除外)。
卡尔·克内希特尔

31
@JonathanMDavis:数组不是指针。对于所有人:为了结束这种日益突出的困惑,请不要将(某些)指针称为“衰减数组”。语言中没有这样的术语,并且实际上没有用。指针是指针,数组是数组。数组可以隐式转换为指向第一个元素的指针,但是仍然只是常规的旧指针,与其他指针没有区别。当然,在关闭大小写的情况下,您无法获得指针的“结尾”。
GManNickG 2011年

5
好吧,除了数组之外,还有许多API公开了类似容器的方面。显然,您不能修改第三方API,但可以轻松编写这些独立的开始/结束函数。
edA-qa mort-ora-y

35

考虑当您的库包含类时的情况:

class SpecialArray;

它有2种方法:

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


8
这不应包括template<>行。您正在声明一个新的函数重载,而不是专门用于模板。
大卫·斯通

33

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

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

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

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

顺便说一句,下一个C ++版本应该复制D的伪成员表示法。如果a.foo(b,c,d)未定义,则尝试运行foo(a,b,c,d)。这只是一点语法上的帮助,可以帮助我们喜欢主题而不是动词排序的穷人。


5
伪成员符号看起来像C#/。NET 扩展方法。尽管它们确实适用于各种情况-与所有功能一样-容易被“滥用”。
加雷斯·威尔逊

5
伪成员表示法是使用Intellisense进行编码的福音。打“ a”。显示相关动词,从记忆列表中释放脑力以及帮助发现相关API函数可以帮助防止功能重复,而不必将非成员函数塞入类。
马特·柯蒂斯

有一些建议将其纳入C ++,并使用术语统一函数调用语法(UFCS)。
underscore_d

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类型!)向前兼容,对其他开发者而言成本较低。


很好的例子。不过,我不会enum通过引用采用任何其他基本类型。复制将比间接复制便宜。
underscore_d

6

的一个好处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::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/ rendstd::sizestd::data

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

  • std::begin在编写模板代码时,使用and friends是一个特别好的主意,因为这使这些模板更加通用。对于非模板,在适用时,您也可以使用方法。

附言:我知道这篇文章已有7年历史了。我碰到它是因为我想回答一个被标记为重复的问题,发现此处没有答案提及ADL。


好的答案,尤其是公开解释ADL,而不是像其他所有人一样让它浮想联翩-即使他们在行动中展示它!
underscore_d

5

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

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


3
在C ++ 03中,您可以只使用boost::begin()/ end(),所以没有真正的不兼容性:)
Marc Mutz-mmutz 2012年

1
@ MarcMutz-mmutz好吧,增强依赖并非始终是一个选择(如果仅用于,那就太过分了begin/end)。因此,我也认为这与纯C ++ 03不兼容。但是就像说的那样,这是一个很小的(并且越来越小)的不兼容性,因为begin/end无论如何C ++ 11(至少尤其是)越来越多地被采用。
Christian Rau 2012年

0

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.