openlang

성공 경로에서 샌 errdefer 해제

테스트에서 메모리가 샜는데, 새는 곳이 성공한 경로였습니다. 함수는 임시 버퍼를 할당하고, 호출자가 소유할 결과로 파싱한 뒤, 임시 버퍼를 errdefer allocator.free(scratch)로 해제했습니다. 오류 경로는 전부 해제합니다. 성공 반환은 해제하지 않았고, 테스트가 끝날 때 std.testing.allocator가 누수를 보고했습니다.

레퍼런스는 이 점에서 정확합니다. errdefer는 함수가 오류로 반환할 때, 그리고 그때만 블록을 빠져나가며 식을 실행합니다. 평범한 반환은 오류 반환이 아닙니다.

errdefer가 맞는 경우는 따로 있습니다. 성공하면 호출자 것이 되고 실패할 때만 해제해야 하는 버퍼입니다. 임시 버퍼는 어느 경로에서든 함수 것입니다. 그건 defer입니다.

그러니 이 키워드는 성공 반환 뒤에 할당을 누가 소유하는지를 기록합니다. errdefer는 호출자라고 말합니다. defer는 함수 밖의 누구도 아니라고 말합니다. 하나를 고르는 일은 정리 스타일이 아니라 소유권 결정입니다. C에서는 같은 선택이 goto 정리 블록에서 성공 경로가 어느 레이블을 건너뛰는지에 숨어 있습니다.

이어지는 대화 3

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

    이건 테스트가 우연히 잡기 전에 두 번의 실행으로 잡을 수 있었어요. 그중 하나만 보통 쓰는 실행이에요.

    1. std.testing.allocator로 성공 경로를 돌리기. why17 님 누수를 찾은 게 이거고, defer를 모두 확인해요.
    2. 그 위에 std.testing.FailingAllocator를 씌워서 첫 번째 할당, 그다음 두 번째 할당을 실패시키는 식으로 실패 경로를 돌리기. 매번 누수도 확인해요. errdefer를 확인하는 건 이쪽이에요.

    둘을 섞어 쓰는 함수라면 둘 다 필요하고, 두 번째는 함수의 할당 횟수만큼 돌려야 해요. 돌리기 전에 호출당 할당 횟수, 성공하면 호출자가 소유하는 할당, 몇 번째 실패에서 어떤 errdefer가 실행돼야 하는지를 적어 둘 거예요. 이 함수로는 아직 둘 다 안 돌려 봤어요.

  2. 고유 링크

    성공 반환 뒤의 소유자를 먼저 정하고, 그다음에 문을 고르면 됩니다. 이 순서는 정리용 키워드가 있는 언어에서든 없는 언어에서든 통합니다.

    제가 Zig보다 자주 쓰는 Odin에는 defer만 있고, 조건부 해제는 defer if cond { ... }로 써서 조건이 눈에 보이는 자리에 놓입니다.

    실제 영향은 문서에 있습니다. 해제를 errdefer에 둔 함수는 성공하면 소유권을 넘기므로, 문서 주석에 호출자가 결과를 해제해야 한다는 것과 어느 할당자로 해제하는지를 적어야 합니다. 그 한 줄이 없으면 정리 코드를 읽는 것 말고는 알 방법이 없습니다.

  3. 고유 링크

    nullbyte 님 순서대로 파일의 나머지를 훑었습니다. errdefer가 다섯 개였습니다. 네 개는 성공하면 할당을 호출자에게 넘기고, 이제 문서 주석에 할당자까지 적혀 있습니다. 다섯 번째는 함수가 만들었다가 버리는 임시 해시 맵을 해제하고 있었습니다. 본문의 버그가 또 있었던 것이고, 그 분기는 키가 중복된 입력에서만 실행되어서 어떤 테스트도 거치지 않았습니다. 지금은 defer입니다.

    그다음 trial27 님이 말한 실패 루프를 돌렸습니다. std.testing.FailingAllocator의 fail_index를 0부터 함수의 할당 횟수까지 올리면서, 매번 누수 검사를 걸었습니다. 감사를 마쳤으니 아무것도 안 나올 거라 생각했습니다. 인덱스 1에서 샜습니다.

    const keys = try allocator.alloc(u32, n);
    const vals = try allocator.alloc(u32, n);
    errdefer allocator.free(keys);
    errdefer allocator.free(vals);

    감사는 errdefer 하나하나를 읽고 성공 뒤에 메모리를 누가 소유하는지 물었고, 네 답은 모두 맞았습니다. 각각이 언제 등록되는지는 묻지 않았습니다. 두 번째 alloc이 실패하면 첫 번째 errdefer가 생기기도 전에 try가 반환하므로, 감사가 안전하다고 본 경로에서 keys가 샙니다. 고치는 방법은 errdefer를 각자 맡은 할당 바로 아래에 두는 것입니다.

    실패 할당자 루프는 이제 할당마다 한 번씩 테스트 파일에 들어가 있고, 다음에 이런 파일을 감사할 때는 그걸 먼저 돌립니다.

같은 질문의 다른 갈래

  1. TaskGroup으로 옮긴 뒤 `except ValueError`가 잡지 못한다

    작은 점수 계산 스크립트를 asyncio.gather에서 asyncio.TaskGroup으로 옮겼더니 대체 경로가 동작하지 않아요. 바꾸기 전에는 가중치 응답이 비어 있으면 가져오는 쪽에서 ValueError가 났고, exc…

    답글 5python
  2. `String::from_utf8`는 실패하면 바이트를 돌려준다

    값을 소비하는 API는 실패했을 때 그 값이 어떻게 되는지 정해야 합니다. String::from_utf8는 Vec<u8>를 값으로 받고, 실패하면 오류가 그것을 소유합니다. 출력은 bad byte at 2, 4 bytes…

    답글 0rust
  3. use 체인 줄마다 붙는 `result.map_error`

    use와 result.try 조합은 Gleam에서 제일 좋아하는 부분인데, 요즘 조금 미워지기 시작했어요. 단계마다 다른 모듈을 부르고 모듈마다 자기 오류 타입이 있으니, 줄마다 result.map_error를 달고 다녀요.…

    답글 1gleam