Lobsters

My C23 Conventions

내가 C23에서 쓰는 세 가지 관례

C23을 적용한 C 프로젝트에서 [[maybe_unused]], 빈 초기화자 {}, 고정 기반 열거형을 쓰는 관례를 소개합니다. 반환값을 명시적으로 무시하고, union 초기화와 비트 플래그 열거형에서 생길 수 있는 혼란을 줄이는 방법입니다.

에디터 노트

가장 구체적인 이점은 기존 C 코드에서 의도를 표시하기 번거로웠던 두 지점에 C23 문법을 적용한다는 데 있습니다. [[maybe_unused]]는 결과를 일부러 버리는 호출을 드러내고, {}는 {0}과 다른 초기화 의미를 제공합니다. 다만 댓글에서 지적하듯 union 멤버 접근은 초기화 표기만으로 끝나지 않습니다. 마지막으로 쓴 멤버에 관한 규칙도 남습니다. enum의 기반형 지정과 constexpr에 대한 반응은 C23의 활용 범위를 보여주지만, Clang 지원 여부처럼 기능별 구현 차이도 댓글에서 쟁점으로 나옵니다. 따라서 이 사례가 뒷받침하는 건 C23 문법의 편의와 명시성이지, 버전 전환만으로 모든 함정이 사라진다는 주장은 아닙니다.

AI 요약

작성자는 C23을 빌드에 적용한 뒤 자연스럽게 자리 잡은 세 가지 코딩 관례를 소개합니다. 기존에는 C를 엄격한 표준 옵션으로만 쓰려 했지만, 사용자가 있는 프로젝트를 유지하며 실수를 줄이는 쪽으로 관점을 바꿨다고 설명합니다.

반환값을 의도적으로 무시하기

반환값 확인이 필요한 함수에는 [[nodiscard]]를 붙여 호출자가 결과를 놓치지 않게 합니다. 하지만 그 결과가 필요 없는 호출에서는 반환값을 담은 변수가 -Werror 설정 때문에 경고를 냅니다. 예전에는 UNUSED() 매크로로 변수를 처리했지만, C23의 [[maybe_unused]]를 붙인 변수를 쓰면 매크로 없이 경고를 막을 수 있습니다. 반환형이 다른 함수에도 적용하도록 [[maybe_unused]] auto _ = foo(); 형태를 제안합니다. 댓글에서 Zardium은 고유한 변수 이름을 만드는 매크로를 덧붙였고, 작성자는 매크로를 없애려는 취지와는 다르다며 웃어넘깁니다.

union을 포함한 구조체 초기화

작성자는 구조체에 일부 지정 초기자를 쓰거나 {0}으로 초기화해도 union 내부의 모든 멤버가 0으로 초기화된다고 믿으면 안 된다고 설명합니다. 예시의 stamp_t에는 시간, 실수, 날짜 타입을 담는 익명 union이 들어 있습니다. C23의 빈 초기화자 {}를 쓰면 전체를 초기화할 수 있다고 소개합니다. 댓글의 accelbread는 표준이 union의 첫 멤버 초기화만 요구하며, GCC와 Clang의 초기화 동작도 달랐다고 덧붙입니다. 또한 C23의 {}와 기존 {0}은 의미가 다르며, union을 직접 배치할 수 있다면 가장 큰 멤버를 첫 번째에 두는 편이 안전하다고 말합니다.

열거형의 기반 정수형 지정

비트 플래그를 열거형 상수로 만들 때 상수가 signed int 범위를 벗어나면 열거형의 정수형이 상수를 담을 수 있도록 조정될 수 있습니다. 작성자는 이를 컴파일러가 잘못된 비트 이동을 경고해 줄 것이라는 기대와 대비합니다. C23에서는 enum options : int처럼 기반 정수형을 명시해 열거형의 표현 범위를 지정할 수 있다고 설명합니다.

