C ++ : 스마트 포인터, 원시 포인터, 포인터가 없습니까? [닫은]


48

C ++로 게임을 개발하는 범위 내에서 포인터 사용과 관련하여 선호하는 패턴은 무엇입니까?

당신은 고려할 수 있습니다

  • 객체 소유권
  • 사용의 용이성
  • 복사 정책
  • 간접비
  • 순환 참조
  • 대상 플랫폼
  • 용기와 함께 사용

답변:


32

다양한 접근 방식을 시도한 결과 오늘 Google C ++ 스타일 가이드 와 일치합니다 .

실제로 포인터 의미가 필요한 경우 scoped_ptr이 좋습니다. STL 컨테이너가 오브젝트를 보유해야하는 경우와 같이 매우 특정한 조건에서만 std :: tr1 :: shared_ptr을 사용해야합니다. auto_ptr을 사용해서는 안됩니다. [...]

일반적으로, 우리는 명확한 객체 소유권을 가진 코드를 디자인하는 것을 선호합니다. 가장 명확한 객체 소유권은 포인터를 전혀 사용하지 않고 객체를 필드 또는 로컬 변수로 직접 사용하여 얻습니다. [..]

권장되지는 않지만 참조 횟수 포인터는 때때로 문제를 해결하는 가장 단순하고 가장 우아한 방법입니다.


14
현재 scoped_ptr 대신 std :: unique_ptr을 사용하고 싶을 수도 있습니다.
Klaim

24

또한 "강력한 소유권"이라는 생각의 기차를 따릅니다. 나는 "이 클래스는이 멤버를 소유하고있다"는 것이 적절할 때 명확하게 묘사하고 싶습니다.

나는 거의 사용하지 않습니다 shared_ptr. weak_ptr그렇게하면 가능한 한 참조를 늘리지 않고 객체의 핸들처럼 취급 할 수 있도록 최대한 자유롭게 사용 합니다.

나는 scoped_ptr모든 곳에서 사용 합니다. 명백한 소유권을 보여줍니다. 내가 멤버와 같은 객체를 만들지 않는 유일한 이유는 scoped_ptr에있는 경우 객체를 전달할 수 있기 때문입니다.

객체 목록이 필요하면을 사용 ptr_vector합니다. 보다 효율적이며 사용하는 것보다 부작용이 적습니다 vector<shared_ptr>. 나는 당신이 ptr_vector에 타입을 선언하지 못할 수도 있다고 생각하지만 (그것은 오래되었습니다), 그것의 의미는 내 의견으로는 가치가 있습니다. 기본적으로 목록에서 개체를 제거하면 자동으로 삭제됩니다. 이것은 또한 명백한 소유권을 보여줍니다.

무언가에 대한 참조가 필요한 경우, 알몸 포인터 대신 참조로 만들려고합니다. 때때로 이것은 실용적이지 않습니다 (즉, 객체가 구성된 후 참조가 필요할 때마다). 어느 쪽이든, 참조는 분명히 객체를 소유하지 않는다는 것을 보여 주며, 다른 곳에서 공유 포인터 의미를 따르는 경우 일반적으로 알몸 포인터는 추가 혼란을 일으키지 않습니다 (특히 "수동 삭제 없음"규칙을 따르는 경우) .

이 방법을 사용하면 내가 작업 한 iPhone 게임 하나만 delete호출 할 수 있었고 Obj-C에서 C ++ 브리지로 작성되었습니다.

일반적으로 나는 메모리 관리가 인간에게 맡기기에는 너무 중요하다고 생각합니다. 삭제를 자동화 할 수 있다면해야합니다. shared_ptr의 오버 헤드가 런타임에 너무 비싸면 (스레딩 지원을 끈 경우 등) 동적 할당을 낮추기 위해 다른 것 (예 : 버킷 패턴)을 사용해야합니다.


1
훌륭한 요약. smart_ptr에 대한 언급과 달리 shared_ptr을 실제로 의미합니까?
jmp97

