std :: map의 키에서 리소스를 훔치는 것이 허용됩니까?


15

C ++에서는 나중에 더 이상 필요하지 않은 맵에서 리소스를 훔치는 것이 괜찮습니까? 더 정확하게 말하면 std::mapwith std::string키가 있고를 map사용하여 s 키 의 리소스를 훔쳐서 벡터를 구성하려고 한다고 가정 합니다 std::move. 키에 대한 이러한 쓰기 액세스는 내부 데이터 구조 (키 순서)를 손상 map시키지만 나중에 사용하지는 않습니다.

질문 : 문제 없이이 작업을 수행 할 수 있습니까 ? 아니면 의도하지 않은 map방식으로 액세스했기 때문에 예를 들어 소멸자에서 예기치 않은 버그 std::map가 발생합니까?

예제 프로그램은 다음과 같습니다.

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

출력을 생성합니다.

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

편집 : 큰지도의 전체 내용에 대해이 작업을 수행 하고이 리소스 도용에 의해 동적 할당을 저장하고 싶습니다.


3
이 "스털링"의 목적은 무엇입니까? 지도에서 요소를 제거하려면? 그렇다면 왜 그렇게하지 않는 것입니까 (지도에서 요소를 지우십시오)? 또한 const값을 수정하는 것은 항상 UB입니다.
일부 프로그래머 친구

분명히 심각한 버그로 이어질 것입니다!
rezaebrh

1
귀하의 질문에 대한 직접적인 대답은 아니지만, 벡터를 반환하지 않고 범위 또는 반복자 쌍을 반환하면 어떻게됩니까? 그것은 완전히 복사를 피할 것입니다. 어쨌든 최적화의 진행 상황을 추적하기위한 벤치 마크와 핫스팟을 찾기위한 프로파일 러가 필요합니다.
Ulrich Eckhardt

1
@ ALX23z이 문장에 대한 자료가 있습니까? 포인터를 복사하는 것이 전체 메모리 영역을 복사하는 것보다 비용이 얼마나 많이 드는지 상상할 수 없습니다.
Sebastian Hoffmann

1
@SebastianHoffmann은 최근 CppCon에서 언급되었지만 어느 대화에 대해 잘 모르겠습니다. 문제는 std::string짧은 문자열 최적화입니다. 포인터 교환뿐만 아니라 복사 및 이동에 대한 사소한 논리가 있으며 대부분의 시간 이동은 복사를 의미합니다. 어쨌든 통계적 차이는 작았으며 일반적으로 어떤 종류의 문자열 처리가 수행되는지에 따라 달라집니다.
ALX23z

답변:


18

사용하여 정의되지 않은 동작을 수행하고 있습니다. const_castconst변수 를 수정하는 데 있습니다. 그렇게하지 마십시오. 그 이유 const는지도가 키별로 정렬되어 있기 때문입니다. 따라서 키를 제자리에서 수정하면 맵이 구축 된 기본 가정이 깨집니다.

변수에서 const_cast제거 하는 데 사용해서는 안됩니다const 수정하는 데 .

그 존재는 C ++ (17)이 문제에 대한 솔루션을 말했다 std::mapextract기능 :

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

그리고하지 마십시오 using namespace std;. :)


고마워, 나는 이것을 시도 할 것이다! 그러나 내가 한 것처럼 할 수 없다고 확신합니까? 나는 map그 메소드를 호출하지 않으면 (내가하지 않는) 불만을 제기하지 않으며 소멸자에서 내부 순서가 중요하지 않을 수 있습니까?
phinz

2
지도 키가 생성 const됩니다. 돌연변이 const개체 것은 아무것도 실제로 나중에 그들을 액세스 여부, 인스턴트 UB입니다.
HTNW

이 방법에는 두 가지 문제가 있습니다. (1) 모든 요소를 ​​추출하고 싶을 때 키 (비효율적 조회)로 추출하고 싶지 않고 반복자로 추출하고 싶습니다. 나는 이것이 또한 가능하다는 것을 알았으므로 이것은 괜찮습니다. (2) 내가 틀렸지 만 모든 요소를 ​​추출하기 위해 (모든 추출에서 내부 트리 구조를 재조정하기 위해) 엄청난 오버 헤드가 있습니까?
phinz

2
@phinz 반복자를 인수로 사용할 때 cppreference에서 볼 수 있듯이 extract상수 복잡성이 상각되었습니다. 약간의 오버 헤드는 피할 수 없지만 중요하지 않을 수 있습니다. 여기에서 다루지 않은 특별한 요구 사항이있는 경우 map이러한 요구 사항을 충족하는 자체 요구 사항 을 구현해야 합니다. std용기는 일반적인 범용 application.They에 대한 의미는 특정 사용 사례에 최적화되어 있지 않습니다.
호두

@HTNW 키가 생성 const되었습니까? 이 경우 내 논증 이 잘못된 부분을 ​​알려주십시오 .
phinz

4

