【已解決】循環引用的疑問

最近在移植程式的過程中,碰到一個循環引用的問題,困擾了我不少時間,所以有寫下來的必要,以免下次再犯。直接來看例子吧!

這是最上層的介面 IElement,其中的 Method 可以處理實作 IVisitor 介面的物件

#pragma once

#include "IVisitor.h" // <== Cause cross reference

struct IElement {
 virtual VOID Accept(IVisitor* pVisitor) = 0;
 virtual VOID Traverse(IVisitor* pVisitor) = 0;
};

typedef IElement* PIElement;

接下來是 IComputer 介面,繼承 IElement 介面

#pragma once

#include "IElement.h"

struct IComputer : public IElement {
 virtual BOOL getMainboardEnabled() = 0;
};

typedef IComputer* PIComputer;

最後是 Computer 類別,實作 IComputer 介面。

#pragma once

#include "IComputer.h"
#include "ISettings.h"
#include "IVisitor.h"

class Computer : public IComputer {
 class Settings : public ISettings {
 public:
  BOOL Contains(LPCTSTR name) { return FALSE; }
  VOID SetValue(LPCTSTR name, LPCTSTR value) { }
  CString GetValue(LPCTSTR name, LPCTSTR value) { return CString(value); }
  VOID Remove (LPCTSTR name) { }
 };

public:
 Computer(void);
 Computer(PISettings pSettings);
 ~Computer(void);

public: //Implement IComputer
 VOID Accept(PIVisitor pVisitor) { }
 VOID Traverse(PIVisitor pVisitor) { }
 BOOL getMainboardEnabled() { return TRUE; }

private:
 PISettings m_pSettings;
};

另外還有一個 IVisitor 介面,它的 Method 可以用來處理實作 IComputer 介面的物件。

#pragma once

#include "IComputer.h"

struct IVisitor {
 virtual VOID VisitComputer(PIComputer pComputer) = 0;
};

最後編譯的下場就是

1>ivisitor.h(6) : error C2061: syntax error : identifier 'PIComputer'

怎麼會這樣呢?(雖然原始 C# 的設計有點複雜,不過執行是沒問題的,沒想到移植到 C++ 卻發生這種狀況)


讓我們從 Computer 開始看一下引用的路徑吧

IComputer.h (Computer class) ==> IElement.h (IComputer struct) ==> IVisitor.h (IElement struct) ==> IComputer.h (IVisitor struct)

如果沒有 #pragma once 的話,將會造成循環引用,直到 Stack overflow 為止。而在 struct IVisitor 要引用 IComputer.h 時,發現前面已經引用過了,所以不會載入,所以 PIComputer 就變成未定義了。


解決辦法就是打破這個循環,我們修改一下 IElement 介面的定義

#pragma once

//#include "IVisitor.h" // <== Cause cross reference

struct IVisitor; // <== Fix cross reference problem

struct IElement {
 virtual VOID Accept(IVisitor* pVisitor) = 0;
 virtual VOID Traverse(IVisitor* pVisitor) = 0;
};

typedef IElement* PIElement;

把原本的 include 改成用 struct IVisitor; 取代,那就萬事 OK 了

2013/4/17
ISensor 跟 IParameter 也發生相同的情況,解法同上
IHardware 跟 ISensor 也是一樣,看來問題很多啊



另外還有一個小插曲:

在 Computer class 有內嵌一個 Settings class 實作 ISettings 介面,其中 ISettings 前面的 public 不能省略,否則下列的敘述會產生錯誤

m_pSettings = new Settings();


1>computer.cpp(8) : error C2243: 'type cast' : conversion from 'Computer::Settings *' to '::PISettings' exists, but is inaccessible


推測應該是預設的實作繼承不是 public,所以才會造成轉型失敗



這裡有一篇 C++中基础类互相引用带来的问题 也不錯,講的是盡量不要在 Header 中再 include 其它 Header,可以參考看看!



不過我還是認為單一 .cpp 應該搭配單一 .h,而不是在 .h 中宣告 external class,然後又在 .cpp 中加入該 class 的 header file。所以研究了一個終極解法說明如下

#ifndef _CLASSA_H_
#define _CLASSA_H_

#ifndef _CLASSB_H_
#include "ClassB.h"
#else
class ClassB;
typedef ClassB *PClassB;
#endif

class ClassA;
typedef ClassA *PClassA;

class ClassA
{
public:
 ClassA(void);
 ~ClassA(void);

 void Hello(PClassB pClassB);
 void Hello() { _tprintf(_T("Hello, I am ClassA.\n")); }
};

#endif // _CLASSA_H_

以上是 ClassA 的宣告,而在它的 Hello 方法中會用到 ClassB

#ifndef _CLASSB_H_
#define _CLASSB_H_

#ifndef _CLASSA_H_
#include "ClassA.h"
#else
class ClassA;
typedef ClassA *PClassA;
#endif

class ClassB;
typedef ClassB *PClassB;

class ClassB
{
public:
 ClassB(void);
 ~ClassB(void);

 void Hello(PClassA pClassA);
 void Hello() { _tprintf(_T("Hello, I am ClassB.\n")); }
};

#endif // _CLASSB_H_

同樣 ClassB 的 Hello 方法也會用到 ClassA

用以上的方式宣告就可以自動帶入相關的 header file,而不須要由 .cpp file 去加入相關的 header file。讓使用 ClassA or ClassB 的程式,只要加入對應的 header file 就可以了。

不過要注意的是,參考 external class 的方法不能使用 inline 的寫法

void Hello(PClassB pClassB) { pClassB->Hello(); }

否則還是會產生錯誤訊息

error C2027: use of undefined type 'ClassB'



忘了把 namespace 考慮進來,結果一使用又 Compiler error 了。所以我們讓它再複雜一點吧,不同 namespace class 互相引用:

#ifndef _CLASSA_H_
#define _CLASSA_H_

#ifndef _CLASSB_H_
#include "ClassB.h"
#else
namespace NameSpaceB {
 class ClassB;
 typedef ClassB *PClassB;
}
#endif

using namespace NameSpaceB;

namespace NameSpaceA {
 class ClassA;
 typedef ClassA *PClassA;

 class ClassA
 {
 public:
  ClassA(void);
  ~ClassA(void);

  void Hello(PClassB pClassB);
  void Hello() { _tprintf(_T("Hello, I am ClassA.\n")); }
 };
}

#endif // _CLASSA_H_

以上藍色是 ClassA 新增加的部份,雖然實驗了兩天才成功,不過最後看看增加的部份也還好

#ifndef _CLASSB_H_
#define _CLASSB_H_

#ifndef _CLASSA_H_
#include "ClassA.h"
#else
namespace NameSpaceA {
 class ClassA;
 typedef ClassA *PClassA;
}
#endif

using namespace NameSpaceA;

namespace NameSpaceB {
 class ClassB;
 typedef ClassB *PClassB;

 class ClassB
 {
 public:
  ClassB(void);
  ~ClassB(void);

  void Hello(PClassA pClassA);
  void Hello() { _tprintf(_T("Hello, I am ClassB.\n")); }
 };
}

#endif // _CLASSB_H_

ClassB 的部份也是一樣。以後終於可以安心引用,不必擔心循環引用的問題了!

參考:C++ namespaces: cross-usage (Search keyword : namespace cross reference)

留言

這個網誌中的熱門文章

Linux 批次檔的寫法

【分享】如何顯示 Debug Message

[分享] Visual Studio 遠端偵錯