예, shared_ptr을 의미했습니다. 내가 고칠 게
Tetrad

10

작업에 적합한 도구를 사용하십시오.

프로그램에서 예외가 발생할 수있는 경우 코드가 예외를 인식하는지 확인하십시오. 스마트 포인터, RAII를 사용하고 2 단계 구성을 피하는 것이 좋은 출발점입니다.

명확한 소유권 의미가없는 순환 참조가있는 경우 가비지 콜렉션 라이브러리 사용 또는 디자인 리팩토링을 고려할 수 있습니다.

좋은 라이브러리를 사용하면 유형이 아닌 개념에 코드를 작성할 수 있으므로 대부분의 경우 리소스 관리 문제 이외의 포인터 유형은 중요하지 않습니다.

다중 스레드 환경에서 작업하는 경우 개체가 여러 스레드에서 공유 될 수 있는지 이해해야합니다. boost :: shared_ptr 또는 std :: tr1 :: shared_ptr 사용을 고려해야하는 주요 이유 중 하나는 스레드 안전 참조 수를 사용하기 때문입니다.

참조 카운트의 개별 할당에 대해 걱정이되는 경우이 문제를 해결하는 방법에는 여러 가지가 있습니다. boost :: shared_ptr 라이브러리를 사용하면 참조 카운터를 풀로 할당하거나 boost :: make_shared (내 환경 설정)를 사용하여 객체와 참조 횟수를 단일 할당으로 할당하여 사람들이 가지고있는 대부분의 캐시 미스 문제를 완화 할 수 있습니다. 객체에 대한 참조를 최상위 수준으로 유지하고 객체에 대한 직접 참조를 전달하여 성능이 중요한 코드에서 참조 횟수를 업데이트 할 때의 성능 저하를 피할 수 있습니다.

공유 소유권이 필요하지만 참조 계산 또는 가비지 콜렉션 비용을 지불하지 않으려는 경우 변경 불가능한 오브젝트 또는 쓰기시 관용구 사용을 고려하십시오.

가장 큰 성능의 승리는 아키텍처 수준, 알고리즘 수준, 그리고 낮은 수준의 관심사는 매우 중요하지만 중요한 문제를 해결 한 후에 만 ​​해결해야한다는 점을 명심하십시오. 캐시 미스 수준에서 성능 문제를 다루는 경우 말 당 포인터와 아무런 관련이없는 허위 공유와 같이 알고 있어야하는 모든 문제가 있습니다.

텍스처 나 모델과 같은 리소스를 공유하기 위해 스마트 포인터를 사용하는 경우 Boost.Flyweight와 같은보다 전문화 된 라이브러리를 고려하십시오.

새로운 표준이 채택되면 이동 의미론, rvalue 참조 및 완벽한 전달 기능을 통해 값 비싼 객체 및 컨테이너 작업을 훨씬 쉽고 효율적으로 수행 할 수 있습니다. 그때까지 auto_ptr 또는 unique_ptr과 같은 파괴적인 복사 의미론을 가진 포인터를 컨테이너에 저장하지 마십시오 (표준 개념). Boost.Pointer 컨테이너 라이브러리를 사용하거나 컨테이너에 공유 소유권 스마트 포인터를 저장하십시오. 성능이 중요한 코드에서는 Boost.Intrusive의 컨테이너와 같은 침입 컨테이너를 선호하여 두 가지를 피하는 것이 좋습니다.

대상 플랫폼이 실제로 결정에 너무 많은 영향을 미치지 않아야합니다. 임베디드 장치, 스마트 폰, 덤폰, PC 및 콘솔은 모두 코드를 올바르게 실행할 수 있습니다. 엄격한 메모리 예산 또는로드 / 애프터로드 후 동적 할당과 같은 프로젝트 요구 사항이보다 유효한 관심사이며 선택에 영향을 미칩니다.


3
콘솔에서의 예외 처리는 다소 어려울 수 있습니다. 특히 XDK는 예외 적대적입니다.
Crashworks

