헤더 지옥을 어떻게 방지 할 수 있습니까?


44

우리는 처음부터 새로운 프로젝트를 시작하고 있습니다. 각각 4 개 또는 5 개의 소스 파일이있는 약 8 개의 개발자, 12 개 정도의 서브 시스템.

"헤더 지옥", 일명 "스파게티 헤더"를 방지하기 위해 무엇을 할 수 있습니까?

  • 소스 파일 당 하나의 헤더?
  • 서브 시스템 당 하나 더하기?
  • 함수 프로토 타입에서 typdef, stucts 및 enum을 분리합니까?
  • 서브 시스템 내부를 서브 시스템 외부와 분리합니까?
  • 헤더 또는 소스가 독립형 컴파일 가능해야하는지 여부에 관계없이 모든 단일 파일을 주장합니까?

나는“최상의”방법을 요구하지 않고, 무엇을주의해야하는지, 슬픔을 유발할 수있는 것에 대한 포인터를 가리 키므로, 우리는 그것을 피하려고 노력할 수 있습니다.

이것은 C ++ 프로젝트가 될 것이지만, C 정보는 미래의 독자들에게 도움이 될 것입니다.


16
Large-Scale C ++ Software Design 의 사본을 얻으십시오. 헤더와 관련된 문제를 피하는 것이 아니라 C ++ 프로젝트에서 소스 및 객체 파일 간의 물리적 종속성과 관련된 더 많은 문제가 있습니다.
Doc Brown

6
여기의 모든 대답은 훌륭합니다. 객체, 메소드, 함수 사용에 대한 문서가 헤더 파일에 있어야한다고 덧붙이고 싶습니다. 여전히 소스 파일에 doc이 있습니다. 소스를 읽지 마십시오. 그것이 헤더 파일의 요점입니다. 구현자가 아닌 한 소스를 읽을 필요는 없습니다.
Bill Door

