이것이 C ++ 11 for 루프의 알려진 함정입니까?
일부 멤버 함수와 함께 3 개의 double을 보유하는 구조체가 있다고 가정 해 봅시다.
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를 사용할 수 있습니다. 예를 들어 x, y, z의 3 개 좌표를 반복 할 수 있습니다 (더 "유용한"예제를 작성할 수 있습니다. Vector를 예를 들어 String으로 대체하여) :
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()) { ... }
이전 두 가지 사용의 논리적 조합처럼 보이지만이 마지막 사용은 매달린 참조를 생성하고 이전 두 사용은 완전히 괜찮습니다.
- 이것이 정확하고 널리 평가됩니까?
- Which part of the above is the "bad" part, that should be avoided?
- Would the language be improved by changing the definition of the range-based for loop such that temporaries constructed in the for-expression exist for the duration of the loop?
Is this correct and Widely appreciated?
Yes, your understanding of things is correct.
Which part of the above is the "bad" part, that should be avoided?
The bad part is taking an l-value reference to a temporary returned from a function, and binding it to an r-value reference. It is just as bad as this:
auto &&t = Vector{1., 2., 3.}.normalize();
The temporary Vector{1., 2., 3.}
's lifetime cannot be extended because the compiler has no idea that the return value from normalize
references it.
Would the language be improved by changing the definition of the range-based for loop such that temporaries constructed in the for-expression exist for the duration of the loop?
That would be highly inconsistent with how C++ works.
Would it prevent certain gotchas made by people using chained expressions on temporaries or various lazy-evaluation methods for expressions? Yes. But it would also be require special-case compiler code, as well as be confusing as to why it doesn't work with other expression constructs.
A much more reasonable solution would be some way to inform the compiler that the return value of a function is always a reference to this
, and therefore if the return value is bound to a temporary-extending construct, then it would extend the correct temporary. That's a language-level solution though.
Presently (if the compiler supports it), you can make it so that normalize
cannot be called on a temporary:
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;
};
This will cause Vector{1., 2., 3.}.normalize()
to give a compile error, while v.normalize()
will work fine. Obviously you won't be able to do correct things like this:
Vector t = Vector{1., 2., 3.}.normalize();
But you also won't be able to do incorrect things.
Alternatively, as suggested in the comments, you can make the rvalue reference version return a value rather than a reference:
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;
}
};
If Vector
was a type with actual resources to move, you could use Vector ret = std::move(*this);
instead. Named return value optimization makes this reasonably optimal in terms of performance.
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
That is not a limitation of the language, but a problem with your code. The expression Vector{1., 2., 3.}
creates a temporary, but the normalize
function returns an lvalue-reference. Because the expression is an lvalue, the compiler assumes that the object will be alive, but because it is a reference to a temporary, the object dies after the full expression is evaluated, so you are left with a dangling reference.
Now, if you change your design to return a new object by value rather than a reference to the current object, then there would be no issue and the code would work as expected.
IMHO, the second example is already flawed. That the modifying operators return *this
is convenient in the way you mentioned: it allows chaining of modifiers. It can be used for simply handing on the result of the modification, but doing this is error-prone because it can easily be overlooked. If I see something like
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));
I wouldn't automatically suspect that the functions modify v
as a side-effect. Of course, they could, but it would be confusing. So if I was to write something like this, I would make sure that v
stays constant. For your example, I would add free functions
auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}
and then write the loops
for( double x : negated(normalized(v)) ) { ... }
and
for( double x : normalized(Vector{1., 2., 3}) ) { ... }
That is IMO better readable, and it's safer. Of course, it requires an extra copy, however for heap-allocated data this could likely be done in a cheap C++11 move operation.
참고URL : https://stackoverflow.com/questions/10593686/is-this-a-known-pitfall-of-c11-for-loops
'code' 카테고리의 다른 글
Chrome Dev Tool에 날짜 __proto__가 잘못된 날짜로 표시되는 이유는 무엇입니까? (0) | 2020.09.08 |
---|---|
Gmail 용 HTML 이메일 스타일링 (0) | 2020.09.08 |
CMD.exe 교체 (0) | 2020.09.08 |
obj 폴더는 무엇을 위해 생성됩니까? (0) | 2020.09.08 |
JQuery에 권장되는 JavaScript HTML 템플릿 라이브러리? (0) | 2020.09.08 |