1
대상 플랫폼은 실제로 디자인에 영향을 미칩니다. 데이터를 변환하는 하드웨어가 소스 코드에 큰 영향을 줄 수 있습니다. PS3 아키텍처는 하드웨어를 리소스와 메모리 관리 및 렌더러 디자인에 실제로 사용해야하는 구체적인 예입니다.
사이먼

특히 GC와 관련하여 약간만 동의하지 않습니다. 대부분의 경우 순환 참조는 참조 횟수 체계에 문제가되지 않습니다. 사람들이 객체의 소유권에 대해 제대로 생각하지 않았기 때문에 일반적으로 이러한 주기적 소유권 문제가 발생합니다. 객체가 무언가를 가리켜 야한다고해서 해당 포인터를 소유해야한다는 의미는 아닙니다. 일반적으로 인용되는 예는 트리의 백 포인터이지만 트리의 포인터에 대한 부모는 안전을 희생하지 않고 안전하게 원시 포인터가 될 수 있습니다.
Tim Seguine

4

C ++ 0x를 사용하는 경우을 사용하십시오 std::unique_ptr<T>.

std::shared_ptr<T>참조 카운트 오버 헤드가있는 것과 달리 성능 오버 헤드가 없습니다 . unique_ptr 은 포인터를 소유하고 있으며 C ++ 0x의 이동 의미론으로 소유권을 전송할 수 있습니다 . 당신은 그들을 복사 할 수 없습니다-단지 이동합니다.

예를 들어 std::vector<std::unique_ptr<T>>바이너리와 호환 가능하고 성능이 동일한 컨테이너에서도 사용할 수 std::vector<T*>있지만 요소를 지우거나 벡터를 지우면 메모리가 누출되지 않습니다. 또한 STL 알고리즘과의 호환성이보다 우수 ptr_vector합니다.

많은 목적을위한 IMO는 이상적인 컨테이너입니다 : 랜덤 액세스, 예외 안전, 메모리 누수 방지, 벡터 재 할당에 대한 낮은 오버 헤드 (장면 뒤의 포인터를 뒤섞음). 많은 목적에 매우 유용합니다.


3

어떤 클래스가 어떤 포인터를 소유하는지 문서화하는 것이 좋습니다. 가급적이면 일반 객체 만 사용하고 가능한 한 포인터는 사용하지 않는 것이 좋습니다.

그러나 리소스를 추적해야 할 경우 포인터를 전달하는 것이 유일한 옵션입니다. 몇 가지 경우가 있습니다.

  • 다른 곳에서 포인터를 가져 오지만 관리하지 마십시오. 일반 포인터를 사용하여 문서화 한 후 코더가 삭제되지 않도록 문서화하십시오.
  • 다른 곳에서 포인터를 가져 와서 추적합니다. scoped_ptr을 사용하십시오.
  • 다른 곳에서 포인터를 가져 와서 추적하지만 추적하려면 특별한 방법이 필요합니다 .custom delete 메소드와 함께 shared_ptr을 사용하십시오.
  • STL 컨테이너에 포인터가 필요합니다. 복사되어 boost :: shared_ptr이 필요합니다.
  • 많은 클래스가 포인터를 공유하며 누가 그것을 삭제할지 확실하지 않습니다 : shared_ptr (위의 경우는 실제로이 시점의 특별한 경우입니다).
  • 포인터를 직접 만들고 필요한 것만 : 일반 객체를 사용할 수없는 경우 : scoped_ptr.
  • 포인터를 만들고 다른 클래스와 공유합니다 : shared_ptr.
  • 포인터를 작성하여 전달하십시오. 일반 포인터를 사용하여 인터페이스를 문서화하여 새 소유자가 자원을 스스로 관리해야 함을 알도록하십시오!

