C++98에서는 멤버 함수가 숨겨진 this 포인터를 통해 암묵적인 객체에 접근하였고, 이로 인해 const 및 비-const 컨텍스트를 처리하기 위해 서로 다른 오버로드가 필요했습니다. C++11에서는 lvalue와 rvalue 객체를 구별하기 위해 참조 한정자를 도입했습니다. 이는 모든 cv-ref 조합을 다루기 위해 함수당 최대 네 개의 오버로드를 요구하게 되어, 제네릭 라이브러리에 대해 상당한 코드 중복 및 유지 보수 부담을 초래했습니다.
핵심 문제는 멤버 함수가 호출자와 동일한 값 범주 및 cv-자격을 가진 객체를 반환해야 할 때 발생하여, 효율적인 이동 의미론을 가능하게 하거나 유효하지 않은 참조를 방지합니다. 객체의 타입을 추론하지 못하면, 개발자들은 장황한 오버로드 집합을 작성하거나 복사 의미론을 타협하여 비효율적인 rvalue 처리가 발생하거나, 개체 참조가 전파되는 일반 코드에서 미세한 생명주기 버그로 이어졌습니다.
C++23은 명시적 객체 매개변수를 도입하여 void foo(this auto&& self)라는 구문을 허용합니다. 여기에서 self는 객체의 값 범주와 cv-자격을 캡처하는 추론된 매개변수가 되어, 별도의 & 및 && 오버로드의 필요성을 없애고, std::forward<decltype(self)>(self)가 올바른 범주를 전파합니다. 그러나 정적 멤버 함수는 암묵적인 객체가 완전히 없기 때문에, 이 구문을 적용하는 것은 self에 바인딩할 객체가 필요하다는 기본 요구 사항을 위반하여 표준에 따라 프로그램을 잘못된 형태로 만듭니다.
// Pre-C++23: 필요한 네 개의 오버로드 class Builder { public: Builder& setName(...) & { /* ... */ return *this; } Builder const& setName(...) const& { /* ... */ return *this; } Builder&& setName(...) && { /* ... */ return std::move(*this); } Builder const&& setName(...) const&& { /* ... */ return std::move(*this); } }; // C++23: 단일 오버로드 class Builder { public: template<typename Self> auto setName(this Self&& self, ...) -> Self&& { // ... return std::forward<Self>(self); } };
우리 팀은 DOM 노드가 트리 구성을 위한 메서드 체이닝을 지원하는 고성능 JSON 라이브러리를 개발하였고, 이로 인해 Node 클래스는 고유한 반환 의미론을 가진 addChild() 메서드를 제공해야 했습니다. 이 메서드는 부모가 lvalue일 때는 참조로 부모를 반환하고, 부모가 rvalue 임시 객체일 때는 값을 반환하여 이동 생략을 가능하게 하며 만료되는 객체의 우연한 수정을 방지해야 했습니다.
초기 구현에서는 전통적인 참조 한정자 오버로드를 사용하였습니다. 우리는 addChild의 네 가지 버전을 유지했습니다: lvalue에 대해 Node&를 반환하는 하나, const lvalue에 대해 Node const&를 반환하는 하나, rvalue에 대해 Node&&를 반환하는 하나, const rvalue에 대해 Node const&&를 반환하는 하나. 이 접근 방식은 성능 요구 사항을 충족했지만, 테스트 면적을 네 배 증가시켰고, const&& 오버로드가 & 오버로드에서 복사-붙여넣기 오류로 인해 유효하지 않은 참조를 잘못 반환하는 심각한 버그가 발생했습니다.
우리는 참조 한정자를 완전히 포기하고 항상 값을 반환하도록 고려했으며, 명명된 객체에 대한 불필요한 이동을 강요하고 반환된 노드에 대한 참조를 저장하는 기존 코드와의 API 호환성을 깨뜨렸습니다. 우리는 파생 타입을 추론하는 기본 클래스 템플릿을 사용하는 CRTP도 평가했지만, 이는 사용자에게 구현 세부 정보를 노출하고 상속 계층을 복잡하게 만들었으며 값 범주 전파 문제를 완전히 해결하지 못했습니다.
C++23의 명시적 객체 매개변수를 채택함으로써 우리는 오버로드 집합을 단일 템플릿 메서드로 축소할 수 있었습니다: template<typename Self> auto addChild(this Self&& self, ...) -> Self. 이는 필요한 정확한 값 범주를 캡처하고 std::move 또는 std::forward 중복 없이 완벽한 전달을 가능하게 하여 메서드의 복잡성을 한 경로로 줄였습니다. 결과적으로 보일러플레이트 코드가 75% 감소하였고 오버로드 발산과 관련된 버그 범주가 제거되었습니다.
명시적 객체 매개변수 구문을 사용할 때 함수가 전통적인 cv-자격이나 참조 한정자를 매개변수 목록 뒤에 추가하는 것을 어떻게 방지합니까?
전통적인 멤버 함수는 cv-자격 및 참조 한정자를 매개변수 목록 뒤에 배치하여 암묵적인 this 포인터 타입을 수정합니다. 명시적 객체 매개변수의 경우, this Self&& self는 이미 Self의 타입 추론 내에서 cv-자격 및 참조 범주를 인코딩합니다. 매개변수 목록 뒤에 const 또는 &와 같은 추가 자격을 추가하면 존재하지 않는 암묵적 객체를 자격이 부여하려고 하여, 타입 시스템의 모순을 초래합니다. 표준은 이 조합을 명시적으로 금지합니다. 왜냐하면 명시적 매개변수가 매개변수와 자격 모두의 역할을 포함하고 있으며, 둘 다를 허용하면 호출을 지배하는 의미론에 대한 모호성을 생성할 수 있기 때문입니다.
명시적 객체 매개변수를 사용할 때 함수 본문 내의 이름 조회가 전통적인 멤버 함수와 어떻게 다릅니까?
전통적인 멤버 함수에서는 비자격 이름 조회가 자동으로 클래스 범위를 검색하듯이 this->가 앞에 붙은 것처럼 작동합니다. 명시적 객체 매개변수에서는 암묵적인 this 포인터가 없기 때문에, 매개변수 self를 통해 명시적으로 멤버에 접근해야 합니다. 후보자들은 void foo(this auto& self) 내의 member가 자동으로 this->member로 해결된다고 가정하지만, 실제로는 self. 자격 지정 또는 ClassName::member와 같은 명시적 클래스 자격이 필요합니다. 이는 기본적인 조회 규칙을 변경하며 코드 마이그레이션 시 적응이 필요합니다. 특히 파생 클래스에서 보호된 멤버에 접근할 때 self.가 추론된 타입에 대해 접근 검사를 명시적으로 트리거하기 때문에 정적 클래스 타입에 대해 접근 검사를 수행하는 것과는 다릅니다.
명시적 객체 매개변수가 가상 함수 재정의에 참여할 수 있으며, 재정의 관계에는 어떤 제약이 있습니까?
명시적 객체 매개변수는 가상 함수에 나타날 수 있지만, 본질적으로 재정의 일치 규칙을 변경합니다. 기본 클래스가 virtual void bar(this Base& self)를 선언할 경우, 파생 클래스가 void bar(this Derived& self)를 선언하여 오버라이드 할 수 없습니다. 비록 전통적인 재정의가 공변 반환 타입을 허용하더라도, 명시적 객체 매개변수는 재정의 일치 목적으로 함수의 시그니처 일부가 됩니다. Base&와 Derived&는 서로 다른 타입이므로 이는 유효한 재정의로 간주되지 않습니다. 이는 명시적 객체 매개변수를 사용하여 "sfinae-friendly" 가상 함수를 달성하거나 다형 계층에서 타입 보존 메서드 체이닝을 시도하는 일반적인 패턴을 방지합니다. 재정의하려면 파생 함수가 기본의 명시적 매개변수 타입과 정확히 일치해야 하며, 이로 인해 재정의 맥락에서 그 매개변수에 대한 추론의 이점을 상실하게 됩니다.