1
나는 당신과 함께 일한 적이 있다고 확신합니다. 종종 :-(
Mawg

5
당신이 설명하는 것은 큰 프로젝트가 아닙니다. 좋은 디자인은 언제나 환영하지만 "대규모 시스템"문제에 직면하지는 않을 것입니다.
Sam

2
Boost는 실제로 모두 포함 접근법을 가지고 있습니다. 각 개별 기능에는 고유 한 헤더 파일이 있지만 더 큰 각 모듈에는 모든 것을 포함하는 헤더도 있습니다. 이것은 매번 수백 개의 파일을 #include하지 않아도 헤더-헬을 최소화 할 수있는 강력한 기능입니다.
Cort Ammon

답변:


39

간단한 방법 : 소스 파일 당 하나의 헤더. 사용자가 소스 파일에 대해 알지 못할 완전한 서브 시스템이있는 경우, 모든 필수 헤더 파일을 포함하여 서브 시스템에 대한 하나의 헤더가 있습니다.

모든 헤더 파일은 자체적으로 컴파일 가능해야합니다 (또는 단일 헤더를 포함하는 소스 파일을 컴파일해야한다고합시다). 원하는 헤더 파일이 포함 된 헤더 파일을 찾은 다음 다른 헤더 파일을 찾아야하면 고통입니다. 이것을 시행하는 간단한 방법은 모든 소스 파일에 헤더 파일을 먼저 포함시키는 것입니다 (doug65536 덕분에 대부분의 시간을 깨닫지도 않고 생각합니다).

사용 가능한 도구를 사용하여 컴파일 시간을 줄이십시오. 각 헤더는 한 번만 포함되어야하고, 사전 컴파일 된 헤더를 사용하여 컴파일 시간을 줄이십시오. 가능한 경우 사전 컴파일 된 모듈을 사용하여 컴파일 시간을 더 줄이십시오.


까다로운 부분은 다른 서브 시스템에서 선언 된 유형의 매개 변수와 함께 서브 시스템 간 함수 호출입니다.
Mawg

6
까다로운, "#include <subsystem1.h>"컴파일해야합니다. 어떻게 당신이 그것을 달성. @ FrankPuffer : 왜?
gnasher729

13
@Mawg 이는 개별 서브 시스템의 공통성을 포함하는 별도의 공유 서브 시스템이 필요하거나 각 서브 시스템에 대해 단순화 된 "인터페이스"헤더가 필요함을 나타냅니다 (이는 구현 헤더가 내부 및 교차 시스템 모두에서 사용됨). . 교차 포함없이 인터페이스 헤더를 작성할 수 없으면 서브 시스템 설계가 엉망이되어 서브 시스템이 더 독립적이되도록 항목을 재 설계해야합니다. (공통 서브 시스템을 세 번째 모듈로 끌어 오는 것을 포함 할 수 있습니다.)
RM

8
헤더를 독립적으로 유지하는 좋은 기술은 소스 파일이 항상 자체 헤더를 먼저 포함한다는 규칙을 따르는 것 입니다. 의존성 포함을 구현 파일에서 헤더 파일로 이동 해야하는 경우를 포착합니다.
doug65536

4
@FrankPuffer : 댓글이 문맥에 맞지 않게되므로 댓글을 삭제하지 마십시오. 당신은 항상 새로운 의견으로 진술을 수정할 수 있습니다. 감사! 나는 당신이 실제로 무엇을 말했는지 알고 싶습니다. 그러나 이제 사라졌습니다. (
MPW

18

지금까지 가장 중요한 요구 사항은 소스 파일 간의 종속성을 줄이는 것입니다. C ++에서는 클래스 당 하나의 소스 파일과 하나의 헤더를 사용하는 것이 일반적입니다. 따라서 좋은 클래스 디자인을 보유하고 있다면 헤더 지옥에 가까워지지 않을 것입니다.

다른 방법으로도 볼 수 있습니다. 프로젝트에 이미 헤더 지옥이있는 경우 소프트웨어 디자인을 개선해야합니다.

특정 질문에 대답하려면 다음을 수행하십시오.

  • 소스 파일 당 하나의 헤더? → 그렇습니다. 대부분의 경우 잘 작동하며 쉽게 찾을 수 있습니다. 그러나 종교로 만들지 마십시오.
  • 서브 시스템 당 하나 더하기? → 아니요, 왜 이렇게 하시겠습니까?
  • 함수 프로토 타입에서 typdef, stucts 및 enum을 분리합니까? → 아니오, 기능과 관련 유형이 함께 속해 있습니다.
  • 서브 시스템 내부를 서브 시스템 외부와 분리합니까? → 물론입니다. 이것은 의존성을 줄입니다.
  • 독립 실행 형의 헤더 또는 소스에 관계없이 모든 단일 파일이 호환 가능하다고 주장합니까? → 예, 다른 헤더 앞에 헤더를 포함하지 않아도됩니다.

12

의존성을 줄인 다른 권장 사항 외에도 (주로 C ++에 적용 가능) :

  1. 실제로 필요한 것, 필요한 곳에만 포함하십시오 (가장 낮은 수준). 예 : 소스에서만 호출이 필요한 경우 헤더에 포함하지 마십시오.
  2. 가능한 경우 헤더에 순방향 선언을 사용하십시오 (헤더에는 포인터 또는 다른 클래스에 대한 참조 만 포함됨).
  3. 각 리팩토링 후 포함을 정리하십시오 (설명 제거, 컴파일 실패 위치 참조, 이동, 주석 처리 된 포함 행 제거).
  4. 공통 파일을 너무 많은 파일을 같은 파일에 넣지 마십시오. 기능별로 분류하십시오 (예 : Logger는 하나의 클래스이므로 하나의 헤더와 하나의 소스 파일; SystemHelper dito 등).
  5. OO 원칙을 고수하십시오. 모두 독립형 함수 대신 정적 메소드로만 구성된 클래스이거나 네임 스페이스를 대신 사용하십시오 .
  6. 특정 공통 기능의 경우 싱글 톤 패턴은 관련이없는 다른 객체에서 인스턴스를 요청할 필요가 없으므로 다소 유용합니다.

5
# 3에서는 추측 및 확인 수동 재 컴파일 방식을 피하면서 사용 도구에 도움이 될 수 있습니다.
RM

1
이 상황에서 싱글 톤의 이점이 무엇인지 설명해 주시겠습니까? 나는 정말로 그것을 얻지 못한다.
Frank Puffer

@FrankPuffer 나의 이론적 근거는 이것이다 : 싱글 톤이 없다면 클래스의 어떤 인스턴스는 보통 Logger와 같은 헬퍼 클래스의 인스턴스를 소유한다. 세 번째 클래스에서 클래스를 사용하려면 소유자에게 도우미 클래스에 대한 참조를 요청해야합니다. 즉, 두 클래스를 사용하고 물론 사용자가 소유자와 비즈니스가없는 경우에도 헤더를 포함해야합니다. 싱글 톤을 사용하면 도우미 클래스의 헤더 만 포함하면되고 인스턴스에서 직접 인스턴스를 요청할 수 있습니다. 이 논리에 결함이 있습니까?
머피

1
# 2 (앞으로 선언)는 컴파일 시간과 종속성 검사에 큰 차이를 만들 수 있습니다. 이 답변 (AS stackoverflow.com/a/9999752/509928 ) 쇼, 그것은 C에 ++와 C에 모두 적용
데이브 콤프 턴

3
함수가 인라인으로 정의되지 않은 한 값으로 전달할 때 전달 선언을 사용할 수 있습니다. 함수 선언은 전체 유형 정의가 필요한 컨텍스트가 아닙니다 (함수 정의 또는 함수 호출은 이러한 컨텍스트입니다).
StoryTeller-Unslander Monica

6

소스 파일 당 하나의 헤더. 소스 파일이 구현 / 내보내는 대상을 정의합니다.

필요한만큼 많은 헤더 파일이 각 소스 파일에 포함됩니다 (자체 헤더로 시작).

순환 종속성을 피하기 위해 다른 헤더 파일 내에 헤더 파일을 포함시키지 마십시오. 자세한 내용은 "C ++를 사용하여 두 클래스가 서로를 볼 수 있습니까?"에 대한 이 답변을 참조하십시오.

이 주제에 관한 책 은 Lakos의 대규모 C ++ 소프트웨어 디자인 입니다. 소프트웨어의 "계층"을 설명합니다. 상위 계층은 하위 계층이 아닌 하위 계층을 사용하므로 순환 종속성을 피할 수 있습니다.


4

나는 두 가지 종류의 헤더 지옥이 있기 때문에 귀하의 질문에 근본적으로 답할 수 없다고 주장합니다.

  • 백만 가지의 다른 헤더를 포함 해야하는 종류와 도대체 누가 헤더를 모두 기억할 수 있습니까? 그리고 그 헤더 목록을 유지합니까? 어.
  • 한 가지를 포함하고 바벨탑 전체를 포함시킨 것을 발견하십시오 (또는 부스트 타워라고 말해야합니까? ...)

문제는 전자를 피하려고하면 결국 후자를 피하고 그 반대도 마찬가지입니다.

순환 의존성 인 세 번째 종류의 지옥도 있습니다. 조심하지 않으면 이러한 메시지가 나타날 수 있습니다. 피하는 것이 매우 복잡하지는 않지만 시간을내어이를 수행하는 방법에 대해 생각해야합니다. CppCon 2016의 레벨 화에 관한 John Lakos의 이야기 (또는 슬라이드 )를 참조하십시오.


1
순환 종속성을 항상 피할 수는 없습니다. 예는 엔터티가 서로를 참조하는 모델입니다. 최소한 서브 시스템에서 순환을 제한하려고 시도 할 수 있습니다. 이는 서브 시스템의 헤더를 포함하면 순환을 추상화했음을 의미합니다.
nalply 2012

2
@nalply : 코드가 아닌 헤더의 순환 종속성을 피하는 것을 의미했습니다 ... 원형 헤더 종속성을 피하지 않으면 아마도 빌드 할 수 없습니다. 하지만 그렇습니다. +1입니다.
einpoklum-복직 모니카

1

디커플링

결국 컴파일러와 링커의 특성에 대한 뉘앙스가없는 가장 근본적인 디자인 수준에서 하루가 끝나면 나에게 분리하는 것입니다. 나는 각 헤더가 하나의 클래스를 정의하게하고, pimpl을 사용하고, 선언 해야하는 유형으로 선언을 전달하고, 정의되지 않고, 선언을 포함하는 헤더를 사용할 수도 있습니다 (예 :), <iosfwd>소스 파일 당 하나의 헤더 선언 / 정의 된 것의 유형 등에 따라 시스템을 일관되게 구성하십시오.

"컴파일 타임 종속성"을 줄이는 기술

또한 일부 기술은 상당히 도움이 될 수 있지만 이러한 방법을 모두 사용하면서도 시스템의 평균 소스 파일은 2 페이지의 프리앰블이 필요하다는 것을 알 수 있습니다. #include인터페이스 디자인에서 논리적 종속성을 줄이지 않고 헤더 수준에서 컴파일 시간 종속성을 줄이는 데 너무 많은 초점을두면 "스파게티 헤더"로 간주되지 않을 수 있습니다. 여전히 실제 생산성과 유사한 해로운 문제로 해석된다고한다. 하루가 끝날 무렵 컴파일 단위에 여전히 많은 양의 정보가 표시되어 무언가를 수행 해야하는 경우 빌드 시간이 길어지고 개발자를 만드는 동안 잠재적으로 돌아가서 변경 해야하는 이유가 배가됩니다. 매일 코딩을 끝내려고 시스템을 방해하는 것처럼 느껴집니다. 그것'

예를 들어, 각 서브 시스템이 하나의 매우 추상적 인 헤더 파일과 인터페이스를 제공하게 할 수 있습니다. 그러나 서브 시스템이 서로 분리되어 있지 않으면 작동하기 위해 혼란스러워 보이는 종속성 그래프가있는 다른 서브 시스템 인터페이스에 따라 서브 시스템 인터페이스와 다시 스파게티를 닮은 것을 얻게됩니다.

외부 유형으로 선언 전달

개발자가 빌드 서버에서 CI 전환을 위해 2 일을 기다리는 동안 2 시간이 걸렸던 이전 코드베이스를 얻으려고 노력한 모든 기술 중에서 (빌드 머신이 열렬한 노력의 짐승이라고 생각할 수 있음) 개발자가 변경 사항을 푸시하는 동안 계속 실패하면) 다른 질문에 정의 된 형식을 선언하는 것이 가장 의문이었습니다. 그리고 코드베이스를 40 분 정도 줄이려고했지만, 조금씩 단계적으로이 작업을 수행 한 후 뒷받침에서 가장 의심스러운 "헤더 스파게티"를 줄이려고 노력했습니다. 터널 상호 의존성에 대한 터널 비전을 고려한 설계)는 다른 헤더에 정의 된 형식을 선언하는 것입니다.