지금은 자원 관리 방법을 거의 다룬다 고 생각합니다. shared_ptr과 같은 포인터의 메모리 비용은 일반적으로 일반 포인터의 메모리 비용의 두 배입니다. 이 오버 헤드가 너무 크다고 생각하지는 않지만 리소스가 부족하면 스마트 포인터의 수를 줄이기 위해 게임 디자인을 고려해야합니다. 다른 경우에는 위의 글 머리 기호와 같은 좋은 원칙을 설계하고 프로파일 러가 더 빠른 속도가 필요한 위치를 알려줍니다.


1

부스트의 포인터를 구체적으로 언급 할 때, 구현이 정확히 필요한 것이 아니라면 피해야한다고 생각합니다. 그들은 처음에 예상했던 것보다 더 큰 비용이 듭니다. 메모리 및 자원 관리에서 중요하고 중요한 부분을 건너 뛸 수있는 인터페이스를 제공합니다.

소프트웨어 개발에 관해서는 데이터에 대해 생각하는 것이 중요하다고 생각합니다. 데이터가 메모리에 어떻게 표현되는지는 매우 중요합니다. 그 이유는 CPU 속도가 메모리 액세스 시간보다 훨씬 빠른 속도로 증가했기 때문입니다. 이것은 종종 메모리 캐시를 가장 현대적인 컴퓨터 게임의 주요 병목 현상으로 만듭니다. 액세스 순서에 따라 데이터를 메모리에 선형으로 정렬하면 캐시에 훨씬 더 친숙합니다. 이러한 종류의 솔루션은 종종 더 깔끔한 디자인, 더 간단한 코드 및 더 쉽게 디버깅하기 쉬운 코드로 이어집니다. 스마트 포인터는 리소스의 동적 메모리 할당을 빈번하게하므로 메모리 전체에 분산됩니다.

이것은 조기 최적화가 아니라 가능한 한 빨리 결정해야하는 건전한 결정입니다. 소프트웨어가 실행될 하드웨어에 대한 아키텍처 이해의 문제이며 중요합니다.

편집 : 공유 포인터의 성능과 관련하여 고려해야 할 몇 가지 사항이 있습니다.

  • 참조 카운터는 힙 할당됩니다.
  • 스레드 안전성을 사용하는 경우 참조 계산은 연동 작업을 통해 수행됩니다.
  • 포인터를 값으로 전달하면 참조 카운트가 수정되므로 메모리에서 임의 액세스 (잠금 + 캐시 미스 가능성)를 사용하는 연동 작업이 가능합니다.

2
당신은 '모든 비용을 피하는 것'으로 나를 잃었습니다. 그런 다음 실제 게임에는 거의 관심을 갖지 않는 최적화 유형을 설명합니다. 대부분의 게임 개발은 CPU 캐시 성능 부족이 아니라 개발 문제 (지연, 버그, 재생성 등)로 특징 지어집니다. 따라서 저는이 조언이 조기 최적화가 아니라는 생각에 강력하게 동의하지 않습니다.
kevin42

2
데이터 레이아웃의 초기 설계에 동의해야합니다. 최신 콘솔 / 모바일 장치에서 성능을 얻는 것이 중요하며 절대 간과해서는 안되는 것입니다.
Olly

1
이것은 내가 일하고있는 AAA 스튜디오 중 하나에서 본 문제입니다. 또한 Insomniac Games의 Mike Acton 헤드 아키텍트를들을 수 있습니다. 부스트가 나쁜 라이브러리라고 말하는 것이 아니라 고성능 게임에만 적합하지는 않습니다.
Simon

1
@ kevin42 : 캐시 일관성은 오늘날 게임 개발에서 저수준 최적화의 주요 소스 일 것입니다. @Simon : 대부분의 shared_ptr 구현은 Linux 및 Windows PC를 포함한 비교 및 ​​스왑을 지원하는 모든 플랫폼에서 잠금을 피하며 Xbox가 포함되어 있다고 생각합니다.