코드는 const객체 를 수정하려고 시도하여 다음 과 같이 정의되지 않은 동작을 갖습니다.druckermanly의 답변이 올바르게 지적 했으므로 습니다.

다른 답변 ( phinzDeuchie ) const은 맵에서 노드를 추출하여 생성 된 노드 핸들 const이 키에 액세스 할 수 없으므로 키를 객체 로 저장해서는 안된다고 주장합니다 . 이 추론은 처음에는 그럴듯 ​​해 보이지만, P0083R3extract 기능 은이 주제에 대해 다음과 같은 논증을 무효화하는 전용 섹션을 가지고 있습니다.

우려 사항

이 디자인에 대해 몇 가지 우려가 제기되었습니다. 우리는 여기서 그것들을 다룰 것입니다.

정의되지 않은 동작

이론적 관점에서이 제안의 가장 어려운 부분은 추출 된 요소가 const 키 유형을 유지한다는 사실입니다. 이것은 밖으로 이동하거나 변경하는 것을 방지합니다. 이를 해결하기 위해 접근 자 기능을 제공하여 노드 핸들이 보유한 요소의 키에 대한 비 const 액세스를 제공합니다. 이 함수는 컴파일러 최적화가있을 때 올바르게 작동하도록하기 위해 "매직"구현이 필요합니다. 이 작업을 수행하는 한 가지 방법은 pair<const key_type, mapped_type> 및 의 조합입니다 pair<key_type, mapped_type>. 이들 사이의 변환은 std::launder추출 및 재 삽입에서 사용되는 것과 유사한 기술을 사용하여 안전하게 수행 될 수 있습니다 .

우리는 이것이 기술적 또는 철학적 문제를 야기한다고 생각하지 않습니다. 표준 라이브러리가 존재하는 이유 중 하나는 클라이언트 ++ (예 : 휴대용 C에서 쓸 수있는 비 휴대용 마법의 코드를 작성하는 것입니다 <atomic>, <typeinfo>, <type_traits>, 등). 이것은 또 다른 예입니다. 컴파일러 공급 업체가이 마법을 구현하기 위해 필요한 것은 최적화 목적으로 통합에서 정의되지 않은 동작을 이용하지 않아야한다는 것입니다.

이는 클라이언트에 이러한 기능이 사용되는 경우 std::pairpair<const key_type, mapped_type>다른 레이아웃 을 갖는 특수화 될 수없는 제한 사항을 부과합니다 pair<key_type, mapped_type>. 우리는 실제로 이것을하고 싶은 사람이 실제로 제로라고 생각하며 공식적인 말로 우리는이 쌍의 전문화를 제한합니다.

점을 유의 멤버 함수는 트릭 필요한 유일한 장소이고, 용기 또는 쌍 변경을 할 필요가있다.

(강조 광산)


이것은 실제로 원래 질문에 대한 답변의 일부이지만 하나만 수락 할 수 있습니다.
phinz

0

나는 const_cast이 경우 수정이 정의되지 않은 동작을 초래 한다고 생각하지 않지만 이 인수가 올바른지 의견을 말하십시오.

답변은

즉, 원래 const 객체를 수정하면 UB가 표시되고 그렇지 않으면 UB가 표시됩니다.

따라서 객체 가 const 객체로 생성되지 않은 v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));경우에만 질문 의 줄 이 UB 로 연결되지 않습니다. 이제 상태에 대한 참조는stringp.firstextract

노드를 추출하면 반복자가 추출 된 요소에 대해 무효화됩니다. 추출 된 요소에 대한 포인터와 참조는 유효하지만 노드 핸들이 요소를 소유 한 동안에는 사용할 수 없습니다. 요소가 컨테이너에 삽입되면 사용할 수 있습니다.

나는 그래서 만약 에 해당하는 , 그 저장 위치에 거주하고 있습니다. 그러나 추출 후 druckermanly의 답변 코드에서 와 같이 리소스 를 제거 할 수 있습니다 . 이 수단 따라서 또한 객체가 되었다 하지 원래 const를 대상으로 만들었습니다.extractnode_handleppmoveppstringp.first

따라서, 나는 map키 의 수정이 UB로 이어지지 않으며 Deuchie의 대답 에서 나온 것처럼, 현재 손상된 트리 구조 (현재 여러 개의 동일한 빈 문자열 키)도 map소멸자에 문제를 일으키지 않는 것으로 보입니다 . 따라서 문제의 코드는 적어도 extract메소드가 존재 하는 C ++ 17에서 유효하게 작동해야 합니다 (포인터가 남아있는 포인터에 대한 설명).

최신 정보

나는 지금이 대답이 잘못되었다는 견해를 가지고 있습니다. 다른 답변에서 참조했기 때문에 삭제하지 않습니다.


1
죄송합니다, phinz, 귀하의 답변이 잘못되었습니다. 나는 이것을 설명하기 위해 답을 쓸 것이다-그것은 노동 조합과 관련이있다 std::launder.
LF

-1

