C++ 표준(특히 [over.ics.list])에 따르면 목록 초기화가 발생할 때 컴파일러는 중괄호로 묶인 초기화 목록을 **std::initializer_list<T>**를 수용하는 생성자와 일치시키려고 시도합니다. 이러한 바인딩은 정체성 변환(정확한 일치)을 구성하며, 이는 개별 요소를 비초기화 목록 생성자와 일치시키기 위해 필요한 사용자 정의 변환보다 높은 순위를 가집니다. 따라서 Container(size_t count, T value)와 같은 생성자는 {10, 20}로 호출될 때 Container(std::initializer_list<T>)에게 패배하는데, 후자는 요소별 축소와 관계없이 중괄호로 감싸진 초기화 목록 인수에 대해 변환이 필요하지 않기 때문입니다.
우리는 리터럴 테이블 초기화를 위한 채우기 생성자 Matrix(size_t rows, size_t cols, double val)와 집합 스타일 생성자 Matrix(std::initializer_list<std::initializer_list<double>>)를 제공하는 Matrix 클래스를 설계하고 있었습니다. 한 주니어 개발자가 Matrix m{1080, 1920, 0.0}를 작성하여 1080x1920의 영 초기화된 매트릭스를 기대했지만, 대신 프로그램은 세 개의 스칼라 값이 포함된 1x3 매트릭스를 생성하였고, 이는 디버깅 세션 중 추적하기 어려운 미세한 런타임 렌더링 충돌을 일으켰습니다.
우리는 처음에 Matrix(1080, 1920, 0.0)과 같은 괄호 구문을 채우기 생성자에 강제하여 std::initializer_list 오버로드를 우회하도록 고려했습니다. 그러나 이는 C++11 통일 초기화에 대한 우리의 코딩 표준의 선호를 위반하였고, 일부 생성자는 괄호가 필요하고 다른 생성자는 중괄호를 사용하는 불일치한 API를 초래했습니다.
다음으로, 우리는 채우기 생성자에 fill_tag_t 매개변수를 추가하여 태그 디스패칭을 탐색했습니다. 이는 사용자에게 Matrix{fill_tag, 1080, 1920, 0.0}를 작성하게 강제하여 호출을 구별했습니다. 그러나 이는 공공 인터페이스를 복잡하게 만들었고, 인위적인 태그 타입 없이 직관적인 생성자 서명을 예상했던 개발자들을 혼란스럽게 했습니다.
셋째로, 우리는 SFINAE를 템플릿 매개변수에 적용하여 std::initializer_list 생성자가 중첩된 중괄호에 대해서만 활성화되도록 제한하려고 했습니다. 그러나 이 접근법은 Matrix{{1.0, 2.0}, {3.0, 4.0}}와 같은 정당한 사용 사례를 깨뜨리고, 컴파일 시간과 오류 메시지의 복잡성을 증가시키는 부서지기 쉬운 템플릿 메타 프로그래밍을 도입했습니다.
궁극적으로, 우리는 정적 팩토리 함수 Matrix::filled(rows, cols, val)를 도입하기로 선택하고 세 매개변수를 가진 채우기 생성자를 비공개로 만들었으며, 사용자들에게 치수 기반 생성을 위한 명시적인 구문을 요구하면서 std::initializer_list 생성자는 집합 구문을 위해 공개 상태로 유지했습니다. 이는 리터럴 테이블에 대한 직관적인 중괄호 초기화를 보존하였고 치수 인수의 우연한 잘못 해석의 위험을 줄였습니다.
리팩토링된 API는 Matrix{1080, 1920, 0.0}를 공통 공개 생성자가 일치하지 않는 컴파일 오류로 만들어 원래 버그를 방지했습니다. 이제 개발자들은 채우기 작업을 위해 Matrix::filled(1080, 1920, 0.0)을 사용하거나 집합 목록을 위해 Matrix{{...}}를 사용해야 했으며, 이는 코드의 명확성과 안전성을 크게 향상시켰습니다.
컴파일러는 중괄호로 감싸진 초기화 목록을 비초기화 목록 생성자와 비교하여 어떻게 순위를 매기나요?
C++ 표준의 목록 초기화에 대한 오버로드 해결 규칙에 따르면, 중괄호로 감싸진 초기화 목록을 std::initializer_list<T> 매개변수에 바인딩하는 것은 가장 높은 순위를 가진 정체성 변환을 구성합니다. 반면에 동일한 중괄호 초기화 목록을 다른 생성자와 일치시키려면 컴파일러가 목록을 괄호에 감싼 표현 목록으로 간주하고 각 요소에 대해 사용자 정의 또는 표준 변환을 수행해야 합니다. 정체성 변환이 모든 다른 변환 시퀀스보다 높은 순위를 가지기 때문에, 초기화 목록 생성자는 대체 생성자가 요구하는 요소 유형이 더 나쁜 논리적 일치일지라도 승리합니다.
auto x = {1, 2, 3};가 C++11 및 C++14에서 std::initializer_list<int>로 유추되는 이유와, auto x{1, 2, 3}가 C++17 이후에 잘못된 형식이 되는 이유는 무엇입니까?
C++17 이전에 = 토큰을 사용한 복사 리스트 초기화는 항상 중괄호 초기화 목록에 대해 std::initializer_list를 유추했습니다. 그러나 C++17은 auto와 함께 하는 직접 리스트 초기화에 대한 새로운 규칙을 도입하여 중괄호 초기화 목록이 여러 요소를 포함할 경우 유추가 실패합니다. 이는 이 문맥에서 auto가 std::initializer_list를 나타낼 수 없게 만들어 프로그램을 잘못된 형식으로 만듭니다. 이 변경 사항은 직접 초기화에 대한 "비밀 std::initializer_list" 함정을 제거하지만, 후보들은 여전히 복사 구문(auto x = {...})이 최신 **C++**에서도 std::initializer_list를 유추하는 점에서 미세한 불일치가 있다는 것을 간과하는 경우가 많습니다.
initializer_list 생성자와 가변 인자 템플릿 생성자를 모두 가진 클래스는 어떤 시나리오에서 모호하게 해결될 수 있으며, std::in_place_t가 이를 어떻게 명확히 할 수 있습니까?
클래스가 Container(std::initializer_list<T>)와 template<typename... Args> Container(Args&&... args)를 모두 제공하는 경우, 가변 인자 팩은 템플릿 인수 유추를 통해 초기화 목록 생성자와 동일한 인수와 일치할 수 있습니다. Container c{1, 2, 3}의 경우 두 생성자 모두 유효합니다: 첫 번째는 중괄호 초기화 목록의 정체성 변환을 통해, 두 번째는 Args가 int, int, int로 유추되는 방식으로 가능합니다. 비-템플릿 초기화 목록 생성자가 일반적으로 동점에서 승리하지만, 가변 생성자에 std::in_place_t와 같은 태그 유형을 추가하면(예: Container(std::in_place_t, Args&&... args)) 사용자가 Container{std::in_place, 1, 2, 3}를 작성해야 하며, 이는 가변 버전이 명시적으로만 호출되도록 보장하고 초기화 목록 생성자는 기본적으로 동종 중괄호 목록을 처리하도록 합니다.