1
@Joe Wreschnig : 캐시 포인터가 여전히 공유 포인터의 초기화 (복사, 약한 포인터에서 생성 등)를 유발할 가능성이 높습니다. 현대 PC의 L2 캐시 미스는 200주기와 같으며 PPC (xbox360 / ps3)는 더 높습니다. 강렬한 게임을 사용하면 각 게임 개체에 스케일링이 중요한 문제를보고있는 리소스가 상당히 많기 때문에 최대 1000 개의 게임 개체가있을 수 있습니다. 이로 인해 개발주기가 끝날 때 문제가 발생할 수 있습니다 (많은 게임 오브젝트에 부딪 칠 때).
Simon

0

나는 어디에서나 스마트 포인터를 사용하는 경향이 있습니다. 이것이 완전히 좋은 아이디어인지 확실하지 않지만 게으르고 실제 단점을 볼 수 없습니다 (C 스타일 포인터 산술을 원한다면 제외). 나는 그것을 복사 할 수 있다는 것을 알고 있기 때문에 boost :: shared_ptr을 사용합니다. 두 엔티티가 이미지를 공유하면 하나가 죽으면 다른 하나도 이미지를 잃지 않아야합니다.

이것의 단점은 하나의 객체가 가리키고 소유하는 것을 삭제하지만 다른 객체도 그것을 가리키면 삭제되지 않는다는 것입니다.


1
나는 거의 모든 곳에서 share_ptr을 사용하고 있지만 오늘은 실제로 일부 데이터에 대한 공유 소유권이 필요한지 여부를 생각하려고합니다. 그렇지 않으면 해당 데이터를 부모 데이터 구조의 비 포인터 멤버로 만드는 것이 합리적 일 수 있습니다. 명확한 소유권은 디자인을 단순화합니다.
jmp97

0

훌륭한 스마트 포인터가 제공하는 메모리 관리 및 문서의 이점은 정기적으로 사용한다는 의미입니다. 그러나 프로파일 러가 파이핑되어 특정 사용량으로 인해 비용이 발생한다고 알려 주면 더 신석기 적 포인터 관리로 되돌아갑니다.


0

나는 늙고, oldskool, 그리고주기 카운터입니다. 내 자신의 작업에서 나는 풀 포인터를 사용하고 런타임에 동적 할당을 사용하지 않습니다 (풀 자체 제외). 모든 것이 풀링되며 소유권이 매우 엄격하고 양도 할 수 없습니다. 실제로 필요한 경우 사용자 지정 작은 블록 할당자를 작성합니다. 게임 중에 모든 수영장이 스스로 지울 수있는 상태가 있는지 확인합니다. 물건이 털이 나면 손잡이로 물건을 감싸서 재배치 할 수는 있지만 오히려 그렇지 않습니다. 컨테이너는 커스텀이며 뼈가 매우 튼튼합니다. 또한 코드를 재사용하지 않습니다.
나는 모든 스마트 포인터와 컨테이너 및 반복자의 미덕에 대해 결코 논쟁하지 않을 것이지만, 나는 매우 빠른 코딩을 할 수있는 것으로 알려져 있습니다. 심장 마비와 영원한 악몽처럼).

물론 프로토 타이핑을하지 않는 한, 일할 때마다 모든 것이 달라집니다.


0

비록 이것이 이상한 답변은 아니지만 거의 모든 사람에게 적합한 곳은 거의 없습니다.

그러나 개인적인 사례에서 특정 유형의 모든 인스턴스를 중앙 랜덤 액세스 시퀀스 (스레드 안전)에 저장하는 대신 32 비트 인덱스 (상대 주소, 즉)로 작업하는 것이 훨씬 더 유용하다는 것을 알았습니다. 절대 포인터가 아닌.