Foo.hpp다음과 같은 헤더 를 상상하면 :

#include "Bar.hpp"

그리고 그것은 Bar정의가 아닌 선언이 필요한 방식으로 만 헤더에 사용 합니다. 헤더에 class Bar;정의가 Bar표시 되지 않도록 선언 하는 것은 쉬운 일 이 아닙니다 . 실제로는 종종 사용 Foo.hpp하지만 대부분의 컴파일 단위는 여전히 자신 Bar을 포함 해야하는 추가 부담으로 정의 해야 할 필요 가 있거나 실제로 도움이되는 다른 시나리오를 실행합니다. 컴파일 단위의 %는 포함하지 않고 작동 할 수 있습니다 . 단, 다음 과 같은 선언문을 볼 필요가있는 이유와 이유에 대한보다 근본적인 디자인 질문 (또는 적어도 요즘은 생각합니다)을 제기합니다.Bar.hppFoo.hppBar.hppBarFoo 대부분의 유스 케이스와 관련이없는 경우 그것에 대해 알고 귀찮게해야합니다 (왜 거의 사용하지 않은 다른 것에 의존하는 디자인에 부담을 주어야합니까?).

개념적으로하기 때문에 우리가 정말 분리하지 않은 Foo에서 Bar. 우리는 방금 헤더가 Foo에 대한 헤더 정보를 많이 필요로하지 않도록 만들었습니다. Bar이 두 가지를 완전히 독립적으로 만드는 디자인만큼 중요하지는 않습니다.

