在一般 C++ 類別的成員函式裡面,一般是都可以直接存取自己的其他成員;另外,也還有一個特別的 this
指標,會指向到自己、在必要時可以用來更明確地使用。
而在 C++23 在核心語言的部分、則是針對這部分則是一個強化、透過「explicit object parameter」(比較常見的是「deducing this」)、來讓開發者有更明確地方法,可以使用 this
這個指標。
他基本的使用形式是:
class CTest { public: int implicitFunc( int v); int explicitFunc(this CTest& self, int v); };
在上面的 CTest
這個類別中,implicitFunc()
和 explicitFunc()
這兩個成員函式的介面實際上是相同的;雖然 explicitFunc()
看起來多了一個名為 self
的引數,但是實際上在呼叫的時候並不需要傳入,而是和 implicitFunc()
一樣、傳入一個 int
即可。
而 explicitFunc()
的第一個引數,是 this CTest& self
,重點是在前面多了一個 this
;這代表它是用來取代本來的 this
指標的。
實際上把上面的程式寫完整一點,會變成像是下面的樣子:
#include <iostream> class CTest { public: int implicitFunc( int v) { //return this->iVal + v; return iVal + v; } int explicitFunc(this CTest& self, int v) { return self.iVal + v; } protected: int iVal = 0; }; int main() { CTest a; std::cout << a.implicitFunc(1) << std::endl; std::cout << a.explicitFunc(1) << std::endl; }
在傳統的成員函式 implicitFunc()
中,可以直接去存取成員資料 iVal
、也可以透過 this
這個指標來做存取。
而在 explicitFunc()
中,由於第一個引數是用來明確地取代 this
,所以在函式內會變成不能使用 this
這個指標,要存取成員要透過 self
來操作。
在呼叫時,則是沒有任何差別,可以把第一個引數完全無視,系統會自己處理掉。
簡化 lvalue / rvalue、const / non-const 的對應
以上面的基本使用情境來說…好像看不出來有什麼用?不過實際上,self
的型別應該是得用 template 的形式來定義,才會讓他比較有用。
在《C++23’s Deducing this: what it is, why it is, how to use it》這篇裡有提到,透過這種 explicit object parameter 的寫法,基本上可以用來簡化對於同一個函式、針對 lvalue / rvalue、const / non-const 四種狀態的 overload。
他這邊舉的例子是 std::optional<>
(之前的介紹)。
本來針對呼叫的物件是 lvalue 或是 rvalue、還有是否有 const 性質、一個 value()
的函式可能會需要四種實作:
template <typename T> class optional { // version of value for non-const lvalues constexpr T& value()& { if (has_value()) return this->m_value; throw bad_optional_access(); } // version of value for const lvalues constexpr T const& value() const& { if (has_value()) return this->m_value; throw bad_optional_access(); } // version of value for non-const rvalues... are you bored yet? constexpr T&& value()&& { if (has_value()) return std::move(this->m_value); throw bad_optional_access(); } // you sure are by this point constexpr T const&& value() const&& { if (has_value()) return std::move(this->m_value); throw bad_optional_access(); } // ... };
裡面基本上差異很小,所以會很繁瑣。而如果透過 explicit object parameter 的話,則可以把四個函式用一個 template 來做掉:
template <typename T> struct optional { // One version of value which works for everything template <class Self> constexpr auto&& value(this Self&& self) { if (self.has_value()) return std::forward<Self>(self).m_value; throw bad_optional_access(); } // ... };
透過這個寫法,可以一次對應上面四種不同的狀態。
而《C++23: Deducing This》這篇裡面,應該算是有比較清楚的說明:
#include <iostream> struct Test { template <typename Self> void explicitCall(this Self&& self, const std::string& text) { std::cout << text << ": "; std::forward<Self>(self).implicitCall(); std::cout << '\n'; } void implicitCall()& { std::cout << "non const lvalue"; } void implicitCall() const& { std::cout << "const lvalue"; } void implicitCall()&& { std::cout << "non const rvalue"; } void implicitCall() const&& { std::cout << "const rvalue"; } }; int main() { Test test; const Test constTest; test.explicitCall("test"); // Self= Test& constTest.explicitCall("constTest"); // Self= const Test& std::move(test).explicitCall("move(test)"); // Self= Test std::move(constTest).explicitCall("move(consTest)"); // Self= const Test }
這邊的程式輸出的結果,會是:
test: non const lvalue constTest: const lvalue move(test): non const rvalue move(consTest): const rvalue
可以看到,雖然呼叫的都是 explicitCall()
這個函式, 但是由於呼叫來源的物件會判定成不同的狀態,所以最後呼叫到的會是不同的實作。
理論上,如果想要讓自己定義的類別 getter 函式能對應各種狀況,都可以使用這樣的寫法;下面是個例子:
class cat { toy held_toy_; public: template <class Self> auto&& get_held_toy(this Self&& self) { return std::forward<Self>(self).held_toy_; } };
而也由於在使用 explicit object parameter 的形式來寫的狀況下,物件本身是否為 const 和是否為參考的狀態已經變成由 self
來負責了,所以這時候函示本身的後面是不能加上 cv-qualifier(const
)或 ref-qualifier(&
)的!
不過老實說,以往就算要寫 getter 函式好像也不會真的寫滿…所以個人對於這部分沒那麼有感覺就是了。
搭配 CRTP 的使用
如果是要寫 CRTP(curiously recurring template pattern) 的靜態多型的話,這個新語法或許算是比較實用了~
CRTP 的基本形式,會是像下面的樣子:
#include <iostream> template <class T> class Base { public: void interface() { static_cast<T*>(this)->implementation(); } }; class Derived : public Base<Derived> { public: void implementation() {} }; int main() { Derived d; d.interface(); }
基本上這邊會透過 Base
類別來定義出介面、但是要呼叫的時候需要把 this
轉換成實作的類別(Derived
)來呼叫實作的函式。
而也因為在定義 Derived
時,需要繼承 Base<Derived>
才能完成這樣的操作,所以讓整個程式看起來有點詭異。
在這邊如果改成使用 C++23 的 deducing this 的話,則可以改寫成:
class Base { public: template<typename Self> void interface(this Self& self) { self.implementation(); } }; class Derived : public Base { public: void implementation() {} };
這邊可以看到,基礎類別 Base
的 template 可以完全拿掉了~取而代之的,是用來作為介面的成員函式 interface()
要改成 explicit object parameter 的寫法、並需要搭配 template 來定義 self
的型別。
如此一來,當在透過 Derived
的物件來呼叫 interface()
這個函式的時候,Self
的型別就會是 Derived
、所以就不需要特別再去轉換型別了~
而也由於在繼承的時候不需要把自身的型別當作 template 的引數,看起來好像也沒那麼詭異了。
不過這邊也要注意的,是這邊雖然看起來好像和用 virtual 函式的時候一樣、可以從 Base
的型別來呼叫 interface()
,但是實際上是不行的!如果寫成下面的型式的話,是會編譯錯誤的!
Base* b = new Derived(); b->interface(); // ERROR!
所以呢,其實這樣的寫法和傳統的 CRTP 有一樣的限制(靜態多型的限制),那就是他雖然可以透過基礎型別來建立介面、但是並不能透過基礎型別來操作。
搭配 lambda
再來,這個新語法也可以搭配 lambda 來使用。
其中一個最直覺的應用,就是讓 lambda 的遞迴更好寫!下面就是一個簡單的例子:
auto Fibonacci = [](this auto&& self, int x) { if (x < 2) return x; return (self(x - 1) + self(x - 2)); };
能這樣玩的原因,主要是因為實際上 lambda 就是一個會產生對應的 operator()
的類別了。
以值的型態傳遞 this(pass this by value)
由於 explicit object parameter 的寫法可以自己去定義物件本身傳遞進來的型態,所以其實也可以不要使用參考、而是使用「值」的形式來傳遞的。
這樣有什麼好處呢?這邊基本上是牽扯到編譯器產生機器碼的最佳化了。以結論來說,就是在針對比較小型的物件,這樣寫在執行階段會比較快!
如果是使用一般的函式寫法的話,中間還會需要需要去 stack 裡面分配空間、儲存 this
的指標等等,步驟比較多;但是如果是使用 「pass this by value」的形式的話,這些步驟則可以省略、在產生出來的 assembly 裡面程式會明顯簡化很多。
以微軟給的範例(參考)來說,如果程式是:
struct just_a_little_guy { int how_smol; int uwu(); }; int main() { just_a_little_guy tiny_tim{ 42 }; return tiny_tim.uwu(); }
那 MSVC 產生出來的組合語言會是:
sub rsp, 40 lea rcx, QWORD PTR tiny_tim$[rsp] mov DWORD PTR tiny_tim$[rsp], 42 call int just_a_little_guy::uwu(void) add rsp, 40 ret 0
但是如果用 pass this by value 來定義 uwu()
的話,可以寫成:
struct just_a_little_guy { int how_smol; int uwu(this just_a_little_guy); };
這時候他產生出來的組語就會變成:
mov ecx, 42 jmp static int just_a_little_guy::uwu(this just_a_little_guy)
感覺上,真的簡化不少啊~不過,Heresy 這邊其實沒有搞得很清楚,到底哪些情況下可以靠這種方法來加速就是了?
特化的 static?
最後,其實在個人來看,explicit object parameter 在各方面來看都有點像是特化的 static member function?
主要的差別大概就只有在要呼叫的時候、會和 static 函式不一樣吧?下面是一個例子:
#include <iostream> class CTest { public: int implicitFunc(int v) { //return this->iVal + v; return iVal + v; } int explicitFunc(this CTest& self, int v) { return self.iVal + v; } static int staticFunc(CTest& self, int v) { return self.iVal + v; } protected: int iVal = 0; }; int main() { CTest a; std::cout << a.implicitFunc(1) << std::endl; std::cout << a.explicitFunc(1) << std::endl; std::cout << CTest::staticFunc(a, 1) << std::endl; }
而同時,explicit object parameter 的成員函式的指標,由於已經把本身(this
)分離了,所以和一般的函式不一樣,也比較接近 static member function:
auto pif = &CTest::implicitFunc; // int (__cdecl CTest::*)(int) __ptr64 std::cout << (a.*pif)(1) << "\n"; auto pef = &CTest::explicitFunc; // int (__cdecl*)(class CTest & __ptr64,int) std::cout << pef(a, 1) << "\n"; auto psf = &CTest::staticFunc; // int (__cdecl*)(class CTest & __ptr64,int) std::cout << psf(a, 1) << "\n";
如果想要統一呼叫的寫法的話,則可以使用 std::invoke()
這個函式(參考)。
而其他像是內部沒有 this
、不能加 cv-qualifier/ref-qualifier 的特性,其實都和 static 是一致的。實際上,在 Visual Studio 裡面如果把 explicit object parameter 加上 const
的話,某些地方給的錯誤訊息也是「靜態成員函式不能有類型限定詞」。
這篇大概就這樣了?其實他還有一些其他的特性、功能,只是有的 Heresy 自己也沒搞得很懂,所以就先跳過了。 XD
而目前 C++23 雖然應該已經定稿了,但是實際上還算是在一個很早期的階段,各家編譯器的支援都很不完整;以這個新語法來說,目前應該只有 Visual Studio 有支援,g++ 和 clang 都還沒能支援。
所以就算要用,看來還是再等一段時間、看看各家編譯器的實作狀況再說吧~
參考: