为什么在c++ 11中使用非成员的开始和结束函数
Why use non-member begin and end functions in C++11?
每个标准容器都有一个begin
和end
方法,用于返回该容器的迭代器。然而,c++ 11显然引入了名为std::begin
和std::end
的自由函数,它们调用begin
和end
成员函数。所以,不要写
auto i = v.begin();
auto e = v.end();
你会写
auto i = std::begin(v);
auto e = std::end(v);
在他的演讲《编写现代c++》中,Herb Sutter说,当你想要容器的开始或结束迭代器时,你应该现在就使用自由函数。然而,他并没有详细说明为什么要。看看代码,它为您节省了一个字符。因此,就标准容器而言,自由函数似乎是完全无用的。Herb Sutter指出了非标准容器的好处,但是他没有详细说明。
那么,问题是std::begin
和std::end
的自由函数版本除了调用它们相应的成员函数版本之外究竟做了什么,为什么要使用它们?
如何调用c数组上的.begin()
和.end()
?
Free-functions允许更多的泛型编程,因为它们可以在之后添加到不能更改的数据结构上。
使用begin
和end
自由函数增加了一层间接。通常这样做是为了更灵活。
在这种情况下,我可以想到几个用途。
最明显的用途是c数组(不是c指针)。
另一个是当试图在不符合标准的容器上使用标准算法时(即容器缺少.begin()
方法)。假设您不能修复容器,那么下一个最佳选择是重载begin
函数。Herb建议您始终使用begin
函数来促进代码的统一性和一致性。而不是必须记住哪些容器支持方法begin
,哪些需要函数begin
。
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
};
现在i
和e
可以合法地用于迭代和访问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::begin
和std::end
的一个好处是它们可以作为扩展点实现外部类的标准接口。
如果你想使用CustomContainer
类与基于范围的for循环或模板函数,它期望.begin()
和.end()
方法,显然必须这样做实现这些方法
如果类确实提供了这些方法,那不是问题。如果没有,你必须修改它*.
这并不总是可行的,例如当使用外部库时,特别是商业和闭源的一个。
在这种情况下,std::begin
和std::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_if
中using std::begin;
和using std::end;
的原因。这允许我们使用非限定的begin
和end
,因此允许ADL 和
std::begin
和std::end
。我们可以吃饼干并拥有饼干-即有一种提供自定义实现的方法begin
/end
的,而编译器可以返回到标准的。
一些注意事项:
出于同样的原因,还有其他类似的功能:
std::rbegin
/rend
,std::size
和std::data
.正如其他答案所提到的,
std::
版本对裸数组有过载。这是有用的,在编写模板代码时,使用
std::begin
和朋友是一个特别好的主意,因为这使得这些模板更加通用。对于非模板,您只需
p。我知道这篇文章是近7年前写的。我偶然发现它是因为我想这么做回答一个被标记为重复的问题,发现这里没有提到ADL的答案。
尽管非成员函数不为标准容器提供任何好处,但使用它们可以强制实现更一致和灵活的样式。如果您有时想要扩展现有的非std容器类,您宁愿定义自由函数的重载,而不是更改现有类的定义。因此,对于非std容器,它们是非常有用的,并且总是使用自由函数使您的代码更加灵活,因为您可以更容易地用非std容器替换std容器,并且底层容器类型对您的代码更加透明,因为它支持更广泛的容器实现。
但是当然,这总是需要适当地加权,过度抽象也不好。尽管使用自由函数并不是一种过度抽象,但它仍然破坏了与c++ 03代码的兼容性,这在c++ 11的年轻时代可能仍然是一个问题。
最终的好处是在一般化的代码中,它与容器无关。它可以对std::vector
、数组或范围进行操作,而无需更改代码本身。
此外,容器,甚至是非自有的容器都可以被改造,这样它们也可以被使用非成员范围访问器的代码不可知地使用。
- 将成员变量添加到共享库中的类中,不会破坏二进制兼容性吗
- 当回溯以零开始时,如何调试崩溃
- 对RValue对象调用的LValue ref限定成员函数
- 为什么使用 "this" 指针调用派生成员函数?
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 助记符和指向成员语法的指针
- 用于访问容器<T>数据成员的正确 API
- 内置函数可查看CPP中的成员变量
- 是否可以初始化不可复制类型的成员变量(或基类)
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 找不到成员对象:没有名为get_event()的成员,也处理多态性和向量
- 嵌套在类中时无法设置成员数据
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 将函数类成员映射到类本身内部
- 基于另一个成员参数将函数调用从类传递给它的一个成员
- 如何初始化非静态模板成员变量从临时工开始,即而无需复制或移动
- 如果绝对没有调用成员函数,是否允许使用不完整类型的向量?如果是这样,从什么时候开始
- 基于循环的C ISTRINGSTREAM范围没有开始成员
- C++11 : 错误:“开始”不是“std”的成员
- 查找从类/结构开始偏移N字节的所有类/结构的所有成员