시작하려면 :

  1. 64 비트 플랫폼에서 아날로그 포인터의 메모리 요구 사항을 절반으로 줄입니다. 지금까지 특정 데이터 유형의 ~ 42 억 개 이상의 인스턴스가 필요하지 않았습니다.
  2. 특정 유형의 모든 인스턴스가 T메모리에 너무 흩어지지 않도록합니다. 이는 모든 종류의 액세스 패턴에 대한 캐시 미스를 줄이고 노드가 포인터가 아닌 인덱스를 사용하여 서로 연결된 경우 트리와 같은 링크 된 구조를 순회하는 경향이 있습니다.
  3. 병렬 데이터는 트리 나 해시 테이블 대신 저렴한 병렬 배열 (또는 희소 배열)을 사용하여 쉽게 연결할 수 있습니다.
  4. 병렬 교차점을 사용하면 선형 교차점 또는 더 나은 교차점을 찾을 수 있습니다.
  5. 인덱스를 기수 정렬하고 캐시 친화적 인 순차 액세스 패턴을 얻을 수 있습니다.
  6. 특정 데이터 유형이 얼마나 많은 인스턴스에 할당되었는지 추적 할 수 있습니다.
  7. 이런 종류의 일에 신경 쓰면 예외 안전과 같은 것들을 처리 해야하는 장소 수를 최소화하십시오.

즉, 편의성은 단점이며 유형 안전성입니다. 컨테이너 인덱스 T모두 액세스 할 수 없으면 인스턴스에 액세스 할 수 없습니다 . 그리고 평범한 구식 은 그것이 어떤 데이터 유형을 참조하는지 전혀 알려주지 않으므로 유형 안전이 없습니다. 에 대한 색인을 사용하여 실수로에 액세스하려고 시도 할 수 있습니다 . 두 번째 문제를 완화하기 위해 종종 이런 종류의 일을합니다.int32_tBarFoo

struct FooIndex
{
    int32_t index;
};

어리석은 것처럼 보이지만 사람들이 실수 로 컴파일러 오류없이 Bar인덱스를 통해 액세스하려고 시도 할 수 없도록 형식 안전성을 다시 제공 Foo합니다. 편의상, 나는 약간의 불편을 받아들입니다.

사람들에게 큰 불편을 줄 수있는 또 다른 것은 OOP 스타일 상속 기반 다형성을 사용할 수 없다는 것입니다. 크기와 정렬 요구 사항이 다른 모든 종류의 하위 유형을 가리킬 수있는 기본 포인터가 필요하기 때문입니다. 그러나 요즘에는 상속을 많이 사용하지 않습니다 .ECS 접근 방식을 선호하십시오.

에 관해서는 shared_ptr너무 많이 사용하지 않으려 고합니다. 대부분의 경우 소유권을 공유하는 것이 이치에 맞으며, 그렇게하면 논리적으로 유출 될 수 있습니다. 종종 적어도 높은 수준에서 한 가지는 한 가지에 속하는 경향이 있습니다. shared_ptr스레드를 완성하기 전에 객체가 파괴되지 않도록 스레드의 로컬 함수처럼 실제로 소유권을 다루지 않는 곳에서 객체의 수명을 연장하는 것이 종종 사용 되는 유혹을 느꼈 습니다. 그것을 사용합니다.

이 문제를 해결하기 위해 shared_ptrGC 또는 이와 유사한 것을 사용하는 대신 스레드 풀에서 실행되는 수명이 짧은 작업을 선호하고 스레드가 객체를 삭제하도록 요청하면 실제 파괴가 안전한 것으로 지연됩니다 시스템이 스레드가 해당 오브젝트 유형에 액세스 할 필요가 없음을 보장 할 수있는 시간.

나는 때때로 ref-counting을 사용하지만 결국 최후의 수단 전략처럼 취급합니다. 그리고 지속적인 데이터 구조의 구현과 같이 소유권을 공유하는 것이 합리적 일 수있는 몇 가지 경우가 shared_ptr있습니다.

어쨌든 나는 주로 인덱스를 사용하고 원시 포인터와 스마트 포인터를 거의 사용하지 않습니다. 나는 물체가 연속적으로 저장되고 메모리 공간에 흩어져 있지 않다는 것을 알 때 색인과 문이 열리는 것을 좋아합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.