在這一點上,該庫是實驗性的,它是一種純粹的好奇心。不能保證接口或實施質量的穩定性。出於自己的風險使用。
Dyno比香草C ++更好地解決了運行時多態性的問題。它提供了一種定義可以非侵入性實現的接口的方法,並提供了一種完全可定制的方式來存儲多態對象並將其分配到虛擬方法。它不需要繼承,分配或留下舒適的價值語義世界,並且可以在表現優於香草C ++的同時這樣做。
Dyno是純淨的純淨實現,也稱為Rust特質對象,go界面,Haskell類型類和虛擬概念。在引擎蓋下,它使用了一種稱為類型擦除的C ++技術,它是std::any , std::function和許多其他有用類型的想法。
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}另外,如果您發現這是太多的樣板,並且可以使用宏觀站站立,則以下等同於:
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}這是一個C ++ 17庫。不會努力支持較老的編譯器(對不起)。眾所周知,該圖書館可以與以下編譯器合作:
| 編譯器 | 版本 |
|---|---|
| 海灣合作委員會 | > = 7 |
| 鐺 | > = 4.0 |
| Apple Clang | > = 9.1 |
該庫取決於boost.hana和boost.callabletraters。單位測試取決於Libawful,基準測試取決於Google基準測試,boost.typeerasure和mpark.variant,但您不需要它們使用庫。對於本地開發, dependencies/install.sh腳本可用於自動安裝所有依賴項。
Dyno是僅限標題的庫,因此沒有什麼可構建的。只需在編譯器的標題搜索路徑中添加include/ Directory(並確保滿足依賴項),就可以了。但是,可以建立一些單位測試,示例和基準:
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks 在編程中,需要使用常見接口來操縱對象,但具有不同動態類型的對像很頻繁。 C ++通過繼承解決此問題:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}但是,這種方法有幾個缺點。這是
侵入性
為了使Square和Circle滿足Drawable接口,它們都需要從Drawable基類繼承。這需要具有修改這些類的許可證,這使得繼承非常不可擴展。例如,您將如何使std::vector<int>滿足Drawable接口?你根本不能。
與價值語義不兼容
繼承要求您將多態性指針或引用對象而不是對象本身,而對象本身的其餘部分和標準庫都非常糟糕。例如,您將如何復制Drawable的s的向量?您需要提供一個虛擬clone()方法,但是現在您剛剛弄亂了界面。
緊密結合動態存儲
由於缺乏價值語義,我們通常最終將這些多態對象分配在堆上。這既是效率低下的效率又是錯誤的,因為我們的機會根本不需要動態存儲持續時間,並且具有自動存儲持續時間的對象(例如,堆棧上)就足夠了。
防止內在
95%的時間,我們最終通過多態指針或參考來調用虛擬方法。這需要三個間接:一個用於在對象內加載指針,一個用於將右輸入加載到VTable中,另一個用於向功能指針進行間接調用。所有這些跳動都使編譯器難以做出良好的內在決定。但是,事實證明,除了間接呼叫以外,所有這些間接都可以避免。
不幸的是,這是C ++為我們做出的選擇,這些是我們需要動態多態性的規則。還是真的?
Dyno解決了C ++中運行時多態性的問題,而沒有上面列出的任何缺點,還有更多的好東西。這是:
非侵入性
可以通過類型實現接口,而無需對該類型進行任何修改。哎呀,一種類型甚至可以以不同的方式實現相同的接口!與Dyno一起,您可以親吻荒謬的班級等級結構。
100%基於價值語義
可以通過其自然價值語義將多態對像傳遞。您需要復制多態對象嗎?當然,只需確保它們具有復制構造函數即可。您想確保他們不會復制嗎?當然,將其標記為已刪除。使用Dyno ,愚蠢的clone()方法和API中指針的擴散是過去的事物。
不與任何特定的存儲策略相結合
多態對象存儲的方式實際上是一個實現細節,並且不應干擾您使用該對象的方式。 Dyno使您可以完全控制對象存儲的方式。您有很多小的多態物體嗎?當然,讓我們將它們存儲在本地緩衝區中,並避免任何分配。還是您可以將東西存儲在堆上是有意義的?當然,繼續。
靈活的調度機制以實現最佳性能
存儲指向VTable的指針只是執行動態調度的許多不同實現策略之一。 Dyno可以完全控制動態調度的發生方式,實際上可以在某些情況下擊敗VTABLES。如果您的功能在熱循環中調用,則可以直接將其存儲在對像中並跳過VTable間接方向。您還可以使用特定於應用程序的知識,編譯器永遠不必優化某些動態調用 - 圖書館級的Devirtualization。
首先,您首先定義通用接口並給它命名。 Dyno提供了一種簡單的特定域語言來做到這一點。例如,讓我們定義一個Drawable接口,描述可以繪製的類型:
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };這將Drawable定義為代表任何具有稱為draw的方法的接口,以引用std::ostream 。 Dyno稱這些接口的動態概念,因為它們描述了要滿足類型的要求集(例如C ++概念)。但是,與C ++概念不同,這些動態概念用於生成運行時接口,因此名稱動態。上面的定義基本上等同於以下內容:
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};一旦定義了接口,下一步就是實際創建滿足此接口的類型。有了繼承,您將寫下這樣的東西:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};使用Dyno ,多態性是非令人震驚的,而是通過所謂的概念圖(在C ++ 0x概念圖之後)提供的:
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);該構造是c ++ 14變量模板的專業化,名為
concept_map在dyno::名稱空間中定義的concept_map。然後,我們使用dyno::make_concept_map(...)初始化該專業化。
lambda的第一個參數是隱式*this參數,當我們將draw為上述方法時暗示。也可以刪除非會員功能(請參閱相關部分)。
該概念圖定義了類型Square如何滿足Drawable概念。從某種意義上說,它將類型的Square映射到其概念的實現,從而激發了稱呼。當一種類型滿足概念的要求時,我們說類型模型(或是該概念的模型)。現在, Square是Drawable概念的模型,我們想將Square多態多態繪製用作Drawable 。有了傳統的繼承,我們將使用一個指針,以下是這樣的基類:
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{});使用Dyno ,多態性和價值語義是兼容的,並且可以高度定制多態性類型的方式。為此,我們需要定義一種可以容納任何可Drawable類型。我們將傳遞到多態函數的情況下,而不是Drawable* 。為了幫助定義該包裝器, Dyno提供了dyno::poly容器,該容器可以容納滿足給定概念的任意對象。如您所見, dyno::poly具有雙重作用:它存儲多態性對象並照顧方法的動態調度。您需要做的就是在dyno::poly上寫一個薄包裝器,以使其完全提供所需的界面:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};注意:從技術上講,您可以直接在接口中使用
dyno::poly。但是,使用具有真實方法的包裝器比dyno::poly更方便,因此建議寫包裝器。
讓我們分解一下。首先,我們定義一個成員poly_ ,該成員是一個對Drawable概念建模的任何東西的多態容器:
dyno::poly<Drawable> poly_;然後,我們定義一個構造函數,該構造函數允許從任意類型T :
template < typename T>
drawable (T x) : poly_{x} { }這裡沒有說明的假設是T實際上對Drawable概念進行了建模。的確,當您從T型對象創建dyno::poly時, Dyno會去看為Drawable和T所定義的概念圖(如果有)。如果沒有這樣的概念圖,庫將報告我們正在嘗試從不支持它的類型中創建dyno::poly ,並且您的程序不會編譯。
最後,上述定義中最奇怪,最重要的部分是draw方法:
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }這裡發生的事情是,當.draw被調用我們的drawable對象時,我們實際上將對當前存儲在dyno::poly中的對象的"draw"函數執行動態調度。現在,要創建一個可以接受任何Drawable功能,不再需要擔心界面中的指針和所有權:
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});順便說一句,如果您認為這都是愚蠢的,並且應該使用模板,那就對了。但是,請考慮以下內容,您確實需要運行時多態性:
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable());嚴格來說,您不需要包裝dyno::poly ,但是這樣做會在Dyno和其他代碼之間遇到一個不錯的障礙,而這些代碼不必擔心如何實現多態性層。另外,我們在很大程度上忽略了在上述定義中如何實現的dyno::poly 。但是, dyno::poly是一個非常強大的基於策略的容器,用於可以根據自己的性能進行定制的多態性對象。創建drawable包裝器可以輕鬆調整dyno::poly使用的實現策略,而不會影響其餘的代碼。
可以在dyno::poly中自定義的第一個方面是對象存儲在容器中的方式。默認情況下,我們只是將指向實際對象的指針存儲,就像一個基於繼承的多態性一樣。但是,這通常不是最有效的實現,這就是為什麼dyno::poly允許自定義的原因。為此,只需將存儲策略傳遞給dyno::poly 。例如,讓我們定義drawable包裝器,以便它試圖在本地緩衝區中存儲多達16字節的對象,但是如果對象較大,則將其倒回堆:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
};請注意,除了我們定義中的策略之外,沒有什麼。那是Dyno的一個非常重要的宗旨。這些策略是實施詳細信息,它們不應更改您編寫代碼的方式。通過上述定義,您現在可以像以前一樣創建drawable S,並且當您要從16字節中繪製的對象創建drawable對象時,不會發生分配。但是,當它不合適時, dyno::poly將在堆上分配足夠大的緩衝區。
假設您實際上永遠不想進行分配。沒問題,只需將策略更改為dyno::local_storage<16>即可。如果您嘗試從太大的對像中構造drawable ,則您的程序將不會編譯。我們不僅保存了分配,而且如果我們將基於傳統的基於繼承的方法進行比較,我們還會保存每次訪問多態對象時的指針間接。通過為您的特定用例調整這些(重要的)實現細節,您可以使程序比經典繼承更有效。
還提供了其他存儲策略,例如dyno::remote_storage和dyno::non_owning_storage 。 dyno::remote_storage是默認的一個,它總是存儲指向堆分配的對象的指針。 dyno::non_owning_storage存儲了一個已經存在的對象的指針,而不必擔心該對象的壽命。它允許對對象實現非持有的多態性視圖,這非常有用。
自定義存儲策略也可以很容易地創建。有關詳細信息,請參見<dyno/storage.hpp> 。
當我們介紹dyno::poly時,我們提到它具有兩個角色。第一個是存儲多態對象,第二個是執行動態調度。就像可以自定義存儲一樣,也可以使用策略來定制動態調度的方式。例如,讓我們定義drawable包裝器,以便將VTable存儲在drawable對象本身中,而不是存儲指向VTable的指針。這樣,每次訪問虛擬功能時,我們都會避免一個間接:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
};請注意,除了VTable策略之外,沒有什麼需要改變我們drawable類型的定義。此外,如果我們願意,我們可以獨立於VTable策略更改存儲策略。在上述情況下,即使我們保存了所有間接方向,我們也通過使drawable對象更大(因為它需要在本地持有VTable)來為其付費。如果我們在VTable中具有許多功能,這可能會很過分。取而代之的是,遠程存儲大多數Vtable,但僅嵌入我們稱為大部分的功能更有意義。 Dyno通過使用選擇器可以輕鬆地進行操作,該選擇可以用於自定義策略適用的功能:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
};鑑於此定義,VTable實際上分為兩部分。第一部分是drawable對象的本地,僅包含draw方法。第二部分是指向靜態存儲中的VTable的指針,該指針具有剩餘方法(例如,驅動器)。
Dyno提供了兩個VTable政策, dyno::local<>和dyno::remote<> 。這兩種策略必須使用選擇器進行自定義。圖書館支持的選擇器是dyno::only<functions...> , dyno::except<...>和dyno::everything_else (也可以拼寫為dyno::everything )。
定義概念時,通常可以為至少與概念相關的某些功能提供默認定義。例如,默認情況下,使用名為draw的成員函數(如果有)實現Drawable概念的抽象"draw"方法可能是有意義的。為此,可以使用dyno::default_concept_map :
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
);現在,每當我們嘗試查看某種T型如何實現Drawable概念時,如果未定義概念圖,我們都會回到默認概念圖。例如,我們可以創建一個新的類型Circle :
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle"即使我們沒有明確定義Circle的概念圖, Circle也是Drawable的模型。另一方面,如果我們要定義這樣的概念圖,則它將比默認一個優先:
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle"有時為完整類型的全部定義概念圖有時很有用。例如,我們可能想製作std::vector<T> Drawable的模型,但只有當T可以打印到流時。通過使用此(不是這樣)的秘密技巧可以很容易地實現這一點:
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "請注意,我們完全不必修改
std::vector。我們如何使用經典多態性來做到這一點?答:不做。
Dyno允許刪除在任意參數上派遣的非會員函數和功能(但也只有一個參數)。為此,只需使用dyno::function定義概念,而不是dyno::method ,然後使用dyno::T佔位符表示被刪除的論點:
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { }; dyno::T const&參數上面使用的代表調用函數的對象的類型。但是,它不必是第一個參數:
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };概念的實現不會改變概念是否使用方法或函數,而是確保您函數實現的參數與概念中聲明的函數的參數相匹配:
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
);最後,當在dyno::poly上調用function時,您必須明確傳遞所有參數,因為Dyno無法猜測您想派遣哪個參數。該概念中用dyno::T佔位符聲明的參數應通過dyno::poly本身:
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};