편집 :이 답변이 잘못되었습니다. 친절한 의견이 실수를 지적했지만 다른 답변에서 참조되었으므로 삭제하지 않습니다.

@druckermanly는 첫 번째 질문에 대답했습니다. 키를 map강제로 변경하면 map내부 데이터 구조가 구축 되는 순서가 손상됩니다 (Red-Black tree). 그러나이 extract방법은 맵에서 키를 옮긴 다음 삭제하여 두 가지 작업을 수행하므로이 방법 을 사용하는 것이 안전합니다 . 따라서 맵 순서에 전혀 영향을 미치지 않습니다.

해체 할 때 문제가 발생하는지 여부에 관한 다른 질문은 문제가 아닙니다. 맵이 해체되면 각 요소 (mapped_types 등)의 해체자를 호출하고 move클래스를 이동 한 후 클래스를 안전하게 해체 할 수 있도록합니다. 걱정하지 마십시오. 간단히 말해서, move"moved"클래스에 새로운 값을 삭제하거나 재 할당하는 것이 안전하도록하는 작업입니다 . 특히 string, move메소드는 char 포인터를로 설정할 수 nullptr있으므로 원래 클래스의 소멸자가 호출되었을 때 이동 한 실제 데이터는 삭제하지 않습니다.


의견은 내가 간과하고있는 요점을 상기시켰다. 기본적으로 그는 옳았지만 내가 완전히 동의하지 않은 한 가지 const_cast가있다. 아마도 UB가 아닐 것이다. const컴파일러와 우리 사이의 약속입니다. 으로 명시 개체 const아직 가진 것과 동일한 목적이다 const바이너리 형태로 해당 유형의 관점에서 표현. const가 캐스트 될 때 마치 일반 가변 클래스 인 것처럼 동작해야합니다. 와 관련하여 move사용하려면 UB &대신 을 전달해야 const &하므로 UB가 아니라는 것을 알면 단순히 약속을 깨고 const데이터를 멀리합니다.

또한 MSVC 14.24.28314와 Clang 9.0.0을 각각 사용하여 두 가지 실험을 수행했으며 동일한 결과를 얻었습니다.

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

산출:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

우리는 이제 문자열 '2'가 비어 있음을 알 수 있습니다. 그러나 빈 문자열을 앞에 배치해야하기 때문에 맵의 순서가 깨졌습니다. 지도의 특정 노드를 삽입, 찾기 또는 삭제하려고하면 파국이 발생할 수 있습니다.

어쨌든 공용 인터페이스를 우회하는 클래스의 내부 데이터를 조작하는 것은 결코 좋은 생각이 아니라는 데 동의 할 수 있습니다. 는 find, insert, remove등 기능과 내부 데이터 구조의 질서에 자신의 정확성을 의지, 그리고 우리가 내부를 엿의 생각을 멀리해야하는 이유입니다.


2
"해체시 문제를 일으킬 지 여부는 문제가되지 않습니다." 정의되지 않은 동작 ( const값 변경 )이 더 일찍 발생 했기 때문에 기술적으로 정확합니다 . 그러나 인수 "를 move개최하지 않습니다이 이동 된 후가 안전하다는 것을 [기능] 보장하지만 클래스 [의 오브젝트]를 해체하는"당신은 안전하게 이동할 수없는 const그 수정을 필요로하기 때문에, 개체 / 참조하는 const방지합니다. 을 사용하여 해당 제한을 해결하려고 시도 할 수 const_cast있지만 그 시점에서 UB가 아닌 경우 구현 특정 동작에 가장 깊이 들어가는 것이 가장 좋습니다.
hoffmale

@hoffmale 감사합니다, 나는 간과하고 큰 실수를했다. 당신이 그것을 지적하지 않은 경우, 내 대답은 다른 사람을 오도 할 수 있습니다. 실제로 move함수 &대신을 사용 한다고 말해야 const&하므로 키를 맵 밖으로 이동한다고 주장하면을 사용해야합니다 const_cast.
Deuchie

1
"const로 표시된 객체는 바이너리 형식의 표현 및 유형 측면에서 const가없는 객체와 여전히 동일한 객체입니다."const 객체는 읽기 전용 메모리에 넣을 수 있습니다. 또한 const를 사용하면 컴파일러가 여러 읽기에 대한 코드를 생성하는 대신 코드에 대해 추론하고 값을 캐시 할 수 있습니다 (성능 측면에서 큰 차이를 만들 수 있음). 따라서 UB로 인한 const_cast끔찍한 일이 될 것입니다. 대부분의 경우 작동하지만 미묘한 방식으로 코드를 손상시킵니다.
LF

그러나 여기서는 객체가 읽기 전용 메모리에 저장되지 않는다고 확신 할 수 있다고 생각합니다. 왜냐하면 이후 extract에 동일한 객체에서 이동할 수 있기 때문입니다 . (내 답변 참조)
phinz

1
답변의 분석이 잘못되었습니다. 나는 이것을 설명하기위한 답을 쓸 것이다.-노동 조합과 관련이있다.std::launder
LF
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.