使用動態多態,我們可以創建一個抽象類Item
,它描述了一個項目在清單中的功能。這很有用,因爲通過這樣的課程,可以管理我們不知道的項目,我們只知道它們會表現得像一個。
class Item
{
public:
virtual ~Item() = default;
virtual const char* description() const = 0;
};
進一步說,所有其他項目(劍,瓶子等),可以從該類繼承,從而使他們的是一個項目的特點:
class Sword: public Item
{
public:
Sword() = default;
virtual ~Sword() = default;
const char* description() const override
{ return "Sword"; }
};
在description
方法,它覆蓋了Item::description
抽象一個,所以只要你從Sword
的實例調用.description
,就會返回"Sword"
字符串。例如:
Sword sword{};
Item& item = sword;
std::puts(item.description()); // prints the "Sword" string.
它現在更簡單的存儲項目,我們只需要使用它們的載體:std::vector<std::unique_ptr<Item>>
。
#include <vector>
#include <memory>
std::vector<std::unique_ptr<Item>> inventory{};
inventory.emplace_back(std::make_unique<Sword>());
但爲什麼我們不能有一個std::vector<Item>
?僅僅因爲不可能從Sword
構建Item
。實際上,甚至不可能構建一個Item
,因爲它具有抽象方法(即它們只是用來描述方法的原型,而不是它的定義/實現)。
std::unique_ptr
是少數C++智能指針之一,它在那裏,所以我們不必手動處理分配。在代碼中使用new
和delete
可能會導致內存泄漏和災難,因爲程序員的注意力分散,所以智能指針會導致此問題不存在。
最後,爲了有一個項目回來,你可以簡單地向下轉換的東西回劍:
const auto& item = inventory[0]; // item is `const std::unique_ptr<Item>&`
puts(item->description()); // prints "Sword"
puts(dynamic_cast<Sword*>(item.get())->description()); // also prints "Sword"
後者(使用的dynamic_cast)將創建一個轉變指針指向第一個項目,從item.get()
method,但是以Sword*
的形式。如果Sword
中存在Item
不常見的方法或數據成員,則需要執行此操作。例如,如果你有這樣的事情「詮釋sword_power`,你可以這樣做:
auto sword = dynamic_cast<Sword*>(item.get());
if (sword != nullptr)
{
std::printf("sword power: %d\n", sword->sword_power);
}
當然,如果檢查中投成功,是可選的,但這樣做可以防止你的代碼執行未定義行爲(以情況下,轉換失敗並返回一個空指針)。
目前仍然在做這個系統(不之前,C++ 17),使用新的庫工具std::variant
的另一種方式。
基本上,變種可以讓你一次有許多不同的類型之一帽子可以讓你有很多不同的類型(比如一個結構體),一個變體只允許一次從一個類型獲取一個值。爲了更好地理解它,這裏是它的工作原理是:
#include <variant> // C++17
struct Sword {};
struct Bottle {};
std::variant<Sword, Bottle> item = Sword{};
像std::tuple
,一個變異體將具有其可能的類型時,模板參數作爲參數(即Sword
和Bottle
類型item
的整體型的一部分) 。這樣,你可以一次有一把劍或一瓶,但永遠不會在同一時間。讓我們用這個新功能來實現我們的庫存。首先,我們必須改變我們的班了一下:
class Sword
{
public:
int power;
Sword() = default;
const char* description() const
{ return "Sword"; }
};
class Bottle
{
public:
bool empty;
Bottle() = default;
const char* description() const
{ return "Bottle"; }
};
我們刪除的虛擬方法和動態多態的需求,你會看得更遠,我們將不再需要動態分配了,因爲std::variant
需要工作在堆棧中(這意味着程序也會更快(也許))。現在
,爲Item
概念,我們做的變體的一個別名與我們的類:
using Item = std::variant<Sword, Bottle>;
而且我們可以用向量也使用此:
std::vector<Item> inventory{};
inventory.emplace_back(Sword{});
inventory.emplace_back(Bottle{});
有幾種方法與這些項目互動,以防你需要他們回來。一種是使用std::holds_alternative
:
auto& item = inventory[0];
if (std::holds_alternative<Sword>(item))
{
auto& sword = std::get<Sword>(item);
sword.power = 42;
std::printf("%s: %d\n", sword.description(), sword.power);
}
它檢查一個變體的對象是否被保持給予類型的值。在這種情況下,我們檢查了Sword
。然後,如果那裏有一把劍,我們使用std::get<>
獲得價值,它返回對我們物品的引用,作爲Sword
。
訪問實物的另一種方式是使用std::visit。簡而言之,訪問者就像一個具有重載功能的對象。你可以像一個函數一樣調用訪問者。爲了創建一個訪問者,我們可以使用一個結構超載的operator()
或lambda表達式。這是第一個方法:
struct VisitItem
{
void operator() (Sword& sword) const
{
std::printf("%s: %d\n", sword.description(), sword.power);
}
void operator() (Bottle& bottle) const
{
std::printf("%s: %s\n", bottle.description(),
bottle.empty? "empty" : "full");
}
};
auto& item = inventory[0];
std::visit(VisitItem{}, item); // we give an instance of VisitItem for std::visit, and the item itself.
這裏,std::visit
將調用正確的operator()
的變種內當前對象(即項目)。如果物品持有劍,則將調用operator() (Sword&)
。
另一種方法是製作超載的lambdas。這是一個有點複雜的是,因爲我們沒有爲圖書館的工具,但與C++ 17它實際上是更容易實現它:
template <typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
template <typename... TTs>
constexpr explicit overload(TTs&&... tts) noexcept
: Ts{std::forward<TTs>(tts)}...
{
}
};
template <typename... Ts>
explicit overload(Ts&&...) -> overload<std::decay_t<Ts>...>;
,然後用它像這樣:
auto& item = inventory[0];
auto visitor = overload(
[] (Sword& s) { std::printf("power: %d\n", s.power); },
[] (Bottle& b) { std::printf("%s\n", b.empty? "empty" : "full"); }
);
std::visit(visitor, item);
如果您想了解overload
結構中發生了什麼,它將繼承您所給出的所有lambda表達式,並將operator()
重載帶入重載查找中(因爲來自基類的函數重載不被視爲候選對象,所以您必須using overload
)。 overload
結構之後的行是user-defined deduction guide,這意味着您可以根據構造函數更改模板結構的模板參數。
爲什麼這些項目從'庫存'繼承? – user463035818
這個設計似乎有一個基本問題。將商品添加到庫存的唯一機制是'add()',它只能將商品添加到自身。作爲庫存的劍的概念很奇怪。 –
你的班級結構令人困惑 - 「Sword' *是一個*庫存」是怎麼回事?爲什麼'Inventory'包含其他'Inventory'? – Quentin