std::enable_shared_from_this는 일반적으로 weak_this라는 이름을 가진 비공개 가변 std::weak_ptr<T> 멤버를 캡슐화하는 믹스인 기본 클래스입니다. 파생 객체의 생성 동안 이 내부 weak_ptr는 기본 생성 과정을 거치며, 비어 있는(만료된) 상태로 남겨집니다. 중요한 아키텍처 세부 사항은 이 내부 포인터가 제어 블록을 참조하도록 초기화되는 것이 std::shared_ptr 생성자의 실행 후, 관리되는 객체의 생성자가 완료된 후에만 발생한다는 것입니다. 따라서 생성자 본문 내에서 shared_from_this()를 호출하면 비어 있는 weak_ptr에 대해 lock() 호출을 시도하게 되며, 이는 C++17부터 std::bad_weak_ptr 예외를 던지도록 요구합니다(이전 표준에서는 정의되지 않은 동작). 이는 새로운 참조를 제공하기 위한 공유 소유권 인프라가 아직 설정되지 않았기 때문입니다.
맥락:
하이프리퀀시 거래 플랫폼은 주식 거래소와의 지속적인 TCP 연결을 관리하기 위해 MarketDataHandler 클래스를 구현했습니다. 핸들러가 비동기 소켓 읽기/쓰기 작업 중에 살아 있도록 보장하기 위해 클래스는 **std::enable_shared_from_this<MarketDataHandler>**를 상속받았습니다. 생성자는 연결 매개 변수를 받아 즉시 비동기 읽기 작업을 시작하며, 완료 핸들러로 shared_from_this()를 Boost.Asio 이벤트 루프에 전달합니다.
문제:
통합 테스트 중에 응용 프로그램은 연결이 수립되자마자 std::bad_weak_ptr 예외를 잡지 못하고 프로세스를 종료하며 충돌했습니다. 개발 팀은 기본 클래스 std::enable_shared_from_this의 서브객체가 파생 클래스 생성자 본문이 실행되기 전에 생성된다는 점에서 내부 추적 메커니즘이 즉시 사용 준비가 되어 있을 것이라고 가정했습니다. 그들은 객체 생성과 std::shared_ptr 래퍼의 완료 사이의 시간 간격을 간과했으며, 이로 인해 내부 weak_ptr는 공장 표현식이 종료될 때까지 초기화되지 않는 상태로 남게 되었습니다.
고려된 대안 솔루션:
post_construct()를 통한 두 단계 초기화:
클래스를 리팩토링하여 모든 비동기 초기화 논리를 생성자에서 분리된 post_construct() 공개 메서드로 이동합니다. 호출자는 먼저 std::make_shared를 사용해 **std::shared_ptr<MarketDataHandler>**를 생성한 다음, 결과에 대해 즉시 post_construct()를 호출하여 포인터를 시스템에 반환합니다.
post_construct()를 호출하는 것을 잊을 수 있어 핸들이 데이터 처리를 시작하지 않는 미세한 버그를 유발할 수 있습니다.외부 수명 보장을 가진 원시 포인터:
비동기 I/O 시스템에 원시 this 포인터를 전달하고 std::shared_ptr 키를 사용하여 활성 연결의 별도 전역 레지스트리를 유지하며, 각 콜백 실행 시 레지스트리 멤버십을 확인합니다.
shared_from_this()가 필요하지 않습니다.비공식 생성자를 가진 정적 팩토리 메서드:
모든 생성자를 비공개로 만들고 **std::shared_ptr<MarketDataHandler>**를 반환하는 공개 정적 create() 메서드를 제공합니다. create() 내에서 메서드는 먼저 std::make_shared를 사용하여 객체를 생성한 다음, 결과 공유 포인터를 사용하여 비동기 작업을 시작하고 이를 호출자에게 반환합니다.
std::make_shared를 사용할 수 없으며; 문법이 약간 더 장황하게 요구됩니다(MarketDataHandler::create() 대신 std::make_shared<MarketDataHandler>()).선택된 솔루션:
정적 팩토리 패턴이 선택되었습니다. 이는 소유되지 않는 객체에서 shared_from_this()를 호출할 가능성을 없앴습니다. create() 메서드에서 생성만 가능하도록 제한함으로써, std::shared_ptr 제어 블록이 항상 완전히 생성되었고, 내부 weak_ptr가 추가 참조를 제공하기 전에 초기화되도록 보장했습니다.
결과:
리팩토링으로 모든 시작 충돌이 제거되었습니다. 코드베이스는 네트워킹 계층에서 일관되게 적용할 수 있는 비동기 객체 생성의 강력한 패턴을 채택했습니다. 코드 리뷰 가이드라인은 공장 생성 이후에 호출된 메서드 외부에서의 모든 shared_from_this() 호출을 금지하도록 업데이트되어, 수명 관련 결함 비율이 크게 감소했습니다.
질문: shared_from_this()가 참조 개수를 증가시키며, 제어 블록과 어떻게 상호 작용하는가?
답변:
shared_from_this()는 새로운 제어 블록을 생성하지 않습니다. 대신, 내부 가변 std::weak_ptr<T> 멤버에 접근하여 이를 호출합니다. 이 연산은 제어 블록이 여전히 존재하는지를 원자적으로 확인하고, 존재하는 경우 기존 제어 블록과 연결된 강한 참조 수를 증가시키며, 소유권을 공유하는 새로운 std::shared_ptr 인스턴스를 반환합니다. 객체가 이미 파괴되었거나(만료된 약한 포인터) lock()은 빈 std::shared_ptr를 반환합니다. 후보자는 종종 shared_from_this()가 내부 shared_ptr의 복사본을 단순히 반환한다고 잘못 생각하지만, 실제로는 약한 참조를 강한 참조로 프로모션하기 때문에 "이중 소유" 시나리오를 피하는 데 매우 중요합니다.
질문: 클래스가 std::enable_shared_from_this<T>를 여러 번 상속받거나 다이아몬드 계층에서 여러 경로를 통해 상속받을 수 있는가?
답변:
클래스는 동일한 T에 대해 **std::enable_shared_from_this<T>**를 직접 여러 번 상속받을 수 없습니다. 이는 모호한 기본 클래스 서브 객체를 생성하기 때문입니다. 그러나 클래스 Derived는 오직 **std::enable_shared_from_this<Derived>**에서만 상속받아야 하며, 기본 클래스의 버전에서 상속받아서는 안 됩니다. 후보자가 간과하는 중요한 세부 사항은 가상 상속입니다: 만약 Base가 **std::enable_shared_from_this<Base>**에서 상속받고, Derived가 Base에서 상속받으면, Derived 내에서 Base 포인터에서 shared_from_this()를 호출하는 것이 제대로 작동합니다. 왜냐하면 내부 weak_ptr가 가장 파생된 객체를 가리키도록 초기화되기 때문입니다. 그러나 만약 Derived도 **std::enable_shared_from_this<Derived>**에서 공개적으로 상속받으면, 이는 두 개의 서로 다른 weak_ptr 멤버를 생성하며 어떤 것이 초기화되는지에 대한 혼란을 초래합니다. 표준은 std::shared_ptr 생성자가 std::enable_shared_from_this 특수화를 찾아 초기화하는 것을 명시적으로 요구하므로, 독립적인 weak_ptr 멤버를 두 개 가지는 것은 하나만 초기화될 경우(일반적으로 첫 번째 std::shared_ptr를 생성하는 데 사용된 정적 유형과 연결된 것) 다른 것들은 비어 있게 남아 후속 shared_from_this() 호출이 실패하게 됩니다.
질문: std::make_shared와 std::shared_ptr<T>(new T)의 안전성 문제는 생성 중 shared_from_this()에 무관한가?
답변:
두 할당 전략 모두 궁극적으로 std::shared_ptr 생성자를 호출하여 std::enable_shared_from_this 기본 클래스를 템플릿 메타프로그래밍을 통해 감지합니다. 내부 weak_ptr의 초기화는 std::shared_ptr 생성자 로직 내에서만 발생하며, new T의 실행 중이나 make_shared의 내부 객체 생성 단계 동안에는 발생하지 않습니다. 구체적으로, make_shared는 저장소를 할당하고 T 객체를 구성하며(이동안 weak_ptr는 비어 있음), 그 이후에야 std::shared_ptr 생성자가 weak_ptr을 새로 생성된 제어 블록을 가리키도록 초기화합니다. 후보자들은 종종 make_shared가 단일 할당 최적화로 인해 객체를 더 빨리 "준비"할 수 있다고 가정하지만, 표준은 어떤 생성자 함수가 사용되었든 상관없이 생성자 본문 내에서 shared_from_this()를 호출하는 것이 안전하지 않다고 보장합니다. 왜냐하면 weak_ptr 할당은 반드시 T 생성자가 완료된 후에 발생하기 때문입니다.