임베디드 스크립팅

이것은 실제로 더 큰 규모의 코드베이스를위한 것이지만, 내가 유용하게 생각하는 또 다른 기술은 적어도 시스템의 가장 높은 수준의 부분에 대해 임베디드 스크립팅 언어를 사용하는 것입니다. 나는 하루에 Lua를 포함시킬 수 있었고 시스템의 모든 명령을 균일하게 호출 할 수 있음을 알았습니다 (명령은 추상적이었습니다). 불행히도 나는 개발자들이 다른 언어의 도입을 거부하고 가장 기괴하게도 가장 큰 의혹으로 성능을 발휘하는 장애물에 부딪쳤다. 그러나 다른 관심사를 이해할 수는 있지만 사용자가 버튼을 클릭 할 때 명령을 호출하기 위해 스크립트를 사용하는 경우에만 성능이 문제가되지 않아야합니다. 버튼 클릭에 대한 응답 시간의 나노초 차이에 대해 걱정하십니까?).

한편 내가 큰 코드베이스에서 컴파일 시간을 단축하는 기술을 소진 한 후 목격 한 가장 효과적인 방법은 아키텍처입니다 진정으로 작업에 시스템의 모든 한 가지에 필요한 정보의 양을 줄이고, 그냥 컴파일러에서 서로 하나의 헤더를 감하지 (인간과 컴파일러의 관점에서 볼 때 컴파일러 종속성을 넘어서는 진정한 분리) 최소한의 인터페이스를 알고 있어야합니다.

