虽然我对Java中的C和OOP了解不多,但我开始深入研究C ++及其特性。我已经阅读了有关C ++的所有基本知识,但我仍然对某些特定于C ++ 11的东西感到困惑,包括语法和性能方面的问题。在这些事情上是容器迭代器,我发现它以无数的语法形式实现(例如range-based loops)。
我想知道哪些是完全等效的,为什么会想要使用其中一个,以及对性能有何影响。
a)auto
vs明确声明:
是否始终支持auto
?除了代码可读性问题,为什么程序员更喜欢显式声明?
list<int>::const_iterator i = myIntList.begin(); /* Option a1 */
auto i = myIntList.begin(); /* Option a2 */
for(auto i : myIntList) { ... } /* Option a3 */
for(int i : myIntList) { ... } /* Option a4 */
b)紧凑形式与扩展循环形式
list<int> l = {1, 2, 3, ...};
for(auto i : l) { ... } /* Option b1 */
for(auto i = l.begin(); i != l.end(); ++i) { ... } /* Option b2 */
c)常量,非常量/访问类型
为什么/何时更喜欢在循环体中使用引用或常量?
/* Constant/non-constant: */
for(list<int>iterator i = l.begin(); ...) { ... } /* Option c1 */
for(list<int>const_iterator i = l.begin(); ...) { } /* Option c2 */
for(const int& i : list) { ... } /* Option c3 */
for(int& i : list) { ... } /* Option c4 */
/* Access by reference/by value: */
for(auto&& i : list) { ... } /* Option c5 */
for(auto i : list) { ... } /* Option c6 */
d)循环退出条件:
/* Option d1: end is defined within the start condition or outside the loop. */
for(auto i = l.begin(), end = l.end(); i != end; ++i) { ... }
/* Option d2: end is defined in the continue condition. */
for(auto i = l.begin(); i != l.end(); ++i) { ... }
也许大多数都是相同的,也许一个选项或另一个选项的选择只对给定的循环体有意义,但我想知道允许这么多可能的方法编程相同行为的目的是什么。
答案 0 :(得分:4)
是否始终支持自动?
仅从C ++ 11开始,但基于范围的循环也是如此,所以如果你可以依赖一个,那么你应该被允许依赖另一个。
除了代码可读性问题之外,为什么程序员更喜欢显式声明?
您可以使用显式类型隐式转换为其他类型。
紧凑形式与扩展循环形式
你可以通过显式使用迭代器(你称之为“扩展”)来做更多的事情,而不是使用基于范围的循环(你称之为“紧凑”)。但是,如果您只想迭代元素范围一次,那么基于范围的循环具有更简单的语法。这就是它被引入语言的原因。
为什么/何时更喜欢在循环体中使用引用[...]?
当一个人无法复制或想要避免复制迭代元素时。
为什么/何时更喜欢在循环体中有一个常数?
当一个人只有const访问范围,或者想表达他们不打算修改对象时。
循环的退出条件
如果循环中的结束指针无效,则只有d2
是正确的。
如果结束迭代器是不变的,那么d1
由于将函数调用带出循环而稍微提高效率。
如果编译器可以看到T::end()
的定义,那么可以通过将d2
转换为d1
进行优化。
在任何情况下,单个函数调用的开销通常可以忽略不计,除非循环体本身很简单。
我想知道允许编程相同行为的许多可能方法的目的是什么。
确实可以使用goto
实现所有循环结构。
那么,为什么c ++会允许任何其他循环结构呢?引入了for
,while
等,使程序更易于理解,更具可读性。好的,那么当goto
更容易理解时,为什么不摆脱for
?这是因为for
无法完成goto
所能做的所有事情。 goto
更为通用。
相同的推理适用于这种新的基于范围的循环。它比更通用的循环结构更容易推理,因此是有用的补充。但是一般结构仍然可以用于基于范围的循环不可能的用途。此外,删除一般for
结构会破坏语言的向后兼容性,这是不可取的。
答案 1 :(得分:3)
所有形式都是替代品,与其他形式相比,每种形式都有优点和缺点。
1)当你想要一个不同于auto
的类型可能推断时,首选显式声明循环变量。如果有必要在C ++ 11之前保持与C ++实现的兼容性,那也是强制性的(是的,有必要的实际实际情况 - 改变编译器需要付出代价,就像维护成本一样旧的)。
2)&#34; compact&#34;如果需求不是在一个范围内的所有元素上顺序迭代,那么form(更准确地说,基于范围的循环)是不合适的。例如,如果循环遍历每个第二个元素,如果循环体由于某种原因调整容器大小(使迭代器无效)。
3)const
限定符表示意图是循环不会更改容器的元素。这对于让编译器诊断循环(可能)导致元素意外更改的问题非常有用。例如呼叫非const
成员。在不使用const
限定符的情况下,有很多情况下这些问题是很难找到的错误。
4)如果循环体以任何方式调整容器大小(因为它使迭代器无效),则在开始条件内定义结束条件会导致未定义的行为。在每次循环迭代中重新计算结束条件可以防止此类问题。
具有如此多的不同编写循环方式的目的是程序员的便利性。根据程序员的尝试,不同的技术可能是合适的。
权衡是有时很难决定最合适的&#34;循环形式。
答案 2 :(得分:2)
基于范围的for(:)
循环在C ++中非常常见,根据for(;;)
循环定义。在C ++ 17之前,它与选项 d1 基本相同,注意到迭代器变量在客户端代码中是不可见的。
请注意,基于范围的迭代遍历元素,而不是有效的迭代器。你可以根据迭代器制作一个范围,但它需要一些胶水代码。
修改了基于C ++ 17范围的for(:)
,以便end
允许begin
具有与auto
不同的类型。这对于sentinal技术很有用,但在这一点上并没有那么重要。
const
总是像它一样工作。除了表达模板的幻想,它总是以人们可以轻易理解的方式工作。使用它可以使类型更难以解决,但有时你不在乎,有时你只是不必要地重复这种类型。如果搞砸了,不使用它会导致令人惊讶的类型不匹配。
引用是别名。值是副本。如果ypu想要遍历副本,你可以。如果要将别名迭代到容器的内容,则可以。
同样,您可以拥有容器内容的const
或非const
视图。你可以选择,这取决于你需要什么或想要什么。
偏向const
和值可以使代码更轻松地为程序员和编译器解码。然而,复杂结构的不必要副本可能很昂贵,end
可以抑制移动和隐式移动以及其他突变效率技巧。
如果你缓存for(:)
(d1 vs d2),它有时会稍快一些。但通常不值得注意,而且是噪音。理论上,如果容器发生变化,非缓存端可以更好地工作,但是在迭代时更改容器通常是疯狂的,并且需要修改advance子句以及终止。 auto&&
循环缓存结束,因为噪声参数消失了。
begin
推导出变量的转发引用:它可以是左值引用,也可以是右值引用到临时。这意味着&#34;我不在乎,不要复制任何东西,但让我使用它&#34;。参考生命周期扩展通常会使悬空引用成为问题,只要您绑定的数据源不会搞砸。
您缺少的另一件大事是非会员for(:)
。这允许访问C风格的数组,就好像它们在范围内一样,并且如果以正确的方式完成,则可以轻松地向第三方范围添加范围支持,因此 class UserResource(ModelResource):
class Meta:
queryset = CustomUser.objects.all()
resource_name = 'user'
allowed_methods = ['get','post']
filtering = {"id": ALL}
excludes = ['is_staff','password','is_superuser','id','is_active','date_joined']
authentication = BasicAuthentication()
也适用于它们。
答案 3 :(得分:1)
[a]是否始终支持
auto
?除了代码可读性问题,为什么程序员更喜欢显式声明?
几乎总是肯定的,如果他们想要在LHS上执行从RHS类型到非相同类型的转换,他们会更喜欢显式输入......或者只是不喜欢隐式的东西。
b)紧凑形式与扩展循环形式
这里有问题吗?当然,不同之处在于您不必取消引用范围内的迭代器 - for
。
如果您的算法要求您使用迭代器和/或使其无效的操作(例如插入,擦除),那么您需要使用迭代器,因此请使用&#39; old&#39; /&#39;手册&# 39;语法。
但如果你不这样做,那么范围 - for
可以避免因为你做这件事而不得不取消引用迭代器的麻烦。
[c]为什么/何时更喜欢在循环体中有引用或常量?
引用或常量之间的选择,就像其他地方一样 - 特别是包括几乎相同的函数参数选择 - 取决于
&
const &
d)循环退出条件:
第二个变体缓存end()
迭代器,即
end()
迭代器答案 4 :(得分:1)
a)auto
vs明确声明:
auto
,因为所有类型在编译时都是已知的。
显式声明类型的最重要原因是代码可读性,因为它可以在以后读取代码时更容易找出类型。此外,如果存在隐式转换,则明确声明类型允许您将项目转换为其他类型。
b)紧凑形式与扩展循环形式:
两者完全相同。你应该尽可能使用紧凑的形式。
c)常数,非常数/访问类型:
您希望使用const
引用而不是可变引用的原因是 const correctness ,这是一种保护机制,可以防止您在不应该使用时改变对象。特别是,如果您只对容器本身有const
引用,则只能在循环体中使用const
引用或值的副本。
一个常见的建议是尽可能使用const
,至少使用方法参数。如果方法接收到对象的const
引用,则可以放心该方法不会改变对象(除非某处有const_cast
)。
按值访问项目是最短的选项,即使使用const
容器也可以使用,如果项类型是小标量类型(bool
,{{1},则不会有任何性能损失}},int
等。这是你默认想要做的事情。
如果项目属于不可复制类型,或者您想要改变它们,则需要通过引用访问这些项目。此外,如果它们属于大类类型,则通过引用访问项目会更快。
d)循环退出条件:
选项double
只调用d2
一次,理论上稍快一些。但是,实际上l.end()
是一种非常快速的方法。选项end()
几乎总是首选,因为它更短。
允许多种方式的目的
例如,考虑循环。 C ++是从C演变而来的,它支持C类循环,目的是熟悉C程序员并向后兼容现有的C代码。基于范围的for循环只是旧循环方法的语法糖。没有理由不允许使用旧的循环语法:此外,禁用它会很成问题,因为在C ++ 11之前的循环中使用的语言结构在其他地方仍然有用。旧的d1
语法对循环数字之类的内容非常有用:
for
同样,for (int i = 0; i < 5; ++i)
std::cout << i;
和begin()
方法返回迭代器,例如,传递给end()
中的标准算法。
真的,C ++委员会没有任何合理的方法来防止旧式循环,也没有删除语言中的有用功能或制作非常奇怪的特殊情况(例如禁止<algorithm>
或begin()
在end()
语句中调用。他们也没有任何理由这样做。
其他情况类似:有多种方法可以做同样的事情,因为这只是语言特征的交互方式,没有理由试图阻止这种情况。