std::declval
和 decltype
題圖來自於 C++ Type Deduction Introduction - hacking C++ 但略有變形以適合 banner
關於 decltype
decltype(expr)
是一個 C++11 新增的關鍵字,它的作用是將實體或者表示式的型別求出來。
#include <iostream>
int main() {
int i = 33;
decltype(i) j = i * 2;
std::cout << j;
}
它很簡單,無需額外解釋。
但如此簡單的一個東西,怎麼就需要新增一個關鍵字這麼大件事呢?還是超程式設計鬧的!超程式設計世界裡,長的懷疑人生的一串模板類宣告讓人崩潰,重複書寫它們更是累贅。例如一條執行時除錯日誌輸出:
這不是我印象中最長的名稱,只是最順手就能擷取的一個引用。這樣的例子多的是。
借用我的 談 C++17 裡的 State 模式之二 也即 fsm-cxx 的使用例子稍加改寫來體現 decltype 的用處:
void test_state_meta() {
machine_t<my_state, void, payload_t<my_state>> m;
using M = decltype(m);
// equals to: using M = machine_t<my_state, void, payload_t<my_state>>;
// @formatter:off
// states
m.state().set(my_state::Initial).as_initial().build();
// ...
}
顯然,using M = decltype(m)
更簡練,特別是當 machine_t<my_state, void, payload_t<my_state>>
可能是一超級長帶超級多模板引數的定義的字串時,decltype 的價值還會體現的更明顯。
在超程式設計裡,特別是涉及到大型類體系彼此糾結的情形時,很多時候可能不能不借助 decltype 的能力以及 auto 自動推導能力,因為在一個具體場景中可能我們不能預設具體型別會是什麼。
規範化的編碼風格
此外,善用 decltype 和 using 能夠為你的程式碼規範性和編碼省力性上貢獻力量。
在編寫一個類時,我們應該多加使用 using 提供的型別別名能力,當然同時這其中可能會涉及到對 decltype 的運用。
使用 using 的好處在於,可以提前顯式地催促編譯器進行相關型別推導,如果有錯可以在一組 using 語句處進行修正,不必在一大堆程式碼段落中去研究為何型別用錯。
用錯了型別又可能引發大堆程式碼的被迫改寫。
使用 using 還能幫助你減少程式碼段落修改。例如 using Container=std::list<T>
改為 using Container=std::vector<T>
時,你的已經寫就的程式碼段落乃至於 Container _container
宣告均可以一絲一毫不做修改,只需要重新編譯就夠。
本小節不給參考用例,因為那會喧賓奪主。而且時機不到,講給你聽也不起作用。
關於 std::declval
std::declval<T>()
也沒什麼好說的,它能返回型別 T 的右值引用參考。
但是 cppref 講的真是雲裡霧裡,說到底 declval 到底能幹什麼?它就是用於返回一個 T 物件的偽造例項,同時又具有右值引用參考。換句話說,它等價於下面的 objref 的編譯期態:
T obj{};
T &objref = obj{};
首先,它在詞法和語義上等價於 objref,是物件 T 的例項值,且具有 T&& 的型別;其次,它僅用於非求值的場合;再次,它並不真的存在。啥意思,說人話就是在編譯期中,需要一個值物件,但並不希望這個值物件被編譯為一個二進位制實體,那就用 declval 虛擬地構造一個,從而彷佛獲得了一個臨時物件,可以在該物件上施加操作,例如呼叫成員函式什麼的,但既然是虛擬的,就不會真的存在這麼個臨時物件,所以我稱之為偽例項。
我們常常並不真的直接需要 declval 求值求得的偽例項,更多的是需要藉助於這個偽例項來求取到相應的型別描述,也就是 T。所以一般情況下 declval 之外往往包圍著 decltype 計算,設法拿到 T 才是我們的真實目的:
#include <iostream>
namespace {
struct base_t { virtual ~base_t(){} };
template<class T>
struct Base : public base_t {
virtual T t() = 0;
};
template<class T>
struct A : public Base<T> {
~A(){}
virtual T t() override { std::cout << "A" << '\n'; return T{}; }
};
}
int main() {
decltype(std::declval<A<int>>().t()) a{}; // = int a;
decltype(std::declval<Base<int>>().t()) b{}; // = int b;
std::cout << a << ',' << b << '\n';
}
可以看到,A<int>
的偽例項能夠“呼叫” A 的成員函式 t(),然後藉助於 decltype 我們就可以拿到 t() 的返回型別,並用來宣告一個具體的變數 a。因為 t() 的返回型別為 T,所以 main() 函式中的這條變數宣告語句實際上等價於 int a{};
。
這個例子是為了幫助你理解 declval 的實際含義,例子本身是比較無意義的。
declval 的力量
declval(expr)
的核心力量在上面的例子中顯示的很明白:它不會對 expr 真正求值。所以你不必在 expr 處產生任何臨時物件,也不會因為表示式很複雜而發生真實的計算。這對於超程式設計的複雜環境是非常有用的。
下面的來自於某 ppt 的一頁還展示了表示式不必求值僅求型別的用例:
FROM: HERE
但不僅如此,declval 的不求值還衍生出了進一步的力量。
無預設建構函式
如果一個類沒有定義預設建構函式,在超程式設計環境中可能是很麻煩的。例如下面的 decltype 就無法通過編譯:
struct A{
A() = delete;
int t(){ return 1; }
}
int main(){
decltype(A().t()) i; // BAD
}
因為 A() 是不存在的。
但改用 declval 就能夠繞過問題了:
int main(){
decltype(std::declval<A>().t()) i; // OK
}
純虛類
在純虛基類上有時候超程式設計會比較麻煩,這時候可能可以藉助 declval 來避開純虛基類不能例項化的問題。
在第一個示例中有相應的參考 decltype(std::declval<Base<int>>().t()) b{}; // = int b;
。
Refs
- C++ Type Deduction Introduction - hacking C++
- std::declval - cppreference.com
- decltype specifier - cppreference.com
Tricks
上面的程式碼涉及到了一些慣用法,下面做一簡單的背景介紹,也包含一點點聯想延伸。
採用一個普通的抽象類作為基類
模板類的體系設計中,如果基類的程式碼、資料很多,可能會導致膨脹問題。一個解決方法是採用一個普通的基類,並在其基礎上建立模板化的基類:
struct base {
virtual ~base_t(){}
void operation() { do_sth(); }
protected:
virtual void do_sth() = 0;
};
template <class T>
struct base_t: public base{
protected:
virtual void another() = 0;
};
template <class T, class C=std::list<T>>
struct vec_style: public base_t<T> {
protected:
void do_sth() override {}
void another() override {}
private:
C _container{};
};
這樣的寫法,可以將通用邏輯(不必泛型化的)抽出到 base 中,避免留在 base_t 中隨著泛型例項化而膨脹。
純虛類如何放入容器裡
順便也談談純虛類,抽象類,的容器化問題。
對於類體系設計,我們鼓勵基類純虛化,但這樣的純虛基類就無法放到 std::vector 等容器中了:
#include <iostream>
namespace {
struct base {};
template<class T>
struct base_t : public base {
virtual ~base_t(){}
virtual T t() = 0;
};
template<class T>
struct A : public base_t<T> {
A(){}
A(T const& t_): _t(t_) {}
~A(){}
T _t{};
virtual T t() override { std::cout << _t << '\n'; return _t; }
};
}
std::vector<A<int>> vec; // BAD
int main() {
}
怎麼破?
這裡用 declval 是沒意義的,應該使用智慧指標來裝飾抽象基類:
std::vector<std::shared_ptr<base_t<int>>> vec;
int main(){
vec.push_back(std::make_shared<A<int>>(1));
}
由於我們為泛型類 base_t 宣告瞭非泛型的基類 base,所以還可能採用 std::vector<base>
的方法,但這要求你將所有 virtual 介面都抽取到 base 中,那樣做的話,總會有一部分泛型介面無法抽取,所以這種方法有可能是行不通的。
如果覺得虛擬函式與其過載如此痛苦竟然不能忍的話,你可以考慮 談 C++17 裡的 Builder 模式 所介紹的 CRTP 慣用法的能力,CRTP 在模板類繼承體系中是個很強大的編譯期多型能力。
除此而外,還可以放棄基類抽象化的設計方案,改用所謂的執行時多型 trick 來設計類體系。
Runtime Polymorphism
這是一種由 Sean Parent 提供的 執行時多型 編碼技術:
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Animal {
public:
struct Interface {
virtual std::string toString() const = 0;
virtual ~Interface() = default;
};
std::shared_ptr<const Interface> _p;
public:
Animal(Interface* p) : _p(p) { }
std::string toString() const { return _p->toString(); }
};
class Bird : public Animal::Interface {
private:
std::string _name;
bool _canFly;
public:
Bird(std::string name, bool canFly = true) : _name(name), _canFly(canFly) {}
std::string toString() const override { return "I am a bird"; }
};
class Insect : public Animal::Interface {
private:
std::string _name;
int _numberOfLegs;
public:
Insect(std::string name, int numberOfLegs)
: _name(name), _numberOfLegs(numberOfLegs) {}
std::string toString() const override { return "I am an insect."; }
};
int main() {
std::vector<Animal> creatures;
creatures.emplace_back(new Bird("duck", true));
creatures.emplace_back(new Bird("penguin", false));
creatures.emplace_back(new Insect("spider", 8));
creatures.emplace_back(new Insect("centipede", 44));
// now iterate through the creatures and call their toString()
for (int i = 0; i < creatures.size(); i++) {
std::cout << creatures[i].toString() << '\n';
}
}
其特點是基類不是基類,基類的巢狀類才是基類:Animal::Interface 才是用於類體系的抽象基類,它是純虛的,但卻不影響 std::vector<Animal>
的有效編譯與工作。Animal 使用簡單的轉接技術將 Animal::Interface 的介面(如 toString())對映出來,這種轉接有點像 Pimpl Trick,但也有一點微小的區別。
後記
總的一句話,declval 就是專門治那些無法例項化具體物件的場合的。
std::declval<T>()
也被典型地用在編譯期測試等用途,下一次有閒再做探討吧,那個話題太大了。