ECS는 단지 하나의 예일뿐입니다 (하지만 나는 당신이 하나를 사용하라고 제안하지는 않습니다). 그러나 그것을 만나면 ECS가 자연은 시스템이 ECS 데이터베이스에 대해서만 알고 있으면되고 일반적으로 소수의 구성 요소 유형 (때로는 하나만)만으로도 매우 분리 된 아키텍처를 만듭니다.

여기에 이미지 설명을 입력하십시오

디자인, 디자인, 디자인

그리고 이러한 종류의 분리 된 건축 설계는 사람의 개념적 수준에서 코드베이스가 성장하고 성장함에 따라 위에서 살펴본 기술보다 컴파일 시간을 최소화하는 데 더 효과적입니다. 컴파일 유닛은 컴파일 및 링크 타임에 필요한 양의 정보를 곱한 것 ). 또한 빌드 시간 단축 및 헤더 풀림 해제보다 더 많은 이점이 있습니다. 이는 개발자가 무언가를 수행하기 위해 즉시 필요한 것 이상으로 시스템에 대해 많이 알 필요가 없기 때문입니다.

예를 들어, 수백만의 LOC를 포괄하는 AAA 게임을위한 물리 엔진을 개발하기 위해 전문 물리 개발자를 고용 할 수 있고 사용 가능한 유형 및 인터페이스와 같은 절대적인 최소 정보를 알고 있으면서도 매우 빠르게 시작할 수 있습니다. 시스템 개념뿐만 아니라 자연스럽게 그와 컴파일러 모두 물리 엔진을 빌드하는 데 필요한 정보의 양을 줄이며, 일반적으로 스파게티와 유사한 것은 없다는 것을 암시하면서 빌드 시간을 크게 줄입니다. 시스템의 어느 곳에서나 이것이 바로 다른 모든 기술보다 우선 순위를 정할 것을 제안하는 것입니다 : 시스템 설계 방법. 다른 기술을 소진하면 그 위에 있으면 그 위에 착빙됩니다.


1
훌륭한 답변입니다! Althoguh 나는 pimpls 가 무엇인지 알아 내기 위해 약간을 파헤쳐 야했다 :-)
Mawg

0

그것은 의견의 문제입니다. 답변과 답변을 참조하십시오 . 또한 프로젝트 크기에 따라 크게 달라집니다 (프로젝트에 수백만 개의 소스 라인이 있다고 생각하는 경우 수십만 개의 라인이있는 것과 같지 않음).

다른 답변과 달리 하위 시스템 당 하나의 공용 헤더 (대개 인라인 기능을 구현하기 위해 별도의 파일이있는 "개인"헤더를 포함 할 수 있음)를 권장합니다. #include 지시문이 여러 개인 헤더를 고려할 수도 있습니다.

많은 헤더 파일이 권장되는 것은 아닙니다. 특히 클래스 당 하나의 헤더 파일이나 수십 줄의 많은 작은 헤더 파일을 권장하지 않습니다.

