这是一个已知的C ++ 11循环的陷阱?

让我们想象一下,我们有一个结构体,可以用一些成员函数来保存3个双打:

struct Vector { double x, y, z; // ... Vector &negate() { x = -x; y = -y; z = -z; return *this; } Vector &normalize() { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } // ... }; 

这简单一点,但我相信你也同意类似的代码在那里。 这些方法可以让您方便地链接,例如:

 Vector v = ...; v.normalize().negate(); 

甚至:

 Vector v = Vector{1., 2., 3.}.normalize().negate(); 

现在,如果我们提供了begin()和end()函数,我们可以在一个新的for循环中使用我们的Vector,比如循环遍历3个坐标x,y和z(毫无疑问,可以构造更多“有用”的例子通过用例如StringreplaceVector):

 Vector v = ...; for (double x : v) { ... } 

我们甚至可以做到:

 Vector v = ...; for (double x : v.normalize().negate()) { ... } 

并且:

 for (double x : Vector{1., 2., 3.}) { ... } 

但是,以下(在我看来)已经被打破:

 for (double x : Vector{1., 2., 3.}.normalize()) { ... } 

虽然它似乎是前两个用法的逻辑组合,但我认为这最后一个用法创造了一个悬而未决的参考,而前两个是完全正确的。

  • 这是正确的,广泛赞赏?
  • 以上哪部分是“坏”部分,应该避免?
  • 通过改变基于范围的for循环的定义来改进语言,使得在for-expression中构造的临时对象在循环的持续时间中存在?

这是正确的,广泛赞赏?

是的,你对事物的理解是正确的。

以上哪部分是“坏”部分,应该避免?

不好的部分是对从函数返回的临时值进行l值引用,并将其绑定到r值引用。 它和这个一样糟糕:

 auto &&t = Vector{1., 2., 3.}.normalize(); 

临时Vector{1., 2., 3.}的生命周期不能被扩展,因为编译器不知道来自normalize的返回值是否引用它。

通过改变基于范围的for循环的定义来改进语言,使得在for-expression中构造的临时对象在循环的持续时间中存在?

这与C ++的工作方式高度不一致。

是否可以防止使用临时expression式的链式expression式或者各种expression式的懒惰评估方法? 是。 但是这也需要特殊的编译器代码,以及为什么它不能与其他expression式结构一起工作。

更合理的解决方法是通知编译器一个函数的返回值总是一个引用,因此如果返回值绑定到一个临时扩展构造,那么它将扩展正确的临时值。 这是一个语言级别的解决scheme。

目前(如果编译器支持的话),你可以这样做,以便normalize 不能被临时调用:

 struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector &normalize() && = delete; }; 

这将导致Vector{1., 2., 3.}.normalize()给出编译错误,而v.normalize()将正常工作。 显然,你将无法做到这样的正确的事情:

 Vector t = Vector{1., 2., 3.}.normalize(); 

但是你也不能做错误的事情。

另外,正如评论中所build议的,你可以让右值引用版本返回一个值而不是引用:

 struct Vector { double x, y, z; // ... Vector &normalize() & { double s = 1./sqrt(x*x+y*y+z*z); x *= s; y *= s; z *= s; return *this; } Vector normalize() && { Vector ret = *this; ret.normalize(); return ret; } }; 

如果Vector是具有实际资源的types,则可以使用Vector ret = std::move(*this); 代替。 命名的返回值优化使得这在性能方面是合理的最佳。

for(double x:Vector {1,2,3。}。normalize()){…}

这不是语言的限制,而是代码的问题。 expression式Vector{1., 2., 3.}创build一个临时的,但是normalize函数返回一个左值引用 。 由于expression式是一个左值 ,因此编译器假定该对象将处于活动状态,但由于它是对临时对象的引用,因此在对完整expression式求值之后对象将死亡,因此您将留下一个悬挂引用。

现在,如果将devise更改为按值返回新对象,而不是对当前对象的引用,那么就没有问题,代码将按预期工作。

恕我直言,第二个例子已经有缺陷。 修改操作符返回*this在您提到的方式中很方便:它允许修饰符的链接。 它可以用于简单地交付修改的结果,但是这样做很容易被忽视,因此容易出错。 如果我看到类似的东西

 Vector v{1., 2., 3.}; auto foo = somefunction1(v, 17); auto bar = somefunction2(true, v, 2, foo); auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo)); 

我不会自动怀疑函数将v作为一个副作用。 当然,他们可以 ,但会混淆。 所以如果我要写这样的东西,我会确保v保持不变。 对于你的例子,我会添加免费的function

 auto normalized(Vector v) -> Vector {return v.normalize();} auto negated(Vector v) -> Vector {return v.negate();} 

然后写循环

 for( double x : negated(normalized(v)) ) { ... } 

 for( double x : normalized(Vector{1., 2., 3}) ) { ... } 

这是国际海事组织更好的可读性,它更安全。 当然,它需要一个额外的副本,但是对于堆分配的数据,这可能会在一个便宜的C ++ 11移动操作中完成。