Qt開發思想探幽]QObject、模板繼承和多繼承

2023-08-28 15:00:23

@

[Qt開發探幽]QObject、模板繼承和多繼承

當我們在用Qt開發一個軟體框架的時候,在一個正式一點的庫或者框架中,我們不可避免地想要使用繼承,但是可能當我們開發完一個模組後,會發現一些問題,比如說在編譯的時候發現父類別會編譯不通過。

先說結論:
1.Qt的QObject不支援模板繼承。
2.如果需要使用QObject進行多繼承的話,子物件參照的父類別鏈至多隻能含有一個QObject
3.如果使用模板類和QObject做多繼承,依然會編譯不通過

1. QObject為什麼不允許模板繼承:

  1. 元物件系統的複雜性:元物件系統(Meta-Object System)是 Qt 的一個重要特性,它允許在執行時獲取物件的元資訊,如類名、屬性、訊號、槽等。這個系統需要在編譯階段生成額外的元資訊,並且在執行時維護一個元物件表。這個系統在處理模板類時會變得複雜,因為模板類的範例化和使用涉及到編譯期和執行時的多個步驟,而元物件系統的設計主要針對靜態型別的類層次結構。

  2. 編譯期型別資訊:元物件系統需要在編譯期生成一些額外的型別資訊,用於支援訊號槽和反射等功能。模板類的範例化和型別資訊在編譯期可能變得模糊不清,這會導致元物件系統難以正確處理模板類的元資訊。

  3. 型別擦除:C++ 模板的一個特點是型別擦除,即編譯器在模板範例化時會生成不同的程式碼,但生成的程式碼是針對特定的型別的,而不是模板本身。這種型別擦除使得在執行時獲取模板型別資訊變得複雜,而元物件系統需要在執行時獲取型別資訊。

  4. 訊號槽連線的動態性:訊號槽機制允許在執行時動態地連線和斷開訊號槽。這就要求在執行時能夠準確地識別訊號和槽的引數型別,以便進行引數匹配。模板類的引數型別在編譯期可能不確定,這給訊號槽的動態連線帶來挑戰。

所以在繼承了QObject的模板類中,編譯會導致QMetaObject找不到元物件而發生編譯期報錯,所以請不要讓任何模板類繼承QObject。

你可能會問了,那類似QList和QSharedPointer類為什麼可以是模板類,而且也是Qt的東西呢?這恰好是Qt這個庫取巧的地方:所有的模板類都是容器,也就是說他們這個模板類中允許裝入任何它們想要的東西。或者換句話說,Qt中所有的模板類,都不是QObject的子類,不然的話可以直接看標頭檔案:
也就是說,Qt內部其實自己也是遵循這個規則的:請不要讓任何模板類繼承QObject。

2.如果需要使用QObject進行多繼承的話,子物件參照的父類別鏈至多隻能含有一個QObject

QObject有一個很重要的特點,就是不支援拷貝。

  1. 在 Qt 的多繼承體系中,只有一個類可以擁有 QObject 功能,這個類必須是多繼承鏈中的第一個類。QObject 類為了支援元物件系統、訊號槽機制以及其他與執行時型別資訊相關的功能,要求繼承 QObject 的類在建構函式中通過傳遞 parent 引數來指定父物件。這個父物件就是用於建立物件之間關係的,例如在父物件解構時,它的子物件也會被解構。

  2. 由於 C++ 的多繼承特性,當一個類需要繼承多個類,而其中一個類是 QObject,為了保證 QObject 的正確功能,必須將 QObject 繼承鏈放在多繼承鏈的第一個位置。這是因為 QObject 繼承鏈在構造和解構過程中有一些特殊的操作,需要在第一個類中執行。

3.如果使用模板類和QObject做多繼承,編譯不通過

這是因為 QObject 類需要在構造和解構過程中執行特殊的操作,以支援元物件系統、訊號槽機制和其他與執行時型別資訊相關的功能。而模板類的繼承可能會干擾這些特殊操作,導致編譯錯誤或者執行時問題。

QObject 的特殊操作需要在建構函式中執行,並且這些操作需要基於類的實際型別來進行。而模板類在編譯時才會範例化,因此編譯器在處理模板類時無法獲得完整的型別資訊,從而無法保證 QObject 的特殊操作能夠正確地執行。

為了避免這些問題,Qt 推薦將 QObject 放在繼承鏈的第一個位置,以確保 QObject 的特殊操作可以正確地執行。如果您的類需要多繼承,可以考慮使用介面繼承(純虛擬函式)來實現所需的功能,以避免在多繼承中使用 QObject 和模板類導致的問題。

問題場景

為什麼我突然會想到這個問題呢,因為我確實遇到了,場景大概是這樣的:
我現在在開發一個軟體框架。我現在這個框架內的所有物件都需要包含一些訊號和槽(這是為了統一上下資訊的交換),我們管這個最底層的東西叫TSG_Caller,理論上我這個框架內所有的東西都應該要繼承到這個TSG_Caller中,方便我進行管理:


它實際上只有一些訊號和槽,我希望任何物件都能通過註冊,在kernel內統一地操作這些訊號與槽,不僅僅是一些細分的類,大類也要提供這些東西。

當我開發到一定程度之後,我細化到對某個裝置的操控。於是我寫了一個模板類,用來初始化一些模板類的基本型別。我希望不同的裝置有不同的輸入引數型別,於是我們就有了如下的裝置模板類:


於是這裡就出現問題了:QObject和模板類的繼承相容不能說不好,只能說基本完全不允許使用。當然了,這也是可以理解的,畢竟MetaObject這麼強大的功能不能支援模板也是理所當然的事。

至於解決方法,那就只能改造這個裝置類了,不能用模板類了,而是隻能採用通用的輸入型別。我這裡因為輸入的引數型別有限而且都是明文,所以我這裡將所有的輸入和輸出都改成QString傳輸json字串,當然了輸入的字串也要提供給外部一個判斷的函數,以免外部輸入錯誤。

修改後的介面如下:

不知道有沒有更好的辦法,也許有,但是目前我只想到這種比較簡單的方法