2012-03-28 67 views
57

只要類聲明只使用另一個類作爲指針,使用類前向聲明​​而不是包含頭文件是否有意義,以便先發制人地避免循環依賴關係的問題?所以,而不是:是否應該使用前向聲明而不是儘可能包含?

//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" 
... 

是否有任何理由,爲什麼不盡可能地做到這一點?

+0

簡單的答案,沒有。 – Nim 2012-03-28 11:20:36

+3

呃 - 這個問題的答案是頂部還是底部? – Mat 2012-03-28 11:21:42

+1

你真正的問題(底部) - AFAIK沒有理由不在這種情況下使用前向聲明... – Nim 2012-03-28 11:22:40

回答

47

前向聲明方法幾乎總是更好。 (我不能想到這樣一種情況,即包含一個可以使用前向聲明的文件更好,但我不會說它總是更好,以防萬一)。

有沒有缺點,以前瞻性的聲明類,但我能想到的一些缺點的用於包括頭不必要的:

  • 較長的編譯時間,因爲所有的翻譯單位包括C.h還將包括A.h,雖然他們可能不需要它。

  • 可能包括你不需要間接

  • 污染與符號翻譯單元不需要

  • 你可能需要重新編譯源文件,其中包括這個頭,如果它改變其它頭(@PeterWood)

+11

另外,增加了重新編譯的機會。 – 2012-03-28 11:40:08

+8

「我無法想象包括可以使用前向聲明的文件更好的情況」 - 當前向聲明產生UB時,請參閱我對主要問題的評論。你是對的謹慎,我認爲:-) – 2012-03-28 11:59:52

+0

@SteveJessop我不知道你能做到這一點。但它確實產生了警告。你爲什麼不加這個作爲答案? – 2012-03-28 12:02:25

28

是的,使用前向聲明總是更好。

一些他們提供的優點是:

  • 減少編譯時間。
  • 沒有命名空間污染。
  • (在某些情況下)可能會減少生成的二進制文件的大小。
  • 重新編譯時間可以顯着減少。
  • 避免潛在的預處理器名稱衝突。
  • 實施PIMPL Idiom因此提供了從接口隱藏實現的手段。

然而,正向聲明一個類可以是特定的類不完全類型和嚴厲,限制什麼樣的操作,你可以在不完整的類型進行。
你不能執行任何需要編譯器知道類的佈局的操作。

一種不完全型可以:

  • 聲明一個構件是一個指針或到不完整的類型的引用。
  • 聲明接受/返回不完整類型的函數或方法。
  • 定義接受/返回不完整類型的指針/引用(但不使用其成員)的函數或方法。

一種不完全類型不能:

  • 使用它作爲一個基類。
  • 用它來聲明一個成員。
  • 定義使用此類型的函數或方法。
+1

「但是,Forward聲明一個類會使該特定類爲」不完整「類型,並且嚴重限制您可以在」未完成「類型上執行的操作。」那麼是的,但如果你*可以*向前宣佈它,這意味着你不需要在標題中的完整類型。如果您確實需要包含該標題的文件中的完整類型,則只需包含所需類型的標題即可。國際海事組織,這是一個優勢 - 它迫使你在你的實現文件中包含任何你需要的東西,而不要依賴它被包含在別的地方。 – 2012-03-29 19:47:48

+0

說某人改變了這個標題,並用一個前向聲明替換了包含。然後,你必須去改變包含該頭的所有文件,使用缺少的類型,但不要自己包含缺失類型的頭(儘管它們應該)。 – 2012-03-29 19:48:52

+1

@LuchianGrigore:* ..但是,如果你可以轉發聲明它...... *,你將不得不嘗試檢查它。所以沒有固定的規則去轉發聲明和不包含標題,知道規則幫助組織你的實現。Forward聲明最常見的用途是打破循環依賴關係,這就是你不能使用Incomplete類型做什麼*通常會咬你。每個源文件和頭文件都應該包含編譯所需的所有頭文件,所以第二個參數不適用,它只是一個組織嚴密的代碼開始。 – 2012-03-30 02:47:26

1

是否有任何理由不盡可能地做到這一點?

我想到的唯一原因就是保存一些打字。

沒有前向聲明,你可以只包含頭文件一次,但我不建議在任何相當大的項目上這樣做,因爲其他人指出的缺點。

+6

命名所有變量'a','b',....,'a1','a2'也可以節省打字。 – 2012-03-28 11:40:29

+0

@Luchian Grigore:對於一些簡單的測試程序可能沒關係 – ks1322 2012-03-28 11:44:12

14

是否有任何理由不盡可能地做到這一點?

方便。

如果您事先知道這個頭文件的任何用戶需要包含A的定義來做任何事情(或者大部分時間)。那麼只需一次性包含它就很方便。

這是一個相當棘手的問題,因爲過於自由的使用這種經驗法則會產生一個近乎不可編譯的代碼。請注意,Boost通過提供特定的「便利」頭文件將不同的問題集中到一起,從而以不同的方式解決問題。

+3

這是唯一的答案,指出它有這樣的生產力成本。 +1 – usr 2012-05-18 13:48:12

+0

從用戶的角度來看。如果您轉發聲明所有內容,這意味着用戶不能只包含該文件並立即開始工作。他們必須弄清楚依賴關係是什麼(可能是因爲編譯器抱怨不完整的類型),並且在開始使用你的類之前也包含這些文件。另一種方法是爲你的庫創建一個「shared.hpp」文件,其中所有頭文件都在該文件中(如上面提到的boost)。他們可以很容易地將其納入考慮範圍,而不必弄清楚爲什麼他們不能「包含和去」。 – Todd 2017-07-11 14:04:36

8

你不想有前向聲明的一種情況是當他們自己很棘手時。

// 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每次放置或更多,這是不好的。

+2

+1破壞了前向聲明總是更好的一致意見:-) IIRC同樣的問題發生在通過類型定義「祕密」模板實例化的類型中。 'namespace std {class string; }'即使允許將類聲明放在名稱空間std中也是錯誤的,因爲(我認爲)你不能合法地將類型定義聲明爲一個類。 – 2012-03-29 11:53:15

0

是否有任何理由不盡可能地做到這一點?

是 - 性能。類對象與其數據成員一起存儲在內存中。當你使用指針時,指向實際對象的內存被存儲在堆的其他地方,通常很遠。這意味着訪問該對象將導致緩存未命中並重新加載。這在性能至關重要的情況下可能會有很大的不同。

在我的電腦的更快()函數運行約2000X比較慢()函數更快:

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(); 
    } 
} 

在其中是關鍵性能或硬件工作時的應用部位是特別容易爲了緩存一致性問題,數據佈局和使用可以產生巨大的差異。

這是關於這個問題和其他性能因素好介紹: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf

+9

您正在回答一個不同的問題(「我應該使用指針嗎?」),而不是被問到的問題(「當我僅使用指針時,是否有任何理由不使用前向聲明?」)。 – 2014-06-24 15:17:56

6

如果一個使用前向聲明,而不是包括在可能的情況?

不,不應將明確的前向聲明視爲一般準則。前向聲明本質上是複製和粘貼的,或者拼寫錯誤的代碼,如果你發現它的錯誤,需要在任何地方使用前向聲明來修復。這可能容易出錯。

爲避免「向前」聲明與其定義之間的不匹配,請將聲明放入頭文件中,並將該頭文件包含在定義聲明和使用聲明的源文件中。

然而,在這種特殊情況下,只有一個不透明的類是前向聲明的,這個前向聲明可能可以使用,但一般來說,「儘可能使用前向聲明而不是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 ++沒有給出編譯器或鏈接錯誤/警告
注:省略extern "C"導致對function()一個鏈接錯誤由於C++名字改編。

運行程序:

> ./a.out 
truncated=abcd, forgotten="♀♥♂☺☻" 
accessing data[1270][1023] 
Segmentation fault 
2

是否有任何理由不盡可能地做到這一點?

絕對:它通過要求類或函數的用戶知道和重複實現細節來打破封裝。如果這些實現細節發生變化,那麼前導聲明的代碼可能會被破壞,而依賴於頭部的代碼將繼續工作。

正向聲明一個函數:

  • 需要知道,它的實現爲一個功能,而不是一個靜態的仿函數對象或(哇!)宏的一個實例,

  • 需要複製默認默認參數的值,

  • 需要知道它的實際名稱和名稱空間,因爲它可能只是一個using聲明,將其拉入另一個名稱空間,可能在別名,和

  • 可能會失去內聯優化。

如果使用代碼依賴於頭,那麼所有這些實現細節都可以被函數提供者改變而不會破壞你的代碼。

轉發聲明類:

  • 需要知道它是否是一個派生類和它的衍生基類(ES),

  • 需要知道,這是一個類,而不是隻是一個typedef或者類模板的特定實例化(或者知道它是類模板並獲得所有模板參數和默認值正確),

  • 要求知道t的真實名稱和命名空間因爲它可能是一個using聲明,將其拉到另一個名稱空間,也許在一個別名下,並且需要知道正確的屬性(可能它有特殊的對齊要求)。

同樣,forward聲明中斷了這些實現細節的封裝,使您的代碼更加脆弱。

如果您需要削減頭文件依賴關係以加快編譯時間,那麼請讓類/函數/庫的提供者提供特殊的前向聲明頭文件。標準庫與<iosfwd>這樣做。該模型保留了實現細節的封裝,並使庫維護者能夠在不破壞代碼的情況下更改這些實現細節,同時減少編譯器的負載。

另一種選擇是使用pimpl慣用法,它可以更好地隱藏實現細節,並以小的運行時間開銷爲代價加快編譯速度。

相關問題