이것이 C ++ 11 for 루프의 알려진 함정입니까?


89

일부 멤버 함수와 함께 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()) { ... }

이전 두 가지 사용의 논리적 조합처럼 보이지만이 마지막 사용은 매달린 참조를 생성하고 이전 두 가지 사용은 완전히 괜찮습니다.

  • 이것이 정확하고 널리 평가됩니까?
  • 위의 "나쁜"부분은 피해야하는 부분입니까?
  • for-expression에 구성된 임시가 루프 기간 동안 존재하도록 범위 기반 for 루프의 정의를 변경하여 언어를 개선 할 수 있습니까?

어떤 이유로 나는 전에 물어 보았던 매우 유사한 질문을 기억하고 있지만 그것이 무엇인지 잊어 버렸습니다.
Pubby

나는 이것이 언어 결함이라고 생각합니다. 임시의 수명은 for 루프의 전체 본문으로 확장되는 것이 아니라 for 루프의 설정에만 적용됩니다. 문제가되는 것은 범위 구문 만이 아니라 고전적인 구문도 마찬가지입니다. 제 생각에 init 문에서 임시의 수명은 루프의 전체 수명 동안 연장되어야합니다.
edA-qa mort-ora-y

1
@ edA-qamort-ora-y : 여기에 약간의 언어 결함이 숨어 있다는 데 동의하는 경향이 있지만, 임시를 참조에 직접 바인딩 할 때마다 수명 연장이 암시 적으로 발생한다는 사실에 특히 동의합니다. 다른 상황-이것은 일시적인 수명의 근본적인 문제에 대한 반쯤 구워진 해결책처럼 보이지만 더 나은 해결책이 무엇인지 분명하지 않다는 것은 아닙니다. 임시를 구성 할 때 명시적인 '수명 연장'구문 일 수 있습니다.이 구문은 현재 블록이 끝날 때까지 지속됩니다. 어떻게 생각하십니까?
ndkrempel

@ edA-qamort-ora-y : ... 이것은 임시를 참조에 바인딩하는 것과 동일하지만 독자에게 '수명 연장'이 인라인 (표현식에서 , 별도의 선언이 필요하지 않음), 임시 이름을 지정할 필요가 없습니다.
ndkrempel

답변:


64

이것이 정확하고 널리 평가됩니까?

예, 사물에 대한 이해가 정확합니다.

위의 "나쁜"부분은 피해야하는 부분입니까?

나쁜 부분은 함수에서 반환 된 임시에 대한 l- 값 참조를 가져 와서 r- 값 참조에 바인딩하는 것입니다. 다음과 같이 나쁩니다.

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

임시 Vector{1., 2., 3.}의 수명은 컴파일러가 반환 값이 normalize참조하는 것을 알지 못하기 때문에 연장 할 수 없습니다 .

for-expression에 구성된 임시가 루프 기간 동안 존재하도록 범위 기반 for 루프의 정의를 변경하여 언어를 개선 할 수 있습니까?

그것은 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;
};

이로 인해 Vector{1., 2., 3.}.normalize()컴파일 오류가 발생하지만 v.normalize()정상적으로 작동합니다. 분명히 다음과 같은 올바른 작업을 수행 할 수 없습니다.

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

그러나 또한 잘못된 일을 할 수 없습니다.

또는 주석에서 제안한대로 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;
  }
};

Vector이동할 실제 자원이있는 유형 이라면 Vector ret = std::move(*this);대신 사용할 수 있습니다 . 명명 된 반환 값 최적화는 성능 측면에서이를 합리적으로 최적화합니다.


1
이를 "잘못"으로 만들 수있는 것은 새로운 for 루프가 참조 바인딩이 내부적으로 진행되고 있다는 사실을 구문 적으로 숨기고 있다는 것입니다. 즉, 위의 "나쁜"예제보다 훨씬 덜 노골적입니다. 그렇기 때문에 새로운 for 루프에 대해 추가 수명 연장 규칙을 제안하는 것이 타당 해 보였습니다.
ndkrempel

1
@ndkrempel : 예,하지만이 문제를 해결하기 위해 언어 기능을 제안하려는 경우 (최소한 2017 년까지 기다려야 함) 더 포괄적 인 경우 모든 곳 에서 일시적인 확장 문제를 해결할 수있는 기능을 선호합니다 .
Nicol Bolas

3
+1. 마지막 접근 방식에서는 deletervalue를 반환하는 대체 작업을 제공 할 수 있습니다. Vector normalize() && { normalize(); return std::move(*this); }( normalize함수 내부에 대한 호출 이 lvalue 오버로드로 전달되지만 누군가 확인해야한다고 생각합니다. :)
David Rodríguez-dribeas

3
나는 이것을 본 적이 없다 &/ &&방법의 자격. 이것은 C ++ 11에서 가져온 것입니까, 아니면 일부 (아마도 널리 퍼져있는) 독점 컴파일러 확장입니까? interresting 가능성을 제공합니다.
Christian Rau

1
@ChristianRau : C ++ 11의 새로운 기능이며, 어떤 의미에서 "this"를 한정한다는 점에서 비 정적 멤버 함수의 C ++ 03 "const"및 "volatile"한정과 유사합니다. 그러나 g ++ 4.7.0은이를 지원하지 않습니다.
ndkrempel

25

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

이는 언어의 제한이 아니라 코드 문제입니다. 표현식 Vector{1., 2., 3.}은 임시를 생성하지만 normalize함수는 lvalue-reference를 반환합니다 . 표현식이 lvalue 이므로 컴파일러는 객체가 살아있을 것이라고 가정하지만 임시에 대한 참조이므로 전체 표현식이 평가 된 후에 객체가 죽으므로 댕글 링 참조가 남습니다.

이제 현재 개체에 대한 참조가 아닌 값으로 새 개체를 반환하도록 디자인을 변경하면 문제가 없으며 코드가 예상대로 작동합니다.


1
싶은 const기준이 경우에 개체의 수명을 연장?
데이비드 스톤

5
normalize()기존 객체에 대한 돌연변이 함수로서의 명확하게 원하는 의미를 깨뜨릴 것입니다 . 따라서 질문. 반복의 특정 목적을 위해 사용될 때 임시가 "연장 된 수명"을 가지며 그렇지 않은 경우는 혼란스러운 잘못된 기능이라고 생각합니다.
앤디 로스

2
@AndyRoss : 왜? 상관 은 R 값 기준 (또는 바인딩은 임시 const&)의 수명이 연장되었다.
Nicol Bolas

2
@ndkrempel : 그래도 범위 기반 for 루프의 제한은 아니지만 참조에 바인딩하면 동일한 문제가 발생합니다 : Vector & r = Vector{1.,2.,3.}.normalize();. 당신의 디자인에는 그 한계가 있으며, 이는 값으로 반환 할 의향이 있거나 (많은 상황에서 의미가있을 수 있으며, rvalue-referencesmove에서 더 그렇다 ), 그렇지 않으면 문제를 처리해야 함을 의미합니다. 호출 : 적절한 변수를 만든 다음 for 루프에서 사용합니다. 또한 표현식 Vector v = Vector{1., 2., 3.}.normalize().negate();두 개의 객체를 생성 합니다.
David Rodríguez-dribeas

1
@ DavidRodríguez-dribeas : const-reference에 바인딩하는 문제는 이것입니다 : T const& f(T const&);완전히 괜찮습니다. T const& t = f(T());완전히 괜찮습니다. 그리고 다른 TU에서 당신은 그것을 발견 T const& f(T const& t) { return t; }하고 당신은 울고 ... operator+가치에 대해 작동 한다면 더 안전합니다 ; 그런 다음 컴파일러는 복사를 최적화 할 수 있지만 (Want Speed? Pass by Values) 보너스입니다. 내가 허용 할 임시 바인딩은 r- 값 참조에 바인딩하는 것이지만 함수는 안전을 위해 값을 반환하고 Copy Elision / Move Semantics에 의존해야합니다.
Matthieu M.

4

IMHO, 두 번째 예는 이미 결함이 있습니다. 수정 연산자가 반환하는 *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일정하게 유지 되도록 할 것 입니다. 예를 들어 무료 기능을 추가합니다.

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}) ) { ... }

그것은 IMO가 더 읽기 쉽고 더 안전합니다. 물론 추가 복사본이 필요하지만 힙 할당 데이터의 경우 저렴한 C ++ 11 이동 작업으로 수행 할 수 있습니다.


감사. 평소와 같이 많은 선택이 있습니다. 귀하의 제안이 실행되지 않을 수있는 한 가지 상황은 예를 들어 벡터가 1000 배의 배열 (힙 할당되지 않음) 인 경우입니다. 효율성, 사용 용이성 및 사용 안전의 절충안.
ndkrempel

2
예,하지만 어쨌든 스택에 크기가> ≈100 인 구조를 갖는 것은 거의 유용하지 않습니다.
leftaroundabout
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.