(작은 파일이 많으면 모든 작은 번역 단위 에 많은 파일을 포함해야 하며 전체 빌드 시간이 길어질 수 있습니다)

당신이 정말로 원하는 것은 각 서브 시스템과 파일을 담당하는 주요 개발자를 식별하는 것입니다.

마지막으로 소규모 프로젝트 (예 : 십만 줄 미만의 소스 코드) 에는 그다지 중요하지 않습니다. 프로젝트를 진행하는 동안 코드를 쉽게 리팩토링하고 다른 파일로 재구성 할 수 있습니다. 코드 덩어리를 복사하지 않고 새로운 (헤더) 파일에 붙여 넣으면됩니다 (더 어려운 것은 파일을 재구성하는 방법을 현명하게 설계하는 것인데 프로젝트마다 다릅니다).

(개인적으로 선호하는 것은 너무 크고 작은 파일을 피하는 것입니다. 저는 종종 각각 수천 줄의 소스 파일을 가지고 있습니다. 인라인 함수 정의를 포함하여 수백 줄 또는 몇 줄의 헤더 파일을 두려워하지 않습니다. 그들 중 수천)

사전 컴파일 된 헤더를 GCC 와 함께 사용하려면 ( 때로는 컴파일 시간을 줄이는 데 현명한 접근 방법 임) 단일 헤더 파일 (다른 모든 파일 및 시스템 헤더도 포함)이 필요합니다.

통지 C ++에 표준 헤더 파일이 당기는 많은 코드를 . 예를 들어 #include <vector>Linux의 GCC 6에서 1 만 개 이상의 줄을 당기고 있습니다 (18100 줄). 그리고 #include <map> 거의 40KLOC로 확장됩니다. 따라서 표준 헤더를 포함하여 작은 헤더 파일이 많은 경우 빌드 중에 수천 줄을 다시 파싱하여 컴파일 시간이 길어집니다. 그렇기 때문에 많은 수의 작은 C ++ 소스 라인 (수백 줄)을 좋아하지 않지만 C ++ 파일의 수는 줄이지 ​​만 수천 줄 정도는 더 좋습니다.

(따라서 간접적으로도 몇 개의 표준 헤더 파일을 항상 포함하는 수백 개의 작은 C ++ 파일이 있으면 빌드 시간이 길어 개발자를 괴롭 힙니다)

C 코드에서 헤더 파일은 더 작은 것으로 확장되므로 트레이드 오프가 다릅니다.

기존의 자유 소프트웨어 프로젝트 (예 : github )의 사전 실습에 대해서도 영감을 얻으십시오 .

좋은 빌드 자동화 시스템 으로 종속성을 처리 할 수 ​​있습니다 . GNU make문서를 연구하십시오 . GCC에 대한 다양한 -M전 처리기 플래그 를 알고 있어야합니다 (자동 종속성을 생성하는 데 유용함).

다시 말해, 파일 (백 개 미만의 파일과 수십 명의 개발자가있는 프로젝트)은 "헤더 지옥"에 실제로 관심을 가질만큼 크지 않기 때문에 걱정이 정당화 되지 않습니다 . 헤더 파일은 수십 개 (또는 훨씬 더 적음) 만 가질 수 있으며, 번역 단위당 하나의 헤더 파일을 갖도록 선택할 수도 있고, 하나의 단일 헤더 파일을 가질 수도 있습니다. "헤더 지옥"(그리고 파일을 리팩토링하고 재구성하는 것이 상당히 쉬울 것이므로 초기 선택은 실제로 중요 하지 않습니다 ).

(귀하의 문제가 아닌 "header hell"에 노력을 집중하지 말고 좋은 아키텍처를 설계하기 위해 집중하십시오.)


언급 한 기술이 정확할 수 있습니다. 그러나 내가 이해했듯이 OP는 컴파일 시간이 아닌 코드 유지 관리 및 구성을 개선하는 방법에 대한 힌트를 요구했습니다. 그리고 나는이 두 목표 사이에 직접적인 갈등이 있습니다.
머피

그러나 여전히 의견의 문제입니다. 그리고 OP는 분명히 그리 크지 않은 프로젝트를 시작하고 있습니다.
바 실레 Starynkevitch
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.