C++프로그래밍C++ 개발자

C++17의 보장된 복사 생략 규칙이 prvalue 표현식을 함수 파라미터에 바인딩할 때 복사/이동 생성자의 접근성을 어떻게 변경합니까?

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

C++17에서는 보장된 복사 생략 (필수 복사 생략) 규칙을 도입하여 prvalue (순수 rvalue)가 물질화되는 방식을 근본적으로 변경했습니다. 클래스 타입의 prvalue가 동일한 타입의 객체를 초기화할 때— 함수의 반환값으로 지정하거나 임시 객체를 함수에 전달할 때— 객체는 목적지 저장소에 직접 생성됩니다. 따라서 복사 생성자 또는 이동 생성자는 호출되지 않으며, 중요하게도, 이들의 접근성(공용 대 비공용)이나 단순한 존재(클래스가 완전하고 소멸 가능하다는 조건 하에)는 작동을 위해 필수적이지 않습니다. 이는 이전 표준과 크게 대조됩니다. 이전 표준에서는 복사 생략이 단순히 선택적인 최적화로, 컴파일을 위해 접근 가능하고 존재하는 생성자가 요구되었습니다.

struct Immovable { Immovable() = default; Immovable(const Immovable&) = delete; Immovable(Immovable&&) = delete; }; Immovable factory() { return Immovable{}; // C++17에서는 OK: 이동/복사 호출되지 않음 } void consume(Immovable x); // 파라미터가 prvalue에서 직접 초기화됨

실제 상황

우리 팀은 리소스 핸들이 하드웨어 컨텍스트를 감싸는 커널 모드 드라이버를 구축하고 있었습니다. 이 핸들은 커널 주소가 등록되어 있기 때문에 메모리에서 복제하거나 이동할 수 없었습니다. 우리는 RAII 관리를 위해 이러한 핸들을 값으로 생성하는 공장 함수를 필요로 했지만, 핸들은 명시적으로 복사 및 이동 생성자를 삭제하여 커널 매핑의 우발적인 무효화를 방지했습니다. C++17 이전에는, 이 설계가 값으로 반환하는 것과 호환되지 않았습니다. 왜냐하면 NRVO 와 함께라도 컴파일러가 이동 생성자에 접근 가능할 것을 요구했기 때문입니다. 이는 컴파일 오류를 초래했습니다.

해결책 1: std::unique_ptr을 통한 힙 할당

우리는 핸들을 std::unique_ptr로 감싸는 것을 고려했습니다. 이것은 포인터를 이동할 수 있게 하면서도 기본 객체는 고정된 상태로 유지되도록 하였습니다. 이 접근 방식은 안전성을 제공하며 C++14에서도 작동했습니다.

장점: 표준 메모리 관리, 메모리 누수 방지, 레거시 코드베이스에서 널리 지원됨.

단점: 동적 할당 오버헤드를 도입하며, 커널 컨텍스트에서는 결정적 저지연성이 요구되므로 문제를 일으킬 수 있습니다. 또한 CPU 캐시를 단편화하고 할당 실패에 대한 예외 처리 고려 사항이 필요합니다.

해결책 2: 아웃-파라미터 초기화

공장으로 호출자가 할당한 객체에 대한 참조를 전달하여 직접 초기화하도록 합니다.

장점: C++ 표준 버전에 관계없이 제로 복사 보장; 힙 할당 없음; 이동할 수 없는 타입과 호환됨.

단점: 유창한 API 스타일을 손상시킵니다 (auto h = create();Handle h; create(h);로 바뀜); 초기화 전 사용의 위험이 증가하고 표준 알고리즘 및 범위 기반 for 루프와 잘 조화되지 않습니다.

해결책 3: C++17 보장된 복사 생략 활용하기

우리는 공장을 리팩토링하여 이동할 수 없는 타입을 값으로 반환하고 필수적인 생략에 의존하여 prvalue를 호출자의 저장소로 직접 생성하도록 하였습니다.

장점: 힙 사용을 없앴습니다; 값 의미 체계를 보존했습니다; 컴파일 시간에 제로 비용 추상화를 보장합니다; 이동/복사 생성자가 존재할 필요가 없거나 접근 가능할 필요가 없습니다.

단점: 순수 rvalue에만 적용됩니다(기존 이름이 있는 변수를 반환할 수 없음); C++17 지원을 하는 컴파일러가 필요합니다; 생성 중 예외 처리에서의 미세한 차이를 이해해야 합니다.

우리는 해결책 3을 선택했습니다. 왜냐하면 공장이 생성하는 새로운 임시 객체가 순수 prvalue로, 보장된 생략 시나리오에 완벽히 적합했기 때문입니다. 이로 인해 핸들이 엄격히 이동할 수 없는 상태를 유지하면서도 편리한 값 의미 체계와 auto 선언과의 호환성을 유지할 수 있었습니다.

드라이버는 수천 개의 동시 연결에 대해 마이크로초 규모의 초기화로 출시되었습니다. 어셈블리 검사는 핸들이 호출자의 스택 프레임에 직접 생성되었음을 확인했고, 이동이나 복사 코드가 없었습니다. 타입 시스템은 건설 과정에서 리소스 안전성을 보장하였고, 핫 경로에서 힙 경합을 완전히 제거했습니다.

후보자들이 자주 놓치는 점


보장된 복사 생략이 함수 내부의 이름이 있는 반환 값 (lvalues)에 적용됩니까, 아니면 순수 rvalues에만 한정됩니까?

보장된 복사 생략은 오로지 prvalues (순수 rvalues)에만 적용됩니다. 즉, 이름이 없는 반환 문에서 생성된 임시 객체와 같습니다. 이름이 있는 반환 값 최적화 (NRVO)는 여전히 선택적인 컴파일러 최적화입니다. 널리 구현되지만 생성자 접근성이나 부작용에 대한 동일한 보장을 제공하지 않습니다. 후보자가 이름이 있는 지역 변수를 반환하고 그것이 보장된 생략을 유도할 것이라고 가정할 경우, 그 프로그램은 ill-formed가 됩니다. 이름이 있는 변수는 lvalues이며, 이동/복사 작업이 필요합니다. 컴파일러가 선택적인 NRVO를 적용하지 않는 한 이러한 변수는 이동 생성자가 삭제되더라도 요구됩니다.


복사 및 이동 생성자가 명시적으로 삭제된 클래스가 보장된 복사 생략 규칙에 따라 값으로 반환될 수 있습니까?

예. C++17에서 반환된 표현식이 prvalue (예: return MyClass{};)인 경우, 복사 및 이동 생성자는 초기화에 대해 고려되지 않습니다. 객체가 호출자의 저장소에 직접 생성되므로 삭제된 생성자는 odr-used되지 않으며 컴파일 오류를 일으키지 않습니다. 하지만 해당 타입의 이름이 있는 변수를 반환하려고 하면 실패하게 됩니다. 이 작업은 개념적으로 lvalue를 반환 슬롯으로 이동해야 하며, 이는 삭제된 이동 생성자를 호출하게 되어 ill-formed 프로그램을 초래합니다.


보장된 복사 생략은 예외 안전성과 어떻게 상호작용합니까, 특히 스택 언와인딩 동안 prvalue 임시 객체의 수명에 관하여?

보장된 복사 생략 하에서는 대상 객체의 수명이 시작되기 전에 별도의 임시 객체가 생성되지 않습니다. prvalue는 최종 목적지에서 직접 물질화됩니다. 따라서 prvalue 생성 중 예외가 발생하면 스택 언와인딩 메커니즘은 파괴가 필요한 별도의 임시 객체를 만나지 않고, 대신 부분적으로 생성된 대상 객체를 마주하게 됩니다. 이는 호출자의 관점에서 객체가 완전히 생성되었거나 전혀 존재하지 않는다는 것을 의미하며, 예외 안전 보장 단순화 및 예외 처리 중 임시 객체가 버려지는 상황에서의 이중 파괴나 리소스 누수가 발생하지 않도록 합니다.