가능한 한 include 대신 포워드 선언을 사용해야합니까?
클래스 선언이 다른 클래스를 포인터로만 사용할 때마다 순환 종속성 문제를 선제 적으로 방지하기 위해 헤더 파일을 포함하는 대신 클래스 전달 선언을 사용하는 것이 합리적입니까? 그래서 대신 :
//file C.h
#include "A.h"
#include "B.h"
class C{
A* a;
B b;
...
};
대신 다음을 수행하십시오.
//file C.h
#include "B.h"
class A;
class C{
A* a;
B b;
...
};
//file C.cpp
#include "C.h"
#include "A.h"
...
가능할 때마다 이것을하지 않는 이유가 있습니까?
앞으로 선언하는 방법은 거의 항상 더 좋습니다. (정방향 선언을 사용할 수있는 파일을 포함하는 것이 더 좋은 상황은 생각할 수 없지만 혹시라도 항상 더 좋다고 말하지는 않겠습니다).
포워드 선언 클래스에는 단점이 없지만 불필요하게 헤더를 포함 할 경우 몇 가지 단점을 생각할 수 있습니다.
더 이상 컴파일 시간, 모든 번역 단위를 포함하여 이후
C.h
도 포함됩니다A.h
그들이 필요하지 않을 수도 있지만.간접적으로 필요하지 않은 다른 헤더를 포함 할 수 있습니다.
필요하지 않은 기호로 번역 단위를 오염시킵니다.
헤더가 변경되면 해당 헤더를 포함하는 소스 파일을 다시 컴파일해야 할 수 있습니다 (@PeterWood)
예, 앞으로 선언을 사용하는 것이 항상 더 좋습니다.
그들이 제공하는 몇 가지 장점은 다음과 같습니다.
- 컴파일 시간 단축.
- 네임 스페이스 오염이 없습니다.
- (경우에 따라) 생성 된 바이너리의 크기를 줄일 수 있습니다.
- 재 컴파일 시간을 크게 줄일 수 있습니다.
- 전 처리기 이름의 잠재적 충돌 방지.
- 구현 PIMPL 관용구 따라서 인터페이스 구현을 은폐하는 수단을 제공한다.
그러나 클래스를 Forward로 선언하면 특정 클래스가 Incomplete 유형이 되고 이는 Incomplete 유형에서 수행 할 수있는 작업을 심각하게 제한합니다.
클래스의 레이아웃을 알기 위해 컴파일러가 필요한 작업을 수행 할 수 없습니다.
불완전한 유형으로 다음을 수행 할 수 있습니다.
- 멤버를 불완전한 유형에 대한 포인터 또는 참조로 선언하십시오.
- 불완전한 유형을 허용 / 반환하는 함수 또는 메소드를 선언하십시오.
- 불완전한 유형에 대한 포인터 / 참조를 수락 / 반환하는 함수 또는 메서드를 정의합니다 (하지만 멤버를 사용하지 않음).
불완전한 유형으로 다음을 수행 할 수 없습니다.
- 기본 클래스로 사용하십시오.
- 이를 사용하여 구성원을 선언하십시오.
- 이 유형을 사용하여 함수 또는 방법을 정의하십시오.
가능할 때마다 이것을하지 않는 이유가 있습니까?
편의.
이 헤더 파일의 사용자 A
가 무엇이든 (또는 대부분의 경우) 수행 할 정의를 반드시 포함해야한다는 것을 미리 알고 있다면 . 그런 다음 한 번만 포함하는 것이 편리합니다.
이 엄지 손가락 규칙을 너무 자유롭게 사용하면 거의 컴파일 할 수없는 코드가 생성되기 때문에 이것은 다소 민감한 주제입니다. Boost는 몇 가지 밀접한 기능을 함께 묶는 특정 "편의"헤더를 제공하여 문제에 다르게 접근합니다.
포워드 선언을 원하지 않는 한 가지 경우는 그 자체가 까다로울 때입니다. 이는 다음 예제와 같이 일부 클래스가 템플릿 화 된 경우 발생할 수 있습니다.
// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer;
// Alternative: more clear to the reader; more stable code
#include "Gibberer.h"
// Declare a function that does something with a pointer
int do_stuff(Gibberer<int, float>*);
순방향 선언은 코드 복제와 동일합니다. 코드가 많이 변경되는 경향이있는 경우 매번 2 곳 이상에서 변경해야하며 이는 좋지 않습니다.
가능한 한 include 대신 포워드 선언을 사용해야합니까?
아니요, 명시적인 전방 선언은 일반적인 지침으로 간주되어서는 안됩니다. 포워드 선언은 본질적으로 복사하여 붙여 넣거나 철자가 틀린 코드로, 버그가있는 경우 포워드 선언이 사용되는 모든 곳에서 수정해야합니다. 이는 오류가 발생하기 쉽습니다.
"앞으로"선언과 해당 정의 사이의 불일치를 방지하려면 선언을 헤더 파일에 넣고 해당 헤더 파일을 정의 및 선언 사용 소스 파일에 포함합니다.
그러나 불투명 한 클래스 만 포워드 선언되는이 특별한 경우에는이 포워드 선언을 사용해도 괜찮지 만, 일반적으로이 스레드의 제목처럼 "가능할 때마다 포함 대신 포워드 선언을 사용"하는 것이 가능합니다. 매우 위험합니다.
다음은 포워드 선언과 관련된 "보이지 않는 위험"의 몇 가지 예입니다 (보이지 않는 위험 = 컴파일러 또는 링커에서 감지하지 않는 선언 불일치).
데이터를 나타내는 기호의 명시 적 전방 선언은 안전하지 않을 수 있습니다. 이러한 전방 선언에는 데이터 유형의 풋 프린트 (크기)에 대한 정확한 지식이 필요할 수 있기 때문입니다.
함수를 나타내는 기호의 명시 적 전방 선언은 매개 변수 유형 및 매개 변수 수와 같이 안전하지 않을 수도 있습니다.
아래의 예는이를 설명합니다. 예를 들어, 데이터와 함수의 두 가지 위험한 전방 선언이 있습니다.
파일 ac :
#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
std::cout << "truncated=" << std::hex << truncated
<< ", forgotten=\"" << forgotten << "\"\n";
}
파일 bc :
#include <iostream>
extern char data[1280][1024]; // 1st dimension one decade too large
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param
int main() {
function(0x1234abcd); // In worst case: - No crash!
std::cout << "accessing data[1270][1023]\n";
return (int) data[1270][1023]; // In best case: - Boom !!!!
}
g ++ 4.7.1로 프로그램 컴파일 :
> g++ -Wall -pedantic -ansi a.c b.c
참고 : g ++는 컴파일러 또는 링커 오류 / 경고를 제공하지 않으므로 보이지 않는 위험입니다.
참고 : 생략 하면 C ++ 이름 변경으로 인해 extern "C"
연결 오류가 function()
발생합니다.
프로그램 실행 :
> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault
재미있는 사실 은 C ++ 스타일 가이드 에서 Google은 #include
순환 종속성을 피하기 위해 모든 곳에서 사용 하도록 권장 합니다.
가능할 때마다 이것을하지 않는 이유가 있습니까?
절대적으로 : 클래스 또는 함수의 사용자가 구현 세부 정보를 알고 복제하도록 요구하여 캡슐화를 중단합니다. 이러한 구현 세부 정보가 변경되면 앞으로 선언하는 코드가 손상 될 수 있지만 헤더에 의존하는 코드는 계속 작동합니다.
Forward declaring a function:
requires knowing that it's implemented as a function and not an instance of a static functor object or (gasp!) a macro,
requires duplicating the default values for default parameters,
requires knowing its actual name and namespace, since it may just be a
using
declaration that pulls it into another namespace, perhaps under an alias, andmay lose out on inline optimization.
If the consuming code relies on the header, then all those implementation details can be changed by the function provider without breaking your code.
Forward declaring a class:
requires knowing whether it's a derived class and the base class(es) it's derived from,
requires knowing that it's a class and not just a typedef or a particular instantiation of a class template (or knowing that it is a class template and getting all the template parameters and default values correct),
requires knowing the true name and namespace of the class, since it may be a
using
declaration that pulls it into another namespace, perhaps under an alias, andrequires knowing the correct attributes (perhaps it has special alignment requirements).
Again, forward declaring breaks the encapsulation of these implementation details, making your code more fragile.
If you need to cut header dependencies to speed up compilation time, then get the provider of the class/function/library to provide a special forward declarations header. The standard library does this with <iosfwd>
. This model preserves the encapsulation of implementation details and gives the library maintainer the ability to change those implementation details without breaking your code, all while reducing the load on the compiler.
Another option is to use a pimpl idiom, which hides implementation details even better and speeds up compiles at the cost of a small run-time overhead.
Is there any reason why not to do this wherever possible?
The only reason I think of is to save some typing.
Without forward declarations you can include header file just once, but I don't advice to do so on any rather big projects due to disadvantages pointed by other people.
Is there any reason why not to do this wherever possible?
Yes - Performance. Class objects are stored with their data members together in memory. When you use pointers, the memory to the actual object pointed to is stored elsewhere on the heap, usually far away. This means accessing that object will cause a cache miss and reload. This can make a big difference in situations where performance is crucial.
On my PC the Faster() function runs approx 2000x faster than the Slower() function:
class SomeClass
{
public:
void DoSomething()
{
val++;
}
private:
int val;
};
class UsesPointers
{
public:
UsesPointers() {a = new SomeClass;}
~UsesPointers() {delete a; a = 0;}
SomeClass * a;
};
class NonPointers
{
public:
SomeClass a;
};
#define ARRAY_SIZE 100000
void Slower()
{
UsesPointers list[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++)
{
list[i].a->DoSomething();
}
}
void Faster()
{
NonPointers list[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++)
{
list[i].a.DoSomething();
}
}
In parts of applications which are performance-critical or when working on hardware which is especially prone to cache coherence problems, data layout and usage can make a huge difference.
This is a good presentation on the subject and other performance factors: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf
'Nice programing' 카테고리의 다른 글
Maven을 사용한 환경 변수 (0) | 2020.10.26 |
---|---|
Android에서 SVG 아이콘을 사용하는 모범 사례는 무엇입니까? (0) | 2020.10.26 |
Optional.ifPresent ()의 적절한 사용 (0) | 2020.10.26 |
dplyr을 사용하여 여러 열의 합계 (0) | 2020.10.26 |
주석을 추가하면 파서가 중단되는 이유는 무엇입니까? (0) | 2020.10.26 |