C ++ 큰 템플릿 구현을 처리하는 기본 방법


10

일반적으로 C ++ 클래스를 선언 할 때는 헤더 파일에 선언 만 넣고 구현을 소스 파일에 두는 것이 가장 좋습니다. 그러나이 디자인 모델은 템플릿 클래스에서 작동하지 않는 것 같습니다.

온라인을 살펴보면 템플릿 클래스를 관리하는 가장 좋은 방법에 대한 두 가지 의견이있는 것 같습니다.

1. 헤더의 전체 선언 및 구현.

이것은 매우 간단하지만 내 생각에 템플릿이 커지면 코드 파일을 유지 관리하고 편집하기가 어려운 것으로 이어집니다.

2. 마지막에 포함 된 템플릿 포함 파일 (.tpp)에 구현을 작성하십시오.

이것은 나에게 더 나은 해결책처럼 보이지만 널리 적용되지는 않습니다. 이 접근법이 열등한 이유가 있습니까?

나는 몇 번이나 코드 스타일이 개인 취향이나 레거시 스타일에 의해 결정된다는 것을 알고 있습니다. 새 프로젝트를 시작하고 (오래된 C 프로젝트를 C ++로 이식) 상대적으로 OO 디자인을 처음 사용하며 처음부터 모범 사례를 따르고 싶습니다.


1
codeproject.com 에서이 9 년 된 기사 를 참조하십시오 . 방법 3은 당신이 설명한 것입니다. 당신이 믿는 것처럼 특별하지 않은 것 같습니다.
Doc Brown

.. 또는 여기 동일한 접근 방식, 2014 년 기사 : codeofhonour.blogspot.com/2014/11/…
Doc Brown

2
밀접한 관련 : stackoverflow.com/q/1208028/179910 . Gnu는 일반적으로 ".tpp"대신 ".tcc"확장자를 사용하지만 그렇지 않으면 거의 동일합니다.
Jerry Coffin

나는 항상 "ipp"를 확장명으로 사용했지만, 내가 작성한 코드에서 똑같은 일을했다.
Sebastian Redl

답변:


6

템플릿 C ++ 클래스를 작성할 때 일반적으로 세 가지 옵션이 있습니다.

(1) 선언과 정의를 헤더에 넣습니다.

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f()
    {
        ...
    }
};

또는

// foo.h
#pragma once

template <typename T>
struct Foo
{
    void f();
};

template <typename T>
inline void Foo::f()
{
    ...
}

찬성:

  • 매우 편리한 사용법 (헤더 포함).

범죄자:

  • 인터페이스와 메소드 구현이 혼합되어 있습니다. 이것은 "가독성"입니다. 일부는 이것이 일반적인 .h / .cpp 접근 방식과 다르기 때문에 유지 관리 할 수없는 것으로 판단합니다. 그러나 다른 언어 (예 : C # 및 Java)에서는 이것이 문제가되지 않습니다.
  • 높은 재 구축 영향 : Foo멤버로 새 클래스를 선언하는 경우을 포함해야합니다 foo.h. 즉, 구현 구현을 변경하면 Foo::f헤더 및 소스 파일을 통해 전파됩니다.

재구성의 영향을 자세히 살펴 보겠습니다. 템플릿이 아닌 C ++ 클래스의 경우 .h에 선언을, .cpp에 메서드 정의를 넣습니다. 이 방법으로 메소드의 구현이 변경되면 하나의 .cpp 만 다시 컴파일하면됩니다. .h에 모든 코드가 포함되어 있으면 템플릿 클래스와 다릅니다. 다음 예를 살펴보십시오.

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

여기서 유일한 사용법 Foo::f은 inside bar.cpp입니다. 당신의 구현을 변경하는 경우에는 Foo::f, 모두 bar.cppqux.cpp필요를 다시 컴파일합니다. 의 Foo::f일부를 Qux직접 사용하는 부분이 없더라도 두 파일 모두에서 삶을 구현 Foo::f합니다. 대규모 프로젝트의 경우 곧 문제가 될 수 있습니다.

(2) 선언은 .h에, 정의는 .tpp에 넣고 .h에 포함하십시오.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};
#include "foo.tpp"    

// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
    ...
}

찬성:

  • 매우 편리한 사용법 (헤더 포함).
  • 인터페이스와 메소드 정의는 분리되어 있습니다.

범죄자:

  • 높은 재건 영향 ( (1) 과 동일 )

이 솔루션은 선언과 메소드 정의를 .h / .cpp와 같이 두 개의 별도 파일로 분리합니다. 그러나이 방법에는 헤더에 메소드 정의가 직접 포함되므로 (1) 과 동일한 재구성 문제가 있습니다.

(3) .h에 선언을하고 .tpp에 정의를 넣지 만 .h에 .tpp를 포함시키지 마십시오.