댓글 반응

  • @tomsmeding — 몇 년 전에는 C의 어떤 종류의 0 초기화도 심하게 불신했습니다. 어떤 마법 같은 표기({}인지 {0}인지, 예외가 있었는지)를 써야 하는지 기억할 수 없어서 변수 선언 뒤에 명시적으로 memset을 쓰곤 했습니다. 그러다 지쳐서 올바른 문법을 찾아 {0}을 쓰기 시작했습니다. 이제 문제는 내가 아니라 C일지도 모르겠다는 생각이 듭니다.
    • @abnercoimbre — {0}이 구조체 안의 union 멤버까지 반드시 0으로 초기화하지 않는 건 정말 실수입니다. C 표준은 컴파일러가 첫 번째 멤버를 0으로 초기화하도록만 요구합니다. 그러니 union에서 첫 번째 멤버가 나머지보다 크도록 배치하세요. 더 황당한 건 GCC는 전체를 0으로 초기화하고 Clang은 그렇지 않다가 작년에 서로 동작을 뒤집었다는 점입니다. 대체 무슨 일이 벌어지는 걸까요? C23은 {}로 이 문제를 해결하니 이제 어디서나 쓰고 있습니다. 그런데 안에 0이 들어간 {0}이 제대로 동작하지 않을 수 있다는 건 이상합니다. 표준위원회는 기존 의미를 바꾸지 않으려는 것 같지만, auto에서는 그렇게 했잖아요.
    • @accelbread — {0}을 C23의 {}와 똑같이 만드는 건 특별한 예외를 두는 셈입니다. 구조체의 {0}은 첫 번째 멤버를 0으로 설정하고 나머지는 기본 초기화합니다. {1}도 첫 번째 멤버를 1로 설정하고 나머지는 기본 초기화합니다. union을 {1}로 초기화하면 첫 번째 멤버를 선택하고 그 멤버를 1로 설정합니다. C23 이전에는 {}가 없었기 때문에 모두 {0}으로 초기화하는 관례가 생겨 이런 함정이 생겼다고 봅니다. 표준을 엄밀히 따르면 마지막으로 값을 쓴 멤버를 통해서만 union에 접근할 수 있으므로, {0}으로 초기화한 뒤 다른 멤버에 값을 쓰지 않고 접근하면 정의되지 않은 동작일 수 있습니다. 다만 첫 부분이 같은 union 멤버를 다루는 예외가 있을 수 있습니다.
  • @nytpu — constexpr가 진정한 컴파일 시점 상수식을 제공해서 거의 C23만 씁니다. 정말 오래전부터 필요했던 기능입니다. 실제 언어 기능이고 디버거에도 나타나는 익명 열거형으로 정수 상수를 흉내 내야 하는 점이 늘 싫었습니다. 다른 종류의 상수에는 전처리기를 써야 했습니다. const 객체는 변경할 수 없을 뿐 진짜 컴파일 시점 상수가 아니므로 정적 배열 길이 같은 곳에 항상 쓸 수 없습니다.
    • @confusomu — 그렇습니다. C23의 constexpr 상수식은 정말 좋습니다. 여기에 static_assert를 쓰면 전처리기와 주변 설정을 곁들여 컴파일 시점 검사를 많이 할 수 있습니다. C를 더 쓰기 편하게 만들고, 이런 컴파일 시점 기법 덕분에 간단한 경우 C++를 찾지 않아도 됩니다.
    • @accelbread — constexpr 복합 리터럴이 Clang에서 컴파일되지도 않고 구문 분석조차 되지 않는 점이 정말 짜증 납니다. 전역 constexpr 상수도 C2Y 전까지는 사용할 수 없습니다. constexpr 변수가 암묵적으로 inline이고, 비정적 inline 함수가 정적 전역 변수에 의존하면 컴파일러가 오류를 내야 하기 때문입니다.
    • @pervognsen — constexpr에서 복합 리터럴을 제한한 데에는 이유가 있는 것 같습니다. Clang을 예외처럼 언급했지만 실제로 C23에서 지원하는 기능이 아닌 것 아닌가요? C99 복합 리터럴은 lvalue이므로 식별성을 갖습니다. Rust에서 const를 사용하는 방식도 식별성에 암묵적으로 의존하면 놀라운 결과를 낼 수 있고, 특히 내부 가변성이 관련되면 그렇습니다. 위에서 말한 암묵적 inline 의미도 고려하면 위원회가 첫 도입부터 이를 지원하지 않는 이유를 짐작할 수 있습니다.
  • @alterae — 결국 전부 그냥 "C23을 쓰자"로 귀결되는 점이 마음에 듭니다.

원문: Abner / 번역·요약: Trawling