这是C++ 11 for循环的已知缺陷吗?

ndk*_*pel 89 c++ foreach for-loop language-lawyer 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;
  }
  // ...
};
Run Code Online (Sandbox Code Playgroud)

这有点简单,但我相信你同意类似的代码.这些方法可以方便地链接,例如:

Vector v = ...;
v.normalize().negate();
Run Code Online (Sandbox Code Playgroud)

甚至:

Vector v = Vector{1., 2., 3.}.normalize().negate();
Run Code Online (Sandbox Code Playgroud)

现在,如果我们提供了begin()和end()函数,我们可以在new-style for循环中使用Vector,比如循环遍历3个坐标x,y和z(你无疑可以构造更多"有用"的例子通过用例如String替换Vector):

Vector v = ...;
for (double x : v) { ... }
Run Code Online (Sandbox Code Playgroud)

我们甚至可以这样做:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }
Run Code Online (Sandbox Code Playgroud)

并且:

for (double x : Vector{1., 2., 3.}) { ... }
Run Code Online (Sandbox Code Playgroud)

但是,以下(在我看来)打破了:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Run Code Online (Sandbox Code Playgroud)

虽然它似乎是前两个用法的逻辑组合,但我认为最后一个用法创建了一个悬空引用,而前两个用法完全没问题.

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

Nic*_*las 64

这是正确的,并广泛赞赏?

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

以上哪部分是"坏"部分,应该避免哪些?

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

auto &&t = Vector{1., 2., 3.}.normalize();
Run Code Online (Sandbox Code Playgroud)

临时Vector{1., 2., 3.}的生命周期无法扩展,因为编译器不知道normalize引用它的返回值.

是否可以通过更改基于范围的for循环的定义来改进语言,使得在for-expression中构造的临时数在循环的持续时间内存在?

这与C++的工作方式非常不一致.

它会阻止人们使用临时表达式或表达式的各种惰性评估方法制作某些陷阱吗?是.但它也需要特殊情况的编译器代码,并且混淆为什么它不能与其他表达式构造一起使用.

一个更合理的解决方案是通知编译器函数的返回值始终是this对它的引用,并且因此如果返回值绑定到临时扩展构造,那么它将扩展正确的临时.这是一个语言级的解决方案.

目前(如果编译器支持它),您可以使它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;
};
Run Code Online (Sandbox Code Playgroud)

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

Vector t = Vector{1., 2., 3.}.normalize();
Run Code Online (Sandbox Code Playgroud)

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

或者,如注释中所建议,您可以使rvalue引用版本返回值而不是引用:

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;
  }
};
Run Code Online (Sandbox Code Playgroud)

如果Vector是具有要移动的实际资源的类型,则可以使用Vector ret = std::move(*this);.命名返回值优化使其在性能方面合理地最佳.

  • +1.在最后一种方法中,您可以提供一个返回右值的替代操作,而不是`delete`:`Vector normalize()&& {normalize(); return std :: move(*this); }`(我相信函数内部对`normalize`的调用将调度到左值超载,但是有人应该检查它:) (3认同)
  • 我从未见过这种方法的`&`/`&&`资格.这是来自C++ 11还是这个(可能是普遍的)专有编译器扩展.提供有趣的可能性. (3认同)

Dav*_*eas 25

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

这不是语言的限制,而是代码的问题.表达式Vector{1., 2., 3.}创建一个临时表达式,但该normalize函数返回一个左值引用.因为表达式是左值,所以编译器假定该对象是活动的,但由于它是对临时对象的引用,因此在评估完整表达式后对象会死亡,因此您将留下悬空引用.

现在,如果您更改设计以按值返回新对象而不是对当前对象的引用,则不会出现问题,代码将按预期工作.

  • 这将打破`normalize()`作为现有对象上的变异函数的明确需要的语义.这样的问题.当用于迭代的特定目的时,临时具有"延长寿命",而不是其他情况,我认为这是一个令人困惑的错误. (5认同)
  • @AndyRoss:为什么?*任何*临时绑定到r值引用(或`const&`)都会延长其生命周期. (2认同)
  • @ndkrempel:尽管如此,不是基于范围的for循环的限制,如果你绑定到一个引用也会出现同样的问题:`Vector&r = Vector {1.,2.,3.}.normalize();` .你的设计有这个限制,这意味着你要么愿意按价值返回(这在许多情况下都有意义,而对于*rvalue-references*和*move*更是如此),否则你需要处理问题在调用地点:创建一个适当的变量,然后在for循环中使用它.还要注意表达式'Vector v = Vector {1.,2.,3.}.normalize().negate();`created*two*objects ... (2认同)