// foo.h
#pragma once
template <typename T>
struct Foo
{
    void f();
};

// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
    ...
}

찬성:

  • .h / .cpp 분리와 마찬가지로 재건 영향을 줄입니다.
  • 인터페이스와 메소드 정의는 분리되어 있습니다.

범죄자:

  • 불편한 사용법 : Foo멤버를 클래스에 추가 할 때 헤더 Bar에 포함해야합니다 foo.h. 당신이 호출하면 Foo::fcpp를, 당신은 또한 포함 할 필요가 foo.tpp있다.

실제로 사용하는 .cpp 파일 만 Foo::f다시 컴파일 하면되므로이 방법을 사용하면 다시 빌드 영향이 줄어 듭니다 . 그러나 가격이 책정됩니다. 모든 파일에는 포함해야합니다 foo.tpp. 위의 예를 들어 새로운 접근법을 사용하십시오.

// bar.h
#pragma once
#include "foo.h"
struct Bar
{
    void b();
    Foo<int> foo;
};

// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
    foo.f();
}

// qux.h
#pragma once
#include "bar.h"
struct Qux
{
    void q();
    Bar bar;
}

// qux.cpp
#include "qux.h"
void Qux::q()
{
    bar.b();
}

당신이 볼 수 있듯이, 유일한 차이점은 추가가의 포함이다 foo.tpp에서 bar.cpp. 이것은 불편하고 클래스 호출에 메소드를 호출하는지 여부에 따라 두 번째 포함을 추가하는 것은 매우 추한 것 같습니다. 그러나 다시 빌드 영향을 줄 bar.cpp입니다 Foo::f. 의 구현을 변경 한 경우 에만 다시 컴파일하면됩니다 . 파일을 qux.cpp다시 컴파일 할 필요가 없습니다.

요약:

라이브러리를 구현하면 일반적으로 재 빌드 영향에 신경 쓸 필요가 없습니다. 라이브러리 사용자는 릴리스를 가져 와서 사용하며 라이브러리 구현은 사용자의 일상 업무에서 변경되지 않습니다. 이 경우, 라이브러리는 접근 방식 (1) 또는 (2) 를 사용할 수 있으며 선택한 것을 선택하는 것은 맛의 문제 일뿐입니다.

그러나 응용 프로그램에서 작업 중이거나 회사의 내부 라이브러리에서 작업중인 경우 코드가 자주 변경됩니다. 따라서 재 구축 영향에주의해야합니다. 개발자가 추가 포함을 수락하게하려면 접근 방식 (3)을 선택 하는 것이 좋습니다.


2

.tpp내가 본 적이없는 아이디어 와 유사하게 , 우리는 대부분의 인라인 기능을 -inl.hpp일반적인 .hpp파일 의 끝에 포함 된 파일에 넣습니다 .

다른 사람들이 지적했듯이, 이것은 다른 파일에서 인라인 구현의 혼란을 (템플릿과 같은) 이동시켜 인터페이스를 읽을 수있게 유지합니다. 인터페이스 인라인을 허용하지만 작은 단일 라인 함수로 제한하려고합니다.


1

두 번째 변형의 하나의 프로 코인은 헤더가 더 깔끔하게 보입니다.

인라인 IDE 오류 검사 및 디버거 바인딩이 망가 졌을 수 있습니다.


2nd는 또한 많은 템플릿 매개 변수 선언 중복성이 필요하며 특히 sfinae를 사용할 때 매우 장황해질 수 있습니다. 그리고 OP와는 달리, 특히 중복 보일러 플레이트로 인해 더 많은 코드를 읽는 것이 더 어렵다는 것을 알았습니다.
소펠

0

구현을 별도의 파일에 넣고 문서와 선언 만 헤더 파일에 넣는 접근 방식을 선호합니다.

아마도이 접근법이 실제로 많이 사용되지 않는 이유는 올바른 장소를 보지 않았기 때문입니다. ;-)

또는 소프트웨어 개발에 약간의 추가 노력이 필요하기 때문일 수 있습니다. 그러나 클래스 라이브러리의 경우 그 노력은 가치가 있지만 IMHO는 사용하기 쉽고 읽기 쉬운 라이브러리에서 비용을 지불합니다.

이 라이브러리를 예로 들어 봅시다 : https://github.com/SophistSolutions/Stroika/

전체 라이브러리는이 방법으로 작성되었으며 코드를 살펴보면 그것이 얼마나 잘 작동하는지 볼 수 있습니다.

헤더 파일은 구현 파일만큼 길지만 선언 및 문서만으로 채워져 있습니다.

Stroika의 가독성을 선호하는 std c ++ 구현 (gcc 또는 libc ++ 또는 msvc)과 비교해보십시오. 이들은 모두 인라인 헤더 구현 방식을 사용하며, 읽기 쉬운 구현 방식이 아니라 매우 잘 작성되었지만 IMHO입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.