이것이 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-references 및 move에서 더 그렇다 ), 그렇지 않으면 문제를 처리해야 함을 의미합니다. 호출 : 적절한 변수를 만든 다음 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.