C ++ 11에서 unorder_map이 삽입 한 것을 파괴하는 것이 C ++ 표준위원회의 의도입니까?


114

나는 unorder_map :: insert ()가 삽입 한 변수를 파괴하는 매우 이상한 버그를 추적하는 내 인생의 3 일을 잃었습니다. 이 매우 명확하지 않은 동작은 최신 컴파일러에서만 발생합니다. clang 3.2-3.4 및 GCC 4.8 이이 "기능"을 보여주는 유일한 컴파일러 라는 것을 발견했습니다 .

다음은 문제를 보여주는 주요 코드베이스의 일부 축소 된 코드입니다.

#include <memory>
#include <unordered_map>
#include <iostream>

int main(void)
{
  std::unordered_map<int, std::shared_ptr<int>> map;
  auto a(std::make_pair(5, std::make_shared<int>(5)));
  std::cout << "a.second is " << a.second.get() << std::endl;
  map.insert(a); // Note we are NOT doing insert(std::move(a))
  std::cout << "a.second is now " << a.second.get() << std::endl;
  return 0;
}

나는 아마도 대부분의 C ++ 프로그래머와 마찬가지로 다음과 같은 출력을 기대할 것입니다.

a.second is 0x8c14048
a.second is now 0x8c14048

그러나 clang 3.2-3.4 및 GCC 4.8을 사용하면 대신 다음과 같이 표시됩니다.

a.second is 0xe03088
a.second is now 0

http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ 에서 unordered_map :: insert ()에 대한 문서를 자세히 살펴보기 전까지는 말이되지 않을 수 있습니다 . 여기서 오버로드 2 번은 다음과 같습니다.

template <class P> pair<iterator,bool> insert ( P&& val );

탐욕스러운 범용 참조 이동 오버로드이며 다른 오버로드와 일치하지 않는 것을 소비하고 이를 value_type 으로 구성 합니다. 그렇다면 위의 코드가 아마도 대부분의 예상대로 unorder_map :: value_type 오버로드가 아닌이 오버로드를 선택한 이유는 무엇입니까?

대답은 얼굴에서 당신을 쳐다보고 : unordered_map도 :: VALUE_TYPE는 한 쌍의 < CONST INT, 표준 : : shared_ptr의>와 컴파일러가 제대로 그 한 쌍의 <생각 INT , 표준 : : shared_ptr의> 변환 될 수 없습니다. 따라서 컴파일러는 이동 범용 참조 오버로드를 선택 하고 프로그래머가 std :: move ()를 사용하지 않음 에도 불구하고 변수가 파괴되는 것이 괜찮다는 것을 나타내는 일반적인 규칙 임에도 불구하고 원본 을 파괴합니다. 따라서 동작을 파괴 삽입은 사실상 올바른 은 C ++ 11 표준에 따라, 세 컴파일러이었다 잘못된 .

이 버그를 진단하는 데 3 일이 걸린 이유를 이제 알 수 있습니다. unorder_map에 삽입되는 유형이 소스 코드 용어로 멀리 정의 된 typedef 인 대규모 코드베이스에서는 전혀 명확하지 않았으며 typedef가 value_type과 동일한 지 확인하는 일이 누구에게도 발생하지 않았습니다.

그래서 Stack Overflow에 대한 내 질문 :

  1. 왜 오래된 컴파일러는 새로운 컴파일러처럼 삽입 된 변수를 파괴하지 않습니까? 내 말은, GCC 4.7조차도 이것을 수행하지 않으며 꽤 표준을 준수합니다.

  2. 컴파일러를 업그레이드하면 작동하던 코드가 갑자기 작동을 멈추기 때문에이 문제가 널리 알려져 있습니까?

  3. C ++ 표준위원회가이 동작을 의도 했습니까?

  4. 더 나은 동작을 제공하기 위해 unorder_map :: insert ()를 수정하는 방법을 제안 하시겠습니까? 여기에 지원이 있으면이 행동을 WG21에 N 노트로 제출하고 더 나은 행동을 구현하도록 요청하기 때문에 이것을 묻습니다.


10
범용 참조를 사용한다고해서 삽입 된 값이 항상 이동 되는 것은 아닙니다. rvalue에 대해서만 그렇게 해야 합니다 a. 복사본을 만들어야합니다. 또한이 동작은 컴파일러가 아닌 stdlib에 전적으로 의존합니다.
Xeo

10
즉, 라이브러리의 구현에 버그가 보인다
데이비드 로드리게스 - dribeas

4
"따라서 삽입 파괴 동작은 C ++ 11 표준에 따라 실제로 정확하며 이전 컴파일러는 올바르지 않습니다." 미안하지만 틀렸어. C ++ 표준의 어떤 부분에서 그 아이디어를 얻었습니까? BTW cplusplus.com은 공식적이지 않습니다.
Ben Voigt 2014

1
내 시스템에서 이것을 재현 할 수 없으며 4.9.0 20131223 (experimental)각각 gcc 4.8.2를 사용하고 있습니다. 출력은 a.second is now 0x2074088 나를 위해 (또는 유사합니다).

47
이것은 GCC 버그 57619 이며, 4.8 시리즈의 회귀로 2013-06 년에 4.8.2로 수정되었습니다.
Casey

답변:


83

다른 사람들이 주석에서 지적했듯이 "보편적 인"생성자는 사실 항상 인수에서 벗어나지 않아야합니다. 인수가 실제로 rvalue이면 이동하고 lvalue이면 복사해야합니다.

항상 움직이는 동작은 libstdc ++의 버그이며, 이제 질문에 대한 의견에 따라 수정되었습니다. 궁금한 사람들을 위해 g ++-4.8 헤더를 살펴 보았습니다.

bits/stl_map.h, 598-603 행

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h, 365-370 행

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

후자는를 std::move사용해야 하는 위치를 잘못 사용 하고 있습니다 std::forward.


11
Clang은 기본적으로 GCC의 stdlib 인 libstdc ++를 사용합니다.
Xeo

나는 gcc 4.9를 사용하고 있으며 libstdc++-v3/include/bits/. 나는 똑같은 것을 보지 않는다. 알겠습니다 { return _M_h.insert(std::forward<_Pair>(__x)); }. 4.8에서는 다를 수 있지만 아직 확인하지 않았습니다.

네, 그래서 그들이 버그를 고친 것 같아요.
Brian

@Brian Nope, 방금 시스템 헤더를 확인했습니다. 같은 것. 4.8.2 btw.

내 것은 4.8.1이므로 둘 사이에 고정 된 것 같습니다.
Brian

20
template <class P> pair<iterator,bool> insert ( P&& val );

탐욕스러운 범용 참조 이동 오버로드이며 다른 오버로드와 일치하지 않는 것을 소비하고이를 value_type으로 구성합니다.

이것이 어떤 사람들은 보편적 인 참조 라고 부르지 만 실제로는 참조 축소 입니다. 인수가 귀하의 경우에, 좌변 유형이 pair<int,shared_ptr<int>>는 것 하지 를 rvalue 참조되는 인수 결과와는 안된다 그것에서 이동합니다.

그렇다면 위의 코드가 아마도 대부분의 예상대로 unorder_map :: value_type 오버로드가 아닌이 오버로드를 선택한 이유는 무엇입니까?

당신은 이전에 다른 많은 사람들 value_type과 마찬가지로 용기의 내용을 잘못 해석했기 때문 입니다. value_type의은 *map(주문 또는 순서가 있는지 여부)입니다 pair<const K, T>귀하의 경우 인 pair<const int, shared_ptr<int>>. 일치하지 않는 유형은 예상 할 수있는 과부하를 제거합니다.

iterator       insert(const_iterator hint, const value_type& obj);

나는 아직도이 새로운 기본형이 존재하는 이유를 이해하지 못한다. "보편적 참조"라는 말이 실제로는 특정한 것을 의미하지도 않고 실제로 "보편적"이 아닌 것에 대한 좋은 이름을 가지고 있지도 않다. 템플릿이 언어에 도입 된 이후로 C ++ 메타 언어 동작의 일부인 이전 축소 규칙과 C ++ 11 표준의 새 서명을 기억하는 것이 훨씬 좋습니다. 어쨌든이 보편적 인 참조에 대해 이야기하는 것의 이점은 무엇입니까? 이미 std::move아무 것도 움직이지 않는 것처럼 충분히 이상한 이름과 사물이 있습니다 .
user2485710 2014-02-03

2
@ user2485710 때때로, 무지는 행복하며 모든 참조 축소 및 템플릿 유형 추론 조정에 대한 포괄적 인 용어 "범용 참조"를 갖는 IMHO는 매우 직관적이지 std::forward않습니다. 또한 이러한 조정을 실제 작업을 수행하는 데 사용해야하는 요구 사항이 있습니다 . Scott Meyers 전달 (범용 참조 사용)에 대한 매우 간단한 규칙을 잘 설정했습니다.
Mark Garcia

1
내 생각에 "유니버설 참조"는 lvalue와 rvalue 모두에 바인딩 할 수있는 함수 템플릿 매개 변수를 선언하는 패턴입니다. "참조 축소"는 "범용 참조"상황 및 기타 컨텍스트에서 (추론되거나 지정된) 템플릿 매개 변수를 정의로 대체 할 때 발생합니다.
aschepler

2
@aschepler : 범용 참조 는 참조 축소의 하위 집합에 대한 멋진 이름입니다. 나는 무지가 행복 하다는 데 동의하고, 멋진 이름을 갖는 것이 더 쉽고 유행에 대해 이야기하고 행동을 전파하는 데 도움이 될 수 있다는 사실에 동의합니다 . 즉, Scott Meyers가 설명한 것 이외의 사건을 맞을 때까지 실제 규칙을 필요가없는 코너로 밀어 붙이기 때문에 나는 이름의 열렬한 팬이 아닙니다 .
David Rodríguez-dribeas 2014

무의미한 의미론, 그러나 내가 제안하려는 구별은 다음과 같습니다. 유니버설 참조는 함수 매개 변수를 deducible-template-parameter로 설계하고 선언 할 때 발생합니다 &&. 참조 축소는 컴파일러가 템플릿을 인스턴스화 할 때 발생합니다. 참조 축소는 범용 참조가 작동하는 이유이지만 내 두뇌는 두 용어를 동일한 영역에 두는 것을 좋아하지 않습니다.
aschepler
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.