C++ 的 forward declaration

這邊稍微來講一下 C++ 的 forward declaration(維基百科C++ Reference)。

Forward declaration 的基本概念,以類別來說,就是先告訴編譯器有這樣一個類別存在(宣告),但是這個時候還先不告訴你他的實際內容(定義);在這個狀況下,這個類別會是「incomplete type」,可以有限度地使用。

如果兩個類別要使用到彼此的時候,通常就會需要用到這種技巧,下面就是 C++ Reference 給的一個例子:

class Vector; // forward declaration
 
class Matrix
{
  // ...
  friend Vector operator*(const Matrix&, const Vector&);
};
 
class Vector
{
  // ...
  friend Vector operator*(const Matrix&, const Vector&);
};

除了這種相對基本的情境之外,實際上在使用、或者開發自己的函式庫的時候,forward declaration 也會相當地有用!不但可以減少建置專案的時間,更可以在某種程度上避免不同函式庫之間重複定義的問題。

而這篇文章,主要會是針對這部分來講。


一般使用函式庫的方法

假設現在要在自己使用的函式庫裡面、使用一個別人寫好的函式庫的時候,通常會需要引用他提供的 header 檔案,然後才能使用他提供的物件、函式。這邊假設有一個函式庫提供的 header 長得像下面這樣:

#pragma once
// LibA.h
 
//#include "A01.h"
// ...
//#include "A09.h"
 
enum ErrorCode
{
  OK,
  Error1,
  Error2
};
 
class LibAObj
{
public:
  ErrorCode func()
  {
    return OK;
  }
  //...
};

他提供了一個類別 LibAObj 作為主要的操作對象,裡面有一個叫做 func() 的函式;另外,他也定義了 ErrorCode 這個列舉型別,來描述函示執行的結果。

而由於這個檔案是由別的開發者撰寫、提供的,考量到後續維護,可以先將這個 Lib.h 視為不可修改的檔案。

至於要使用它來開發的函式庫,一般可能會寫成下面的樣子:

#pragma once
// MyLib.h
#include "libA.h"
 
class MyLib
{
public:
  bool func()
  {
    if (mObj.func() == OK)
      return true;
    
    return false;
  }
 
protected:
  LibAObj mObj;
};

使用時,則是會是:

#include <iostream>
 
#include "MyLib.h"
 
int main()
{
  MyLib a;
  if (a.func())
    std::cout << "OK" << std::endl;
  else
    std::cout << "Error" << std::endl;
}

在一般狀況下,這樣大概都是沒問題的,使用者也不需要去碰到底層 libA 的東西。

但是,如果在應用程式端,又需要用到別的函式庫的時候…恩,就可能就會碰到命名衝突的問題了。

比如說,如果有的 libB.h 裡面,寫了:

// LibB.h
enum ErrorCode
{
  BOK
};

那如果同時 include MyLib.hlibB.h,就會出現 ErrorCode 被重複定義、而無法正確編譯的狀況了!而如果是有定義同名的巨集的時候,更是有可能會其他的問題產生。

此外,上面的寫法中,由於 MyLib.h 中會 include libA.h 這個 header、而 libA.h 裡面可能又 include 了更多的 header,這會導致在編譯應用程式的檔案時,編譯器只要遇到有 include MyLib.h 的檔案的時候,就需要去處理 libA.h 有引用到的 header,所以有可能會多花不少時間。


使用 Forward declaration

而要避免上面的問題呢,一個方法就是使用 forward declaration 的形式、來改寫 MyLib.h

#pragma once
// MyLib.h
#include <memory>
 
class LibAObj; // Forward declaration
 
class MyLib
{
public:
  MyLib();
  ~MyLib();
 
  bool func();
 
protected:
  std::unique_ptr<LibAObj> mObj;
};

這邊修改的點包括了:

  • 不在 header 檔中 include libA.h
  • 針對 libA.h 需要使用的型別(libAObj)透過 forward declaration 宣告
  • 使用 pointer 的形式取代 libAObj 的物件(這邊是用 smart point
  • 將會使用到 libAObj 的內容的部分都從 .h 移到 .cpp(包括 new 和 delete)

然後,對應的 MyLib.cpp 會變成下面的樣子:

// MyLib.cpp
#include "libA.h"
 
#include "MyLib.h"
 
MyLib::MyLib()
{
  mObj = std::make_unique<LibAObj>();
}
 
MyLib::~MyLib(){}
 
bool MyLib::func()
{
  if (mObj->func() == OK)
    return true;
 
  return false;
}

透過這樣的修改,和 libA.h 有關的東西,都會被藏在 MyLib.cpp 裡面,在編譯其他 include MyLib.h 的檔案的時候,都不會真的碰到和 libA.h 的內容,也就代表和 libA.h 有關的 header 只需要在編譯 MyLib.cpp 才需要處理。

所以如果在專案中有很多檔案有用到 MyLib.h 的時候,這樣的寫法是可以減少編譯時需要的時間的。

同時,由於 libA.h 內定義的東西,也都被藏在 MyLib.cpp 裡面,所以就算應用程式那邊還有 include LibB.h、也有同名的 ErrorCode 列舉型別,也不會造成命名的衝突。

另外,這邊為什麼 destructor 明明是空的,還是要寫在 MyLib.cpp 呢?
這是因為 std::unique_ptr<> 這類的 smart pointer 雖然可以自己釋放資源,但是在釋放的時候會需要完整的型別;而如果使用預設的解構子、或是把解構子寫在 header 的話,就會變成要 smart pointer 去刪除一個 incomplete type 的物件,所以會無法正確編譯。
而寫在 MyLib.cpp 中,他才能知道 LibAObj 的型別內容、並正確地刪除。


當然,這種在 header 中用 forward declaration 來取代 include 的方法也不盡然都是好處,像是《Google C++ Style Guide》就列了一些理由建議不要用(連結);他也有舉例,在特殊的狀況下,甚至有可能會因為改用 forward declaration 而改變程式的運作。

下面就是他舉的例子:

// b.h:
struct B {};
struct D : B {};
 
// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // Calls f(B*)

如果把 #include “b.h” 改成 BD 的 forward declaration 的話,test() 就會不知道 BD 的繼承關係,而不會去呼叫 f(B*)、而改去呼叫 f(void*)

老實說,個人在看到她的例子之前,到還真的沒想過發有這種問題就是了…

不過除了 Google 外,大部分的人好像都還是建議使用的?所以到底要不要用,就見仁見智了,但是如果是要把既有的程式改成用 forward declaration 的話,可能就還是得注意一些東西了。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。