此文檔參考自:https://gracicot.github.io/conceptmodel/2017/09/13/concept-model-part1.html ,覺得很有趣,就翻譯過來了
一、Concept-Model:多態的新視角
面向對象編程大家都很熟悉,只需實現一個接口 Interface
。但這種使用經典 OOP 實現的多態性是侵入性的,即使在真正不需要的地方也會強制多態行爲,比如總會觸發堆分配,伴隨着一個包含基類的列表。今天我們要介紹一種新的概念模型( Runtime Concept
或 Virtual Concept
),這是一種可能會改變你對多態性及其試圖解決的問題的看法的模型。
讓我們從一個簡單的例子開始:樣例中有一個接口 abstract_task
,一個或多個實現(即 print_task
),一個類型擦除列表 tasks
,以多態方式執行該 process
函數。
// Our interface.
struct abstract_task {
virtual void process() = 0;
virtual ~abstract_task() = default;
};
// An implementation of our interface.
struct print_task : abstract_task {
void process() override {
std::cout << "Print Task" << std::endl;
}
};
// A type erased list of tasks.
std::vector<std::unique_ptr<abstract_task>> tasks;
// A function that push a new task in the list.
void push(std::unique_ptr<abstract_task> task) {
tasks.emplace_back(std::move(task));
}
// execute all tasks and clear the list.
void run() {
for(auto&& task : tasks) {
task->process();
}
tasks.clear();
}
上面的代碼符合我們大部分的編程直覺。首先,這裏需要動態分配,且沒有辦法解決它。但真實意圖不是“我們想要 100% 的時間進行動態分配!” ,真實意圖是“我們想要一個類型擦除的任務列表”。然而,始終通過動態分配和多態恰好是最常見的方式,而且它也是語言自動實現多態的唯一方式。
其次,它不適用於所有類,很多人可能會說:
是的,只需實現該接口即可!所有類型都有效!
問題是,並非所有類型都可以繼承 abstract_task
。假設有這樣一個類:
struct some_library_task : library_task {
void process() { /* ... */ }
};
且要求你不能更改該類,你必須實現一個 Adaptor
才能使其在代碼中工作。
此外,還有另一種類不可能擴展接口:lambdas
。是的,lambda
!你的代碼可能與他們不兼容!想象一下寫這樣的東西:
push([] { std::cout << "print something!"; });
遺憾的是,這行不通,因爲 lambda
不是動態分配的,也不會擴展類 abstract_task
。
Concept-Model
慣用法旨在解決這些問題,我們來具體是怎麼實現的。
二、Concept-Model: The adapter pattern on steroids
在本節中,我們將解釋從經典 OOP 到 Concept-Model
的遷移過程。將會分解爲許多小步驟,以便更容易理解。
首先,函數 push
接受一個 std::unique_ptr
。想象一下,假如你有幾十個函數以這種方式執行任務,如果有一天你需要所有那些採用 std::unique_ptr<abstract_task>
原始指針或引用的函數怎麼辦?我們先從這一點入手:取而代之的是包含指針的結構:
struct task {
task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}
std::unique_ptr<abstract_task> wrapped;
};
// A vector of task, which wrap a unique pointer.
std::vector<task> tasks;
// We take a task by value, since it's constructible from a unique pointer.
void push(task t) {
tasks.emplace_back(std::move(t));
}
但現在還是有些問題,some_task.wrapped->process()
的用法會很難受,繼續調整:
struct task {
task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}
void process() {
wrapped->process();
}
private:
std::unique_ptr<abstract_task> wrapped;
};
void run() {
for(auto&& task : tasks) {
task.process();
}
tasks.clear();
}
現在已經很不錯了!對於任何地方 std::unique_ptr<abstract_task>
,你都可以放到 tasks裏(隱式構造),且是 pass-by-value
。
push(std::make_unique<print_task>());
但是等等……這並沒有解決我們的問題!我們想要支持 lambda,改變對象的發送方式,避免堆分配,這真的有用嗎?
當然!在該列表中,我們現在可以做一件事:改變傳遞對象的方式。無需更改 200 個函數簽名,我們只需更改 task
的構造函數。
現在,希望 push
函數能夠接收 some_library_task
。爲此,我們需要一個 Adaptor
來使這些類型適應接口abstract_task
:
// Our adapter. We contain a library task and implementing the abstract_task interface
struct some_library_task_adapter : abstract_task {
some_library_task_adapter(some_library_task t) : task{std::move(t)} {}
void process() override {
task.process();
}
some_library_task task;
};
struct task {
task(std::unique_ptr<abstract_task> t) noexcept : wrapped{std::move(t)} {}
// We can now receive a library task by value.
// We move it into a new instance of adapter.
task(some_library_task t) noexcept :
wrapped{std::make_unique<some_library_task_adapter>(std::move(t))} {}
void process() {
wrapped->process();
}
private:
std::unique_ptr<abstract_task> wrapped;
};
int main() {
// push a new task to the vector
push(some_library_task{});
}
到此,我們可以通過 pass-by-value
方式來 push
未繼承 abstract_task
的 some_library_task
對象。
但是,那些繼承自 abstract_task
的 task
還不能 pass-by-value
,而必須使用 ptr
。因此,我們需要將爲每個類創建一個 Adaptor
, 但我們不希望任何外部類擴展 abstract_task
,因此它將是一個私有成員類型:
struct task {
task(some_library_task task) noexcept :
self{std::make_unique<library_model_t>(std::move(t))} {}
task(print_task task) noexcept :
self{std::make_unique<print_model_t>(std::move(t))} {}
task(some_other_task task) noexcept :
self{std::make_unique<some_other_model_t>(std::move(t))} {}
void process() {
self->process();
}
private:
// This is our interface, now named concept_t instead of abstract_task
struct concept_t {
virtual ~concept_t() = default;
virtual void process() = 0;
};
// We name our struct `model` instead of `adapter`
struct library_model_t : concept_t {
library_model_t(some_library_task s) noexcept : self{std::move(s)} {}
void process() override { self.process(); }
some_library_task self;
};
struct print_model_t : concept_t {
library_model_t(print_task s) noexcept : self{std::move(s)} {}
void process() override { self.process(); }
print_task self;
};
struct some_other_model_t : concept_t {
library_model_t(some_other_task s) noexcept : self{std::move(s)} {}
void process() override { self.process(); }
some_other_task self;
};
// We quite know it's wrapped. Let's name it self
std::unique_ptr<concept_t> self;
};
這太荒謬了!我們總不能爲所有的
abstract_task
的派生類都複製一份構造函數,以及繼承concept_t
的子類代碼。
的確,C++ 中有一個很棒的工具,它經過精心設計,可以避免無意識的複製粘貼:模板!
struct task {
template<typename T>
task(T t) noexcept : self{std::make_unique<model_t<T>>(std::move(t))} {}
void process() {
self->process();
}
private:
struct concept_t {
virtual ~concept_t() = default;
virtual void process() = 0;
};
template<typename T>
struct model_t : concept_t {
model_t(T s) noexcept : self{std::move(s)} {}
void process() override { self.process(); }
T self;
};
std::unique_ptr<concept_t> self;
};
int main() {
// natural syntax for object construction! Yay!
push(some_library_task{});
push(my_task{});
push(print_task{});
}
問題解決了!我們代碼的 API 中不再有複製粘貼,不再有繼承,不再有指針!
三、Conclusion
這個 Concept-Model
是如何解決我們在開頭列出的所有問題?
首先,它可以自然地應用多態性,與其他代碼看起來很統一,語法也更簡潔。
void do_stuff() {
// Initialize a std::string using a value in direct initialization
std::string s{"value"};
// Pretty similar syntax eh?
task t{print_task{}};
// Or if you like AAA style
auto s2 = std::string{"potato"};
auto t2 = task{print_task{}};
// use string like this
auto size = s.size();
// use task like that. Again, pretty similar
t.process();
}
沒有箭頭,沒有 new
,沒有std::make_*
。所有的多態性隱藏在實現細節中,潛在的生效的。其次,它避免了堆分配。是的,即使我們在內部通過唯一指針傳遞我們的對象。
void do_stuff() {
some_task t;
// do some stuff with task
t.stuff();
// maybe push the task
if (condition()) {
push(std::move(t));
}
}
在上面示例中,t有條件地被推入列表。如果我們不需要堆分配和多態性,我們可以在運行時決定不使用它。還有其他策略,比如使用 SBO 來避免動態分配,我將在其他部分介紹。
第三,我們的任務實現可以按照 process
自己想要的方式實現功能。例如:
struct special_task {
int process(bool more_stuff = false) const {
// ...
}
};
這仍然滿足了這個概念。t.process()
即使函數是常量、接受可選參數或具有不同的返回類型,我們仍然可以調用。