openlang

카탈로그를 다시 읽으면 무엇이 남습니까?

슬라이스는 주소와 길이일 뿐입니다. 둘 중 어느 것도 뒤에 있는 할당을 살려 두지 않고, Zig 0.15.2 레퍼런스도 포인터의 유효성과 수명은 프로그래머가 책임진다고 분명히 적고 있습니다. 함수가 슬라이스를 반환한다고 저장 공간의 수명이 늘어나지는 않습니다.

그래서 카탈로그의 names 호출 문서에는 빌려주는 것이 누구 소유인지 적혀 있어야 합니다. 여기서는 카탈로그가 할당을 소유하고, 호출자는 그 뷰를 해제하지 않습니다. 카탈로그를 파괴하면 모든 뷰가 무효가 됩니다. 새 저장 공간으로 바꿔 끼우는 리로드가 성공해도 이전 뷰는 전부 무효가 됩니다. 이름을 추가할 때 저장 공간이 커지면서 옮겨질 수 있다면 추가 연산에도 같은 경고가 필요합니다. 슬라이스를 복사하면 서술자가 복사될 뿐 이름은 복사되지 않습니다.

실패는 따로 한 문장을 받아야 합니다. 교체본을 먼저 만들고 성공했을 때만 설치하는 리로드라면, 준비가 실패해도 이전 할당과 그 뷰는 유효하게 남습니다. 이전 저장 공간부터 비우는 구현은 그런 약속을 할 수 없으니 문서에 그렇게 밝혀야 합니다.

다른 형태도 있습니다. 할당한 복사본을 돌려주고, 문서에 적힌 할당자로 호출자가 해제하게 하는 방식입니다. 수명은 독립적이 되지만 호출하는 곳마다 소유권 관리가 늘어납니다. 어느 쪽이 나은지는 호출자가 실제로 어떻게 읽고, 리로드하고, 정리하는지에 달려 있는데, 그 흐름은 아직 추적해 보지 않았습니다.

이어지는 대화 6

서로의 근거를 따라
  1. 고유 링크

    Swift로 감싸도 생각만큼 얻는 게 없어요. 강한 참조는 카탈로그 인스턴스를 살려 두긴 해요. 그런데 리로드는 그 살아 있는 인스턴스 안에서 돌면서 밑의 저장 공간을 갈아 끼워요. 약한 참조는 nil이 될 수 있고요. unowned는 카탈로그가 더 오래 산다고 단언하는 거라서, 그 수명 근거는 여전히 직접 대야 해요. 셋 중 어느 것도 이름들이 한 주소에 머문다고 말해 주지 않아요. ARC가 추적하는 건 클래스 인스턴스가 언제 사라지느냐예요. 인스턴스가 빌려준 저장 공간은 다른 문제라서, 말씀하신 무효화 규칙은 Swift 쪽에서도 그대로 적용돼요.

  2. 고유 링크

    리로드가 시작될 때 아직 진행 중인 읽기는 어느 것입니까? 실제 문제는 그 겹침이고, nell 님 말처럼 인스턴스를 살려 두는 것으로는 거기까지 닿지 않습니다. Rust에서는 이것이 그대로 드러납니다. 벡터에서 &[u8]를 빌리고 push를 호출한 뒤 그 슬라이스를 다시 쓰면 컴파일되지 않습니다. 용량이 충분해서 아무것도 옮겨지지 않았더라도 마찬가지입니다. 공유 대여가 살아 있는 동안 push가 &mut self를 요구하기 때문입니다. 소유자를 살려 두는 것은 다른 질문에 대한 답입니다. 저렴한 방법은 두 가지입니다. 수정 전에 읽기를 끝내거나, 교체 뒤에도 남아야 하는 작은 조회 키를 복사하는 것입니다. 그 정도 크기의 키라면 복사 비용은 받아들일 만합니다.

  3. 고유 링크

    읽기 순서를 정하면 겹침은 해결된다. 생성 도중에 실패하는 교체본에는 아무 효과가 없다.

    C++ 교체용 카탈로그를 하나 잡자. 멤버는 선언 순서대로 파일 소유자, 인덱스 소유자, 버퍼다. 버퍼의 생성자가 예외를 던진다. 버퍼의 하위 객체 중 생성이 끝난 것은 파괴된다. 이어서 인덱스 멤버, 그다음 파일 멤버가 생성 완료의 역순으로 파괴된다. 카탈로그의 소멸자는 실행되지 않는다. 카탈로그의 생성이 끝나지 않았기 때문이다.

    따라서 카탈로그 소멸자만 해제해 주던 원시 핸들은 샌다. 획득한 자원은 모두 생성이 끝난 멤버가 쥐고 있어야 한다. 그렇게 해도 되감기가 정리하는 것은 새 객체뿐이다. 이전 카탈로그의 불변 조건은 생성이 성공한 뒤에 교체할 때만 유지된다.

  4. 고유 링크

    그런데 키를 어디로 복사하나? Odin에서는 목적지가 결정한다. context.allocator가 임시 할당자인 동안 동적 배열을 만들면 배열은 그 할당자를 기억한다. 나중에 원래 컨텍스트로 돌려놔도 배열에는 달라지는 게 없고, 다음 확장도 여전히 임시 쪽으로 간다. 요청이 끝날 때 free_all(context.temp_allocator)를 부르면 뒤의 메모리와 그 안을 가리키던 뷰가 함께 사라진다. 키는 리로드에서는 살아남았지만 요청과 함께 죽었다. 마지막으로 읽는 쪽보다 늦게 비워지는 저장 공간에 넣어야 한다.

  5. 고유 링크

    놓치기 쉬워서 적어 둡니다. 배열을 더 오래 사는 할당자로 옮기면 뷰가 옮겨집니다. 바이트는 아닙니다. 각 항목은 여전히 임시 저장 공간을 가리키는 주소와 길이입니다.

    그러니 소유자가 둘입니다. 바깥 컬렉션의 소유자, 그리고 이름마다 바이트의 소유자입니다. 어느 쪽도 임시 할당자가 비워지지 않기를 기대하면 안 됩니다. 예전에 올린 FFI 버퍼 질문에서는 Vec<u8>가 바이트를 직접 소유했기 때문에 이 문제가 없었습니다. 이름 카탈로그에는 층이 하나 더 있습니다. 복사 경로가 실제로 둘 다 복사하는지는 아직 제 가정입니다.

  6. 고유 링크

    재밌게 깨지는 경우가 있어요. 이름 바이트는 전부 복사돼서 살아 있는데, 서술자 하나가 아직 이전 세대를 가리키고 있다면요. 그 뷰는 하나도 독립적이지 않아요. 이전 세대를 해제하면 예전과 똑같이 허공을 가리켜요. 그러니 복사에는 두 약속 중 하나가 따라와야 해요. 모든 서술자를 새 저장 공간 기준으로 다시 만들거나, 마지막 뷰가 없어질 때까지 이전 세대를 붙잡아 두거나요. 두 번째 약속은 보기보다 커요. 카탈로그가 슬그머니 세대를 가진 물건이 되거든요.

같은 질문의 다른 갈래

  1. realloc 결과를 유일한 포인터에 바로 넣으면 실패 시 메모리가 샌다

    버퍼를 늘리는 함수에서 buffer = realloc(buffer, n) 같은 줄이 자주 보이는데, 이 줄은 분명한 지점에서 틀렸습니다. 할당에 실패하면 유일하게 소유한 포인터가 널로 덮어써집니다. C11 초안에 따르면 이때…

    답글 2clua