摘要訊息 : 如何使用樣板超編程來接管編碼器的函式引數匹配工作?
0. 前言
如果大家使用過 OpenCV 等程式庫進行程式設計, 那麼我相信大家應該體會過對於那些陌生的函式, 我們需要仔細地閱讀官方給出的教學, 並且正確地給出每一個函式的引數. 但是有時候, 我們難免將函式引數的順序搞錯, 從而導致了函式給出錯誤結果, 程式運作不正常甚至產生編碼錯誤. 考慮下面這個函式 :
float f(float a, float b, float weight) {
assert(weight >= 0.0f and weight <= 1.0f);
return a * weight + b * (1 - weight);
}
一旦引數的順序出現錯誤, 很可能導致運作時程式被終止或者產生錯誤的結果. 像 Python 這樣的程式設計語言就有這樣的特性 : f(weight = 0.8f, b = 5, a = 1)
. 這樣即使引數的順序不正確, 但是結果仍然是正確的, 並且我們無需記住函式的參數順序. 然而 C++ 並沒有這樣的特性. 為了達到類似的效果, 我們可以借助 std::map
來完成 :
#include <map>
float f(std::map<std::string, float> params) {
auto a {params.find("a")};
auto b {params.find("b")};
auto weight {params.find("weight")};
if(a == params.cend()) {
throw std::invalid_argument("Argument a is invalid");
}
if(b == params.cend()) {
throw std::invalid_argument("Argument b is invalid");
}
if(weight == params.cend()) {
return a->second * 0.5f + b->second * 0.5f;
}
return a->second * weight->second + b->second * (1 - weight->second);
}
使用 std::map
雖然可以一定程度上減少錯誤的發生, 但是缺點也很明顯 : std::map
的維護需要付出一些運作時效能. 在參數量比較多的情況下, 我們容易遺漏引數, 從而導致一些例外情況. 另外, 如果參數型別不同的情況下, 我們可能需要借助型別擦除 (例如 std::variant
或者 void *
). 如果這樣做的話, 必定導致 std::map
更加難以維護.
仔細觀察函式的參數 (parameter) 和引數 (argument), 我們不難發現, 函式匹配的本質就是參數到引數的影射 (mapping), 也就是鍵 (key) 到值 (value) 的影射. 這個過程我們稱之為引數決議 (argument resolution). 這就是為什麼我們可以借助 std::map
來完成類似工作的原因. 不用 std::map
的原始版本, 這種影射是由編碼器來處理的; 使用了 std::map
的版本由我們主動接管了這項工作. 如果我們能把這個過程從運作期移動到編碼期, 那麼就可以節省大量的時間.
要在編碼期解決這個問題, 樣板超編程的使用就不可避免. 為了做到類似的效果, std::map
對我們仍然有啟發, 因為我們要構造的就是帶有鍵到值影射的編碼期容器, 它類似於 std::map
.
我們需要明確的是 :
- 參數和引數繫結的過程應該簡潔的, 但它可能比一個一個地直接給出引數要複雜一些;
- 當給定引數數量不足的時候, 編碼器應該擲出編碼錯誤;
- 引數獲取的方式應該比
std::map
要更簡單; - 引數的給出順序可以像 Python 那樣任意, 只要參數型別和引數型別匹配即可.
基於這些需求, 我們可以開始嘗試實現引數決議.
1. 引數決議的雛形
最開始, 我們針對引數決議實作之後的樣子是模糊的, 但是我們可以大致給出其用法 :
template <typename Params>
auto f(const Params ¶ms) {
auto a {params.get_arg_a()};
auto b {params.get_arg_b()};
auto weight {params.get_arg_weight()};
return a * weight + b * (1 - weight);
}
struct f_args;
int main(int argc, char *argv[]) {
auto default_f_args {f_args::create()};
auto args {default_f_args.set_arg_a(1.5f).set_arg_b(2.0f).set_arg_weight(0.42f)};
auto result {f(args)};
// ...
}
首先, 這是引數決議的雛形, 這種方案是顯然不可行的. 因為我們不可能為每一個參數都預先寫入 get_arg_???
到 f_args
的成員函式中. 為此, 對於 get_arg_???
和 set_arg_???
, 我們可以從 std::get
中獲得啟示, 使用數字作為鍵影射到值. 於是, Code 3 中的虛擬碼可以修改為 :
template <typename Params>
auto f(const Params ¶ms) {
auto a {params.get<0>()};
auto b {params.get<1>()};
auto weight {params.get<2>()};
return a * weight + b * (1 - weight);
}
struct f_args;
int main(int argc, char *argv[]) {
auto default_f_args {f_args::create()};
auto args {default_f_args.set<0>(1.5f).set<1>(2.0f).set<2>(0.42f)};
auto result {f(args)};
// ...
}
但是這種方案的缺點也很明顯, 如果有兩個函式, 那麼 0
就可以代表兩種不同的參數, 編碼器如何知道這個 0
代表著哪一個函式的哪一個參數的呢? 另外, 在多人協作的時候也會有麻煩, 我們必須為固定的程式設計師分配固定的數字編號以防止混亂. 除了這兩個之外, 我們根本無法從數字中理解要影射的值代表著什麼. 而普通的函式呼叫和使用了 std::map
的方案中, 我們可以很容易地從參數的名稱知道其作用.
為了解決這些問題, 我們效仿 std::map
, 使用字串替代數字. 使用 std::string
是不可以的, 因為它的所有操作都不是編碼期常數表達式 (C++ 20 之後, std::string
才支援 constexpr
). 因此, 我們需要使用字面值字串, 也就是 const char []
或者 const char *
, 它是可以作為樣板引數的. 那麼, 我們再度修改 Code 4 中的虛擬碼 :
template <typename Params>
auto f(const Params ¶ms) {
auto a {params.get<"a">()};
auto b {params.get<"b">()};
auto weight {params.get<"weight">()};
return a * weight + b * (1 - weight);
}
struct f_args;
int main(int argc, char *argv[]) {
auto default_f_args {f_args::create()};
auto args {default_f_args.set<"a">(1.5f).set<"b">(2.0f).set<"weight">(0.42f)};
auto result {f(args)};
// ...
}
但是, 上面的程式碼會出現編碼錯誤. 因為不論對於函式樣板還是類別樣板, 只要樣板非型別參數接受的是一個字面值字串, 那麼 C++ 要求這個字串必須有地址 :
template <const char *>
struct type1;
template <const char []>
struct type2;
constexpr const char str1[] {""};
constexpr const char *str2 {""};
using t1 = type1<str1>; // OK
using t2 = type1<str2>; // Error
using t3 = type2<str1>; // OK
using t4 = type2<str2>; // Error
using t5 = type1<"">; // Error
using t6 = type2<"">; // Error
int main(int argc, char *argv[]) {
constexpr const char str3[] {""};
constexpr const char *str4 {""};
static constexpr const char str5[] {""};
static constexpr const char *str6 {""};
using t7 = type1<str3>; // Error
using t8 = type1<str4>; // Error
using t9 = type2<str3>; // Error
using t10 = type2<str4>; // Error
using t11 = type1<str5>; // OK
using t12 = type1<str6>; // Error
using t13 = type2<str5>; // OK
using t14 = type2<str6>; // Error
}
因此, 如果我們需要使用字面值字串實作引數決議的話, 我們就需要這樣 :
constexpr const char f_param_a[] {"a"};
constexpr const char f_param_b[] {"b"};
constexpr const char f_param_weight[] {"weight"};
template <typename Params>
auto f(const Params ¶ms) {
auto a {params.get<f_param_a>()};
auto b {params.get<f_param_b>()};
auto weight {params.get<f_param_weight>()};
return a * weight + b * (1 - weight);
}
struct f_args;
int main(int argc, char *argv[]) {
auto default_f_args {f_args::create()};
auto args {default_f_args.set<f_param_a>(1.5f).set<f_param_b>(2.0f).set<f_param_weight>(0.42f)};
auto result {f(args)};
// ...
}
使用字面值字串雖然解決了可讀性和多人協作的問題, 但是歧異問題仍然存在. 例如 f1
有一個參數為 a
, f2
有一個參數也是 a
, 那麼 constexpr const char param_a[] {"a"};
是同時適用於 f1
和 f2
的. 我們並不希望這樣.
其實這裡還有一個問題 :
template <const char *> struct s; constexpr const char str1[] {"hello"}; constexpr const char str2[] {"hello"}; using t1 = s<str1>; using t2 = s<str2>; static_assert(std::is_same_v<t1, t2>); // pass or error?
上面這段程式碼中的靜態斷言是否通過呢? 就我在最新的 Clang, GCC 和 MSVC 中測試的結果顯示, 上面程式碼的編碼是不能通過的. 也就是說, Clang, GCC 和 MSVC 都將
str1
和str2
區分對待並且放入了不同的儲存空間中, 雖然它們內部的值是完全相同的. 但是不排除有編碼器將str1
和str2
優化到同一片空間中, 導致上面的程式碼可以編碼通過.
通過上面的討論, 我們否定了使用數字和字面值字串作為影射中的鍵, 仍然需要尋找其它方案. 其實, std::is_same
就給予我們提示 : 類別名稱作為鍵是再適合不過的. 首先, 類別名稱可以提示我們參數代表著什麼, 也就相當於函式參數的名稱; 其次, 不同的類別必定使得 std::is_same
這個超函式回傳 false
. 那麼, 我們就可以寫出如下的虛擬碼 :
template <typename ...>
struct argument_resolution;
struct f_arg_a;
struct f_arg_b;
struct f_arg_weight;
template <typename Params>
auto f(const Params ¶ms) {
auto a {params.get<f_arg_a>()};
auto b {params.get<f_arg_b>()};
auto weight {params.get<f_arg_weight>()};
return a * weight + b * (1 - weight);
}
using f_args = argument_resolution<f_arg_a, f_arg_b, f_arg_weight>;
int main(int argc, char *argv[]) {
auto default_f_args {f_args::create()};
auto args {default_f_args.set<f_arg_a>(1.5f).set<f_arg_b>(2.0f).set<f_arg_weight>(0.42f)};
auto result {f(args)};
// ...
}
這樣, f_args
就被具體化為 argument_resolution<f_arg_a, f_arg_b, f_arg_weight>
. 而類別樣板 argument_resolution
中有一個名稱為 create
的靜態成員函式. 我們可以大致將 argument_resolution
寫出來 :
template <typename ...Prototypes>
struct argument_resolution {
static auto create() {
// ...
return /* ... */;
}
};
為了方便起見, 我們先針對一些名稱進行統一, 然後開始接下來的章節.
名稱 | 說明 |
鍵, 原型型別 | 指的是影射中的參數. 例如, Code 9 中的 f_arg_a , f_arg_b 和 f_arg_weight . |
Prototype | 將會作為樣板參數的名稱出現, 指的是單個原型型別. |
Prototypes | 將會作為樣板參數包的名稱出現, 指的是多個原型型別. |
值 | 指的是影射中的引數. 例如, Code 9 中的 f_arg_a 要影射的值是 1.5f , f_arg_b 要影射的值是 2.0f , f_arg_weight 要影射的值是 0.42f . |
實際型別, 影射型別 | 值得是影射中的引數對應的型別. 例如 Code 9 中的 f_arg_a 要影射的值 1.5f 的型別為 float , f_arg_b 要影射的值 2.0f 的型別為 float , f_arg_weight 要影射的值 0.42f 的型別為 float . |
RealType | 將會作為樣板參數的名稱出現, 指的是單個實際型別. |
RealTypes | 將會作為樣板參數包的名稱出現, 指的是多個實際型別. |
2. 實作靜態成員函式 create
為了實作類別樣板 argument_resolution
中的靜態成員函式 create
, 我們首先要解決它的回傳型別是什麼, 我們稱之為 create_returning_type
. 這同樣是一個類別. 通過 Code 9, 我們發現 create_returning_type
包含了兩個成員函式 set
和 get
. 除此之外, create_returning_type
還必須負責儲存 argument_resolution
的樣板參數包 Prototypes
對應的實際型別包 RealTypes
. 在 Code 9 中, 函式 f
的 Prototypes
為 f_arg_a
, f_arg_b
和 f_arg_weight
, 這三個鍵影射的實際型別都是 float
. 也就是說, Code 9 中的函式 f
的 RealTypes
為 float
, float
和 float
. 而實際型別我們可以放在 create_returning_type
的樣板參數中 :
template <typename ...RealTypes>
struct create_returning_type {
auto set();
auto get();
};
這樣, argument_resolution<f_arg_a, f_arg_b, f_arg_weight>
就會對應到 create_returning_type<float, float, float>
, 第一個 f_arg_a
對應到第一個 float
, 第二個 f_arg_b
對應到第二個 float
, 第三個 f_arg_weight
對應到第三個 float
. 而 create_returning_type<float, float, float>
也會作為函式 f
唯一參數的對應型別. 到此, 我們解決了原型型別 Prototypes
和實際型別 RealTypes
的儲存問題.
例題 1. 使用引數決議類別樣板 argument_resolution
替換下面函式 f2
的參數, 並且寫出函式 f2
唯一參數的對應型別.
big_floating_point_type f2(int a, float b, double big, double x1, double x2, double x3, double y, long double z, const char *other);
解 :替換之後的程式碼為
struct f2_arg_a; struct f2_arg_b; struct f2_arg_big; struct f2_arg_x1; struct f2_arg_x2; struct f2_arg_x3; struct f2_arg_y; struct f2_arg_z; struct f2_arg_other; using f2_args = argument_resolution<f2_arg_a, f2_arg_b, f2_arg_big, f2_arg_x1, f2_arg_x2, f2_arg_x3, f2_arg_y, f2_arg_z, f2_arg_other>; template <typename Params> big_floating_point_type f2(const Params &);
\square
f2
的參數Params
的實際型別會被替換為create_returning_type<int, float, double, double, double, double, long double, const char *>
.\square
\blacksquare
我們上面說的一直是函式使用了引數決議之後最終的回傳型別, 那麼也就是說, create_returning_type
的成員函式 set
的回傳型別和最終的回傳型別可能不太一樣. 而成員函式 set
的用法為
params.set<f_arg_???>(value);
其中, f_arg_???
為原型型別, value
為要影射的值. 因此, 成員函式 set
必須接收一個樣板引數以明確原型型別. 此外, set
還需要接受一個實際型別, 這個實際型別是值 value
的對應型別. 成員函式 get
的用法為
auto value {params.get<f_arg_???>()};
故成員函式 get
必須接收一個樣板引數作為原型型別, 以便找到要影射的值進行回傳. 綜上所述, 我們可以將 create_returning_type
改為
template <typename ...RealTypes>
struct create_returning_type {
template <typename Prototype, typename RealType>
auto set(const RealType &);
template <typename Prototype>
auto get();
};
有人可能注意到了成員函式 set
和 get
的回傳型別都被設為 auto
. get
這樣設計無可厚非, 因為它必定存在回傳值. set
這樣設計是因為它也存在回傳值, 以支援類似於 params.set<f_arg_second>(value_2).set<f_arg_first>(value_1)
這樣交換順序的用法. 這也是為了滿足引數的給出順序可以像 Python 那樣任意. 我們仍然以 Code 9 中的函式 f
為例 :
using f_args = argument_resolution<f_arg_a, f_arg_b, f_arg_weight>;
auto temp1 {f_args::create()};
auto temp2 {temp1.set<f_arg_b>(2.0f)};
auto temp3 {temp2.set<f_arg_weight>(0.42f)};
auto args {temp3.set<f_arg_a>(1.5f)};
在 Code 15 中, 我們並沒有按照 a
, b
和 weight
這樣的順序給定引數的值, 而是首先給定了 b
的值, 再給定 weight
的值, 最後才給定 a
的值. 如果 set
沒有回傳值, 就無法做到引數順序可以交換的要求.
我們繼續針對 Code 15 中的程式碼進行分析. 已經知道的是, 無論引數給定的順序是什麼樣的, 變數 args
的型別必定是 create_returning_type<float, float, float>
. 那麼, 現在的問題就是 temp1
, temp2
和 temp3
的型別是什麼.
當我們呼叫函式 f_args::create
時, 我們只給定了原型型別 f_arg_a
, f_arg_b
和 f_arg_weight
. 這個時候, 這些原型型別要影射到的實際型別及其值都沒有給出, 因此這個時候我們還不知道原型型別應該影射到的實際型別是什麼. 由於 create_returning_type
是一個可變參數的類別樣板, 因此一種解決方案是讓 create
直接回傳 create_returning_type<>
, 然後接下來向引數列表中不斷添加原型型別引數即可. 這對函式 f
來說確實可行. 因為最終 f_arg_a
, f_arg_b
和 f_arg_weight
都會影射到實際型別 float
. 但若 f_arg_a
要影射為 int
, 若 f_arg_b
要影射為 char
, f_arg_weight
要影射到 double
, 那麼按照 Code 15 的順序, 各個變數的型別應該是
變數 | 型別 |
temp1 | create_returning_type<> |
temp2 | create_returning_type<char> |
temp3 | create_returning_type<char, double> |
args | create_returning_type<char, double, int> |
這樣, 影射的順序就完全亂了. 因為第 k 個原型型別必須影射到第 k 個實際型別. 因此, 這種方案不太可行 (但是並非完全不可行, 把這個方案想象為向 std::vector
中插入元素即可, 只是實作起來非常麻煩罷了). 最好的方法是先用一個型別進行佔位, 我們引入 :
struct nil {};
專門用於填充未知的實際型別. 於是, 我們有
變數 | 型別 |
temp1 | create_returning_type<nil, nil, nil> |
temp2 | create_returning_type<nil, char, nil> |
temp3 | create_returning_type<nil, char, double> |
args | create_returning_type<int, char, double> |
這樣, 影射的順序就正確了. 接下來, 我們實作 argument_resolution
的靜態成員函式 create
就有思路了 : 我們首先檢查 Prototypes
中有多少原型型別, 然後向 create_returning_type
的樣板引數列表中添加對應數量的 nil
即可. 最終, create
的回傳型別為 create_returning_type<nil, nil, ..., nil>
, 而回傳值為 create_returning_type<nil, nil, ..., nil> {}
. 為此, 我們必須把類別樣板名稱和類別樣板參數分開來 :
我們曾在《【C++ Template Meta-Programming 與 Standard Template Library】實作 <type_traits>
(下)》中實作過 class_template_traits
, 由此我們可以受到啟發 :
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
為了使得遞迴停止, 我們在 N = 0
的時候停止添加 Nil
即可 :
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
}
為了理解 default_returning_type
, 大家可以直接從 N = 0
的時候開始理解. 剛開始, Ts
這個引數包是空的, 每當 N
增加 1
, 那麼 Ts
這個引數包就在最後增加一個 Nil
. 這樣, ReturningType
的樣板引數列表中就會有 N
個 Nil
.
有了 default_returning_type
, 我們就可以直接實作 argument_resolution
中的靜態成員函式 create
:
template <typename ...Prototypes>
struct argument_resolution {
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, Nil>::type {};
}
};
到此, create
的實作就完成了, 我們整理一下到目前為止關於引數決議的程式碼, 並且測試一下運作是否正常, 結果是否如我們所願 :
template <typename ...RealTypes>
struct create_returning_type {
template <typename Prototype, typename RealType>
auto set(const RealType &);
template <typename Prototype>
auto get();
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename ...Prototypes>
struct argument_resolution {
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
我們用下面的程式碼來測試 Code 19 是否運作正常 :
#include <type_traits>
float f(float a, float b, float weight);
struct f_arg_a;
struct f_arg_b;
struct f_arg_weight;
using f_args = argument_resolution<f_arg_a, f_arg_b, f_arg_weight>;
static_assert(std::is_same_v<decltype(f_args::create()), create_returning_type<nil, nil, nil>>); // OK
大家也可以用其它類似的函式替換 Code 20 中的函式 f
進行進一步測試. 值得一提的是, 到目前為止, 我們所有的操作都是在編碼期完成的, 沒有任何運作期消耗.
3. 用實際型別替換佔位型別 nil
接下來, 我們的目標是能夠讓 f_args()::create().set<f_arg_b>(2.0f
) 能夠將 create_returning_type<nil, nil, nil>
中的第二個 nil
替換為 2.0f
對應的型別, 即 float
.
我們知道, 第 k 個原型型別必定影射到第 k 個實際型別. 那麼替換的大致思路就可以這樣來描述 :
- 當原型型別
Prototype
和要替換的實際型別RealType
給定時, 設原型型別包Prototypes
和實際型別包RealTypes
中有 n 個型別引數; - 找到
Prototype
在Prototypes
中的位置, 不妨記為 k; - 記實際型別包
RealTypes
中前 k - 1 個引數組成的引數包為RealTypesPrevious
, 原型型別包中後 n - k - 2 個引數組成的引數包為RealTypesNext
; - 向
create_returning_type<>
的樣板引數列表中添加RealTypesPrevious
形成create_returning_type<RealTypesPrevious...>
, 然後再添加要替換的型別RealType
(丟棄原來處在這個位置nil
) 形成create_returning_type<RealTypesPrevious..., RealType>
, 最後將RealTypesNext
加入到RealType
的後面即可. 最終, 我們有create_returning_type<RealTypesPrevious..., RealType, RealTypesNext>
.
我們假設有一個超函式 substitute
可以做到上述步驟, 那麼我們可以初步將 substitute
寫出來 :
template <typename Prototype, typename RealType, template <typename ...> typename ReturningType, typename ...RealTypes>
struct substitute {
using type = /* ... */;
};
其中, Prototype
是原型型別, RealType
是替換後的型別. ReturningType<RealTypes...>
就是替換之前的型別, 更深入地, 它還可以寫為 ReturningType<RealTypesPrevious..., nil, RealTypesNext...>
. 而超函式 substitute
的回傳值應該是 ReturningType<RealTypesPrevious..., RealType, RealTypesNext...>
.
為了實作 substitute
, 我們首先要獲得 RealTypesPrevious
. 然而遺憾的是, C++ 並不支援 using ...types = Ts...;
這種寫法 (但是對於非型別引數, 在 Lambda 表達式的捕獲列表中, 允許 [...args = args...]
這種寫法), 也就是型別包是不可以作為超函式的回傳值的. 因此, 我們還需要將 RealTypesPrevious
儲存在 ReturningType
的樣板引數列表中. 我們可能需要為此實作一個名稱為 real_types_previous
的超函式用於回傳前 k - 1 個原型型別. 然而幸運的是, 我們並不需要實作 real_types_previous
這個超函式. 因為在實作 substitute
的過程中, 就會產生 real_types_previous
這個超函式的效果.
真正需要實作的是 type_location
和 perform_real_types_next
. type_location
的作用是定位 Prototype
在 Prototypes
中的位置並且回傳. 超函式 perform_real_types_next
的作用是在實際型別 RealType
替換了 nil
之後, 把剩餘的 RealTypesNext...
直接放到 ReturningType<RealTypesPrevious..., RealType>
的樣板引數列表最後. 有了前面的實作經驗之後, perform_real_types_next
的實作就比較簡單 :
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
type_location
和其它超函式稍有不同, 因為我們需要它回傳一個值, 而不是型別 :
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
有了 type_location
和 perform_real_types_next
之後, 我們就可以開始實作 substitute
. substitute
的第一件事應該是從原型型別包 Prototypes
中找到 Prototype
, 如果當前要找的原型型別和 Prototype
不同, 那麼就把當前 Prototype
對應的 RealType
直接放置到 ReturningType<...>
樣板引數列表的最末尾即可. 這便是我們之前所說的 real_types_previous
的效果. 當 substitute
找到了 Prototype
的位置的時候, 前 k - 1 個實際型別就已經被放到了 ReturningType
的樣板引數列表中形成了 ReturningType<RealTypesPrevious...>
. 接下來我們只需要借助 perform_real_types_next
完成 substitute
的實作即可 :
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
我們整理一下到現在為止的所有程式碼 :
#include <type_traits>
template <typename ...RealTypes>
struct create_returning_type {
template <typename Prototype, typename RealType>
auto set(const RealType &);
template <typename Prototype>
auto get();
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename ...Prototypes>
struct argument_resolution {
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
我們發現, substitute
是被 create_returning_type
的成員函式 set
所用, 因此應該把 perform_prototypes_next
, type_location
和 substitute
移動到 create_returning_type
前面. 當我們使用 create_returning_type
的成員函式 set
的時候, 由 substitute
負責將 Prototype
對應位置的 nil
替換為 RealType
. 但是, create_returning_type
沒有保存 Prototypes
, 也就無法在 Prototypes
中找到 Prototype
的位置. 這樣來說, 應該把 create_returning_type
作為 argument_resolution
的巢狀類別 :
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type {
template <typename Prototype, typename RealType>
auto set(const RealType &) {
return typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type {};
}
template <typename Prototype>
auto get();
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
現在, create_returning_type
的成員函式 set
已經可以回傳正確的型別了. 我們用下面的程式碼測試一下 :
#include <type_traits>
float f(int, char, double, float, void *, unsigned long long);
struct f_arg_1;
struct f_arg_2;
struct f_arg_3;
struct f_arg_4;
struct f_arg_5;
struct f_arg_6;
using f_args = argument_resolution<f_arg_1, f_arg_2, f_arg_3, f_arg_4, f_arg_5, f_arg_6>;
using t1 = decltype(f_args::create());
using t2 = decltype(t1 {}.set<f_arg_1>(1));
using t3 = decltype(t1 {}.set<f_arg_2>('a'));
using t4 = decltype(t1 {}.set<f_arg_3>(2.0));
using t5 = decltype(t1 {}.set<f_arg_4>(2.0f));
using t6 = decltype(t1 {}.set<f_arg_5>((void *)0));
using t7 = decltype(t1 {}.set<f_arg_6>(1ull));
using t = decltype(f_args::create().set<f_arg_5>((void *)0).set<f_arg_1>(1).set<f_arg_6>(1ull).set<f_arg_3>(0.0).set<f_arg_4>(0.0f).set<f_arg_2>('a'));
static_assert(std::is_same_v<t1, f_args::create_returning_type<nil, nil, nil, nil, nil, nil>>); // OK
static_assert(std::is_same_v<t2, f_args::create_returning_type<int, nil, nil, nil, nil, nil>>); // OK
static_assert(std::is_same_v<t3, f_args::create_returning_type<nil, char, nil, nil, nil, nil>>); // OK
static_assert(std::is_same_v<t4, f_args::create_returning_type<nil, nil, double, nil, nil, nil>>); // OK
static_assert(std::is_same_v<t5, f_args::create_returning_type<nil, nil, nil, float, nil, nil>>); // OK
static_assert(std::is_same_v<t6, f_args::create_returning_type<nil, nil, nil, nil, void *, nil>>); // OK
static_assert(std::is_same_v<t7, f_args::create_returning_type<nil, nil, nil, nil, nil, unsigned long long>>); // OK
static_assert(std::is_same_v<t, f_args::create_returning_type<int, char, double, float, void *, unsigned long long>>); // OK
可以看出, 到目前為止, 所有程式碼都不存在運作期消耗, 所有計算都在編碼期就已經完成.
4. 儲存給定 set
的函式引數
到這裡為止, 我們只解決了關於型別的問題. 對於 f_args::create().set<f_arg_a>(1.5f)
中的 1.5f
應該如何儲存, 我們還沒有解決. 不過很顯然, 這個值必定會存在類別當中, 只是我們還沒想到應該用什麼樣的方式去儲存. 如果 C++ 支援直接使用引數包來宣告變數, 那麼我們可以直接把 create_returning_type
寫成 :
template <typename ...RealTypes>
struct create_returning_type {
private:
RealTypes variables...; // Error
public:
template <typename Prototype, typename RealType>
auto set(const RealType &) {
return typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type {};
}
template <typename Prototype>
auto get();
};
然而這種方法不被支援. 因此, 我們必須想辦法把 RealTypes
展開, 分別對型別引數包中的每一個型別都宣告一個變數, 然後作為成員變數再放入 create_returning_type
中. 於是, 繼承就成了最好的方法. 我們宣告一個專門用於持有值的類別 :
template <typename Prototype, typename RealType>
struct value_holder {
RealType value;
};
接著, 我們借助樣板引數包的展開, 直接讓 create_returning_type
多繼承 value_holder
, 並且使用列表初始化來設定其成員函式 set
的回傳值 :
template <typename Prototype, typename RealType>
struct value_holder {
RealType value;
};
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(const RealType &value) {
return typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type {value};
}
template <typename Prototype>
auto get();
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
但是我們想得簡單了, Code 30 中的成員函式 set
是有問題的. 如果我們一旦將 nil
改為
struct nil;
也就是只進行宣告, 而不進行定義, 那麼 value_holder
中的成員變數 value
就完全無法進行初始化. 因為此時 value
的型別是 nil
, 而 nil
只是進行了宣告, 沒有被定義. 然後我們修改一下 value_holder
, 不直接進行宣告, 而是採用指標 :
template <typename Prototype, typename RealType>
struct value_holder {
RealType *value;
};
現在, 即使 RealType
是 nil
, 那麼 value
仍然可以被 nullptr
初始化, 只要我們不進行 new nil {}
這種類似操作即可. 然後, 我們重新檢查一下 Code 30 中的成員函式 set
. 我們採用 Code 9 中的函式 f
, 那麼 f_args::create().set<f_arg_weight>(0.42f)
的回傳型別為為 f_args::create_returning_type<nil, nil, float>
, 它繼承自 value_holder<f_arg_a, nil>
, value_holder<f_arg_b, nil>
和 value_holder<f_arg_weight, float>
. 那麼 set
的 return
陳述式的寫法應該是 return typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes…>::type {{}, {}, {new RealType(0.42f)}};
, 但是這個顯然是不行的. 因為 RealTypes
的數量沒有限制, 而且對於 f_args::create().set<f_arg_weight>(0.42f).set<f_arg_a>(1.5f)
, 這樣的寫法應該對 weight
的 0.42f
如何處理? 難道直接扔掉嗎...
其實, set
的回傳值可以參考 substitute
. substitute
替換的是型別, set
替換的是值. 我們知道, create_returning_type<RealType_1, RealType_2, ..., RealType_n>
繼承自 value_holder<Prototype_1, RealType_1>
, value_holder<Prototype_2, RealType_2>
, ..., value_holder<Prototype_n, RealType_n>
. 設 values
是一個引數包, 展開之後為 value_holder<Prototype_1, RealType_1>::value
, value_holder<Prototype_2, RealType_2>::value
, ..., value_holder<Prototype_n, RealType_n>::value
. 參考 substitute
, 我們要做的是
- 找到成員函式
set
的樣板引數Prototype
在型別引數包Prototypes
中的位置, 不妨記為 k; - 取
values
中的前 k - 1 個引數放入return
陳述式的初始化列表中展開; - 將函式接收的引數
value
放到初始化列表的最後; - 將
values
中的後 n - k - 2 個引數放入初始化列表的value
之後, 並且展開.
第一步我們已經在前面實作了 type_location
, 這裡可以繼續使用它. 對於第二步和第四步, 我們要在 create_returning_type
中引入一個新的函式 extract
, 用於獲取第 i
個 value_holder<Prototype_i, RealType_i>
中的 value
. 其中, i = 1, 2, ..., k - 1, k + 1, ..., n
. 因此, extract
差不多可以寫成這樣 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype>
auto extract() noexcept {
return static_cast<value_holder<Prototype, RealType>>(*this).value;
}
// ...
};
public:
// ...
};
在 return
陳述式中, RealType
是 Prototype
要影射的實際型別. 但是, 這個 RealType
是我們要在 RealTypes
中搜尋的. 而 RealType
在 RealTypes
中位置取決於 Prototype
在 Prototypes
中的位置. 為此, 我們還要實作一個超函式 find_real_type
來輔助我們找到 RealType
:
template <int I, typename RealType, typename ...RealTypes>
struct find_real_type {
using type = typename find_real_type<I - 1, RealTypes...>::type;
};
template <typename RealType, typename ...RealTypes>
struct find_real_type<0, RealType, RealTypes...> {
using type = RealType;
};
那麼 create_returning_type
的成員函式 extract
我們就可以這樣寫 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype>
auto extract() noexcept {
return static_cast<value_holder<Prototype, typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type>>(*this).value;
}
// ...
};
public:
// ...
};
然而, 我們在 set
的 return
陳述式中遇到了問題. 我們先把 set
的 return
陳述式的虛擬碼寫出來 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
// ...
template <typename Prototype, typename RealType>
auto set(const RealType &value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
return result_type {this->extract<PrototypesPrevious>()..., new RealType {value}, this->extract<PrototypesNext>()...};
}
// ...
};
public:
// ...
};
問題就在於 PrototypesPrevious
, PrototypesNext
, RealTypesPrevious
和 RealTypesNext
怎麼獲得? 我們在上面就已經說過 C++ 不支援超函式直接回傳一個型別引數包. 因此, 我們沒有辦法在 argument_resolution
和 create_returning_type
中獲得這些引數包.
轉換一下思路, 我們也可以這樣去做 :
- 找到成員函式
set
的樣板引數Prototype
在型別引數包Prototypes
中的位置, 不妨記為 k; - 使用
set
正確的回傳型別typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type
宣告一個變數result
, 把values
展開到result
的初始化列表中; - 把
result
的參考轉換為value_holder<Prototype, RealType>
, 並把value_holder<Prototype, RealType>::value
置換為給定的值 - 回傳
result
.
不管這個思路可不可行, 我們先寫出來 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
// ...
template <typename Prototype, typename RealType>
auto set(const RealType &value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
return result;
}
// ...
};
public:
// ...
};
但是, Code 32 中的成員函式 set
存在編碼錯誤. 我們仔細分析一下. 對於 Code 9 中的函式 f
, f_args::create().set<f_weight>(0.42f)
回傳值的型別為 f_args::create_returning_type<nil, nil, float>
, 它繼承自 value_holder<f_arg_a, nil>
, value_holder<f_arg_b, nil>
和 value_holder<f_arg_weight, float>
. 而替換之前, 也就是 f_args::create()
的回傳值的型別為 f_args::create_returning_type<nil, nil, nil>
, 它繼承自 value_holder<f_arg_a, nil>
, value_holder<f_arg_b, nil>
和 value_holder<f_arg_weight, nil>
. 換句話說, result
的初始化列表應該放入的各個引數的型別分別是 value_holder<f_arg_a, nil>
, value_holder<f_arg_b, nil>
和 value_holder<f_arg_weight, float>
. 問題就在最後一個 value_holder<f_arg_weight, float>
. 我們現在正在嘗試使用 value_holder<f_arg_weight, nil>::value
去初始化一個 value_holder<f_arg_weight, float>::value
. 歸根結底, 我們正在用型別為 nil *
的值去初始化一個型別為 float *
的變數. 編碼器給出的編碼錯誤也是 cannot initialize a member subobject of type 'float *' with an rvalue of type 'nil *'
. 如果我們想把 value_holder<f_arg_weight, nil>::value
通過強制型別轉化轉換為 float *
, 這就又回到了之前我們要把它從 values
中提取出來的問題...
那麼 create_returning_type
的成員函式 set
應該如何實作呢? 答案就在 float *
和 nil *
之間. 我們注意到, 任意型別都可以像萬能指標型別 void *
轉型. 因此, 解決方案就是把 value_holder
中的 RealType *
改為 void *
:
template <typename Prototype, typename RealType>
struct value_holder {
void *value;
};
那麼採用 Code 32 的 set
就不再會有編碼錯誤. 如果要用到 value_holder
中的 value
, 我們只需要通過一次型別轉換 static_cast<RealType *>(value)
再解參考就可以得到真正的值. 至於之前實作的成員函式 extract
, 我們就不再需要了.
現在我們整理一下到目前為止的所有程式碼 :
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename Prototype, typename RealType>
struct value_holder {
void *value;
};
template <int I, typename RealType, typename ...RealTypes>
struct find_real_type {
using type = typename find_real_type<I - 1, RealTypes...>::type;
};
template <typename RealType, typename ...RealTypes>
struct find_real_type<0, RealType, RealTypes...> {
using type = RealType;
};
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(const RealType &value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
return result;
}
template <typename Prototype>
auto get();
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
我們不打算在這一節進行測試, 因為我們可以在接下來實作成員函式 get
, 然後用 get
進行測試.
5. 實作 get
寫過前面這些程式碼之後, 還剩下 create_returning_type
的成員函式 get
沒有實作. get
的實作並不麻煩, 給出原型型別 Prototype
之後, 我們只需要找到對應的實際型別 RealType
, 然後通過回傳型別轉換之後的 value_holder<Prototype, RealType>::value
即可 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
// ...
template <typename Prototype>
auto &get() {
using real_type = typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type;
return *static_cast<real_type *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}
};
public:
// ...
};
為了可以從外部直接修改引數的值, 我將成員函式 get
的回傳型別改為了參考. 之前我們在丟棄的成員函式 extract
中使用了 find_real_type
. 本來在成員函式 extract
被丟棄之後, find_real_type
也應該被丟棄. 然而我仍然將 find_real_type
放入 Code 34 中, 原因就是成員函式 get
的實作也需要 find_real_type
的幫助.
到目前為止, 我們已經完成了引數決議的全部實作. 所有程式碼如下 :
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename Prototype, typename RealType>
struct value_holder {
void *value;
};
template <int I, typename RealType, typename ...RealTypes>
struct find_real_type {
using type = typename find_real_type<I - 1, RealTypes...>::type;
};
template <typename RealType, typename ...RealTypes>
struct find_real_type<0, RealType, RealTypes...> {
using type = RealType;
};
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(const RealType &value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
return result;
}
template <typename Prototype>
auto &get() {
using real_type = typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type;
return *static_cast<real_type *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
我們先用 Code 9 中的函式 f
測試一下, 測試的程式碼如下 :
#include <iostream>
float f(float a, float b, float weight) {
return a * weight + b * (1 - weight);
}
struct f_arg_a;
struct f_arg_b;
struct f_arg_weight;
using f_args = argument_resolution<f_arg_a, f_arg_b, f_arg_weight>;
template <typename Params>
float f(const Params ¶ms) {
auto a {params.get<f_arg_a>()};
auto b {params.get<f_arg_b>()};
auto weight {params.get<f_arg_weight>()};
return a * weight + b * (1 - weight);
}
int main(int argc, char *argv[]) {
std::cout << f(f_args::create().set<f_arg_b>(2.0f).set<f_arg_weight>(0.42f).set<f_arg_a>(1.5f)) << std::endl;
std::cout << f(1.5f, 2.0f, 0.42f) << std::endl;
}
我們寫了兩個多載的函式 f
, 一個使用常規的方法, 另外一個使用了引數決議. 但是上面的程式碼有些問題.
首先, params.get<f_arg_???>()
會出現編碼錯誤. 出現錯誤的原因是編碼器首先會假定成員函式樣板 get
不是樣板, 結果可能是編碼器錯把 get<f_arg_???>
中的尖括號當成是小於和大於, 從而導致這個函式呼叫變成了一個表達式. 因此, 像這種情況, 我們必須在 get
之前添加一個 template
關鍵字, 告訴編碼器 get
是一個成員函式樣板, 而不是其它.
另外一個問題是第二個函式 f
的函式參數被宣告為 const Params &
, 那麼 params.get<f_arg_???>()
中的成員函式 get
必須被 const
標識, 而我們實作的成員函式沒有被 const
標記, 並且成員函式 get
也不能被 const
標識 (使用 const
標識產生的後果是回傳的值的型別都會帶有 const
標識, 這可能會產生錯誤). 因此, 函式參數應該使用萬能參考 Params &&
, 讓編碼器自行推導 params
的型別.
綜合上面兩個解決方案, 我們修改 Code 37 :
#include <iostream>
float f(float a, float b, float weight) {
return a * weight + b * (1 - weight);
}
struct f_arg_a;
struct f_arg_b;
struct f_arg_weight;
using f_args = argument_resolution<f_arg_a, f_arg_b, f_arg_weight>;
template <typename Params>
float f(Params &¶ms) {
auto a {params.template get<f_arg_a>()};
auto b {params.template get<f_arg_b>()};
auto weight {params.template get<f_arg_weight>()};
return a * weight + b * (1 - weight);
}
int main(int argc, char *argv[]) {
cout << f(f_args::create().set<f_arg_b>(2.0f).set<f_arg_weight>(0.42f).set<f_arg_a>(1.5f)) << endl; // 輸出結果 : 1.79
cout << f(1.5f, 2.0f, 0.42f) << endl; // 輸出結果 : 1.79
}
我們發現, 兩個多載的函式 f
回傳了同一個結果. 也就是說, 引數決議的實作大致是正確的!
5. 錦上添花
儘管我們已經完成了引數決議的實作, 但是本節中, 我們將繼續完善 Code 36 中的程式碼.
5.1 支援參考和指標
為了找到更多可能存在的問題, 我寫了一個實際的演算法用於測試 argument_resolution
:
#include <iostream>
#include <string>
struct kmp_table_arg_substr;
struct kmp_table_arg_table;
struct kmp_table_arg_substr_len;
using kmp_table_args = argument_resolution<kmp_table_arg_substr, kmp_table_arg_table, kmp_table_arg_substr_len>;
template <typename Params>
void kmp_table(Params &¶ms) noexcept {
auto &&substr {params.template get<kmp_table_arg_substr>()};
auto &&table {params.template get<kmp_table_arg_table>()};
auto &&len {params.template get<kmp_table_arg_substr_len>()};
auto pos {2};
auto cursor {0};
table[0] = -1;
table[1] = 0;
while(pos < len) {
if(substr[pos - 1] == substr[cursor]) {
++cursor;
table[pos] = cursor;
++pos;
}else if(cursor > 0) {
cursor = table[cursor];
}else {
table[pos] = 0;
++pos;
}
}
}
struct kmp_search_arg_str;
struct kmp_search_arg_substr;
using kmp_search_args = argument_resolution<kmp_search_arg_str, kmp_search_arg_substr>;
template <typename Params>
int kmp_search(Params &¶ms) noexcept {
auto &&str {params.template get<kmp_search_arg_str>()};
auto &&substr {params.template get<kmp_search_arg_substr>()};
auto len_str {str.length()};
auto len_substr {substr.length()};
auto m {0}, i {0};
auto table {new int[len_substr] {}};
kmp_table(kmp_table_args::create().
set<kmp_table_arg_substr_len>(len_substr).
template set<kmp_table_arg_table>(table).
template set<kmp_table_arg_substr>(substr));
while(m + i < len_str) {
if(substr[i] == str[m + i]) {
if(i == len_substr - 1) {
return m;
}
++i;
}else {
m = m + i - table[i];
if(table[i] > -1) {
i = table[i];
}else {
i = 0;
}
}
}
return -1;
}
int main(int argc, char *argv[]) {
std::string sentence {"^^__^^I'm a noob C++ programmer, Hello~~~~!???...."};
std::string search {"llo"};
auto args {kmp_search_args::create().
set<kmp_search_arg_substr>(search).
set<kmp_search_arg_str>(sentence)};
std::cout << kmp_search(args) << std::endl;
}
在 Code 39 中, 我實作的是 KMP 演算法. KMP 演算法是用於在一個字串中搜尋另外一個字串的字串搜尋演算法. 不使用引數決議的 kmp_search
接受兩個 std::string
型別, 目的是在第一個字串中找到第二個字串. 如果能找到, kmp_search
會回傳第二個字串在第一個字串中的位置; 如果找不到, 則回傳 -1
. kmp_table
是輔助 kmp_search
的函式, 因為 KMP 演算法需要建立一個用於搜尋的表. 通過編碼錯誤, 我們大致可以了解到我們的程式碼存在兩個錯誤 : new T & {...}
和嘗試把 new T * {...}
的值指派給 void *
. 第一個錯誤很好理解, 因為 C++ 中不存在參考的指標這樣的型別. 第二個錯誤是因為 new T * {...}
的回傳值對應的型別為 T **
, 但是 T **
只能隱含地轉換為 void **
指標, 不能隱含地轉換為 void *
指標 (只有 T *
才可以).
我們思考一下. 對於指標和參考, 它們都是左值, 本身已經帶有一個地址了, 我們就沒有必要再為它們使用 new
運算子配置另一塊記憶體空間. 為此, 在成員函式 set
和 get
中, 我們要對指標和參考這兩個型別特別關注一下 :
#include <type_traits>
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(RealType &&value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
if constexpr(std::is_pointer_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = static_cast<void *>(value);
}else if constexpr(std::is_lvalue_reference_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = &value;
}else {
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
}
return result;
}
template <typename Prototype>
auto &get() {
using real_type = typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type;
if constexpr(std::is_pointer_v<real_type>) {
return static_cast<real_type>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else if constexpr(std::is_lvalue_reference_v<real_type>) {
return *static_cast<std::remove_cvref_t<real_type> *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else {
return *static_cast<real_type *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}
}
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
我們增加了兩個 if constexpr
判斷陳述式 :
- 對於指標型別 : 我們通過強制型別轉化直接將其轉換為
void *
(對於多重指標, 我們也直接用void *
), 並且指派給value_holder<Prototype, RealType>
中的value
; - 對於參考型別 : 我們取其記憶體位址, 然後將這個記憶體位址通過隱含型別轉化轉換為
void *
, 再指派給value_holder<Prototype, RealType>
中的value
; - 對於剩餘情況, 我們我們才使用記憶體配置的方式.
修改之後, Code 39 的最終輸出結果為 35
. 這個結果是正確的.
5.2 記憶體流失
有些人可能早就察覺到前面 new
出來的記憶體我們沒有對其進行回收. 為了回收這部分記憶體, 我們有幾個方案.
5.2.1 增加一個成員函式 destroy
最容易想到的方案就是在 create_returning_type
中增加一個名為 destroy
的成員函式, 用於尋訪所有 value_holder<Prototype, RealType>
, 並且針對 RealType
非指標非參考的所有 value_holder<Prototype, RealType>::value
的記憶體進行回收.
本來, 我們只需要 delete static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...
即可. 但是, 參考和指標的記憶體不是我們配置的, 我們不負責回收, 因此這個方案不可行.
由於我們要逐個將 argument_resolution<Prototype_1, Prototype_2, ..., Prototype_n>::create_returning_type<RealType_1, RealType_2, ..., RealType_n>
轉換為 value_holder<Prototype_1, RealType_1>
, value_holder<Prototype_2, RealType_2>
, ..., value_holder<Prototype_n, RealType_n>
, 那麼單獨一個 destroy
函式肯定是做不到的, 需要一個輔助函式 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
// ...
template <typename Prototype, typename RealType>
constexpr void destroy(value_holder<Prototype, RealType> &arg) noexcept {
if constexpr(not std::is_pointer_v<RealType> and not std::is_lvalue_reference_v<RealType>) {
delete static_cast<RealType *>(arg.value);
}
}
void destroy() noexcept {
this->destroy(static_cast<value_holder<Prototypes, RealTypes> &>(*this))...;
}
};
public:
// ...
};
我們把真正的回收工作交給帶有函式參數的 destroy
函式, 不帶有參數的 destroy
作為回收的介面. 這裡需要注意一個細節, 直接使用 delete
運算子去回收一個型別為 void *
型別的指標對應的記憶體是一個無效的表達式 (但是不會產生編碼錯誤, 部分編碼器可能會擲出編碼警告), 因此首先需要使用 static_cast
進行強制型別轉化. 但是 Code 41 存在編碼錯誤 : expression contains unexpanded parameter packs 'Prototypes' and 'RealTypes'
. 真正的問題就在於 C++ 不允許包展開的 ...
寫在括號之外.
為此, 我們需要一個小技巧 : 借助逗號運算子. 我們知道, 如果我們把不帶有參數的函式 destroy
寫成 this->destroy(static_cast<value_holder<Prototype_1, RealType_1> &>(*this)), this->destroy(static_cast<value_holder<Prototype_2, RealType_2> &>(*this)), ..., this->destroy(static_cast<value_holder<Prototype_n, RealType_n> &>(*this));
是沒有關係的. 因為關於 value_holder<Prototype, RealType>
的逗號運算子並沒有多載. 由此我們受到啟發, 可以將 Code 41 中不帶有參數的成員函式 destroy
改為 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
// ...
template <typename Prototype, typename RealType>
constexpr void destroy(value_holder<Prototype, RealType> &arg) noexcept {
if constexpr(not std::is_pointer_v<RealType> and not std::is_lvalue_reference_v<RealType>) {
delete static_cast<RealType *>(arg.value);
}
}
void destroy() noexcept {
(this->destroy(static_cast<value_holder<Prototypes, RealTypes> &>(*this)), ...);
}
};
public:
// ...
};
當然, 還有另外一種方法. 因為基本的思路就是不讓 ...
出現在括號之外, 並且表達式 a, b
最終的結果是 b
. 那麼, 我們也可以寫成 int dummy_array[] {0, (this->destroy(static_cast &>(*this)), 0)…};
. 還有很多方案, 大家可以自由發揮.
5.2.2 例外安全
對於出現在成員函式 set
中的陳述式 static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
, 有兩個地方可能擲出例外情況. 一個是 new
可能因為記憶體不足擲出以 std::bad_alloc
為基礎的例外情況, 另一個是在 value
建構的時候可能擲出例外情況. 舉個例子, f_args::create().set<f_arg_a>(1.5f).set<f_arg_b>(2.0f)
如果在設定 f_arg_a
的時候成功, 而在設定 f_arg_b
的時候擲出例外情況並且被捕獲, 那麼我們對於 f_arg_a
配置的記憶體就會流失.
為了解決這個問題, 我們需要針對成員函式 set
增加 try
-catch
區塊 :
template <typename ...Prototypes>
struct argument_resolution {
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(RealType &&value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
if constexpr(std::is_pointer_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = static_cast<void *>(value);
}else if constexpr(std::is_lvalue_reference_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = &value;
}else {
try {
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
}catch(...) {
this->destroy();
throw;
}
}
return result;
}
// ...
};
public:
// ...
};
5.2.3 使用智慧指標
其實使用智慧指標應該是最簡單的方案. 我們直接將 value_holder
修改為 :
#include <memory>
template <typename Prototype, typename RealType>
struct value_holder {
std::shared_ptr<void> value;
};
這個時候, 我們就無需自行實作 destroy
並且也無需再考慮例外安全問題了. 但是 std::shared_ptr
除了維護強計數之外, 還維護了弱計數. 因此效能上比我們自己實作 destroy
要差一些, 使用的記憶體也多一些. 如果要追求程式效能和記憶體使用量, 那麼我們可以自己實作一個簡單的強計數智慧指標 strong_shared_ptr
(當然, 使用 strong_shared_ptr
的效能也不如使用我們自己實作的 destroy
). 這部分大家就自己發揮吧.
5.3 原型型別重複檢查
我們知道, 任意函式的參數名稱不可以相同. 那麼自然地, 原型型別包 Prototypes
中不能出現相同的型別. 為此, 我們實作一個超函式 check_prototypes
進行檢查. check_prototypes
應該接受一個被檢查的原型型別和要檢查的原型型別包, 然後直接回傳一個 bool
型別的結果 :
template <typename Prototype, typename ...Prototypes>
struct check_prototypes {
constexpr static auto value {/* ... */};
};
那麼, check_prototypes
中的 value
應該如何初始化? 基本思想應該是從 Prototypes
中提取出一個 Prototype
, 和剩餘的原型型別進行檢查 : 如果不存在相同的型別, 那麼就再提取出另外一個原型型別進行類似的檢查; 否則, value
的值被初始化為 false
. 如果不存在相同的型別, 那麼 value
的值會被初始化為 true
.
利用上面的思想, 單獨的 check_prototypes
無法做到, 我們需要一個 check_prototypes_helper
:
template <typename ...>
struct check_prototypes_helper;
template <typename Check, typename Prototype, typename ...Prototypes>
struct check_prototypes_helper<Check, Prototype, Prototypes...> {
constexpr static auto value {std::bool_constant<not std::is_same_v<Check, Prototype> and check_prototypes_helper<Check, Prototypes...>::value>::value};
};
template <typename Check>
struct check_prototypes_helper<Check> {
constexpr static auto value {true};
};
有了 check_prototypes_helper
, check_prototypes
的實作就比較簡單了 :
template <typename Prototype, typename ...Prototypes>
struct check_prototypes {
constexpr static auto value {check_prototypes_helper<Prototype, Prototypes...>::value and check_prototypes<Prototypes...>::value};
};
template <typename Prototype>
struct check_prototypes<Prototype> {
constexpr static auto value {true};
};
然後我們在 argument_resolution
中增加一個靜態斷言 static_assert
即可 :
template <typename ...Prototypes>
struct argument_resolution {
static_assert(check_prototypes<Prototypes...>::value, "There are repetitive types in parameter pack Prototypes!");
public:
// ...
};
對於類似於 argument_resolution<f_arg_a, f_arg_b, f_arg_weight, f_arg_a>::create()
這種存在重複的錯誤程式碼, 編碼器就會擲出靜態斷言失敗的編碼錯誤.
5.4 程式碼的位置調整
其實到目前位置, 我們的引數決議 argument_resolution
基本完美了. 我把到目前為止的所有程式碼都整合一下 :
#include <type_traits>
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningType, typename ...RealTypes, typename ...RealTypesNext>
struct perform_real_types_next<ReturningType<RealTypes...>, RealTypesNext...> {
using type = ReturningType<RealTypes..., RealTypesNext...>;
};
template <typename Find, typename Prototype, typename ...Prototypes>
struct type_location {
constexpr static auto value {type_location<Find, Prototypes...>::value + 1};
};
template <typename Find, typename ...Prototypes>
struct type_location<Find, Find, Prototypes...> {
constexpr static auto value {0};
};
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
struct nil {};
template <typename Prototype, typename RealType>
struct value_holder {
void *value;
};
template <int I, typename RealType, typename ...RealTypes>
struct find_real_type {
using type = typename find_real_type<I - 1, RealTypes...>::type;
};
template <typename RealType, typename ...RealTypes>
struct find_real_type<0, RealType, RealTypes...> {
using type = RealType;
};
template <typename ...>
struct check_prototypes_helper;
template <typename Check, typename Prototype, typename ...Prototypes>
struct check_prototypes_helper<Check, Prototype, Prototypes...> {
constexpr static auto value {std::bool_constant<not std::is_same_v<Check, Prototype> and check_prototypes_helper<Check, Prototypes...>::value>::value};
};
template <typename Check>
struct check_prototypes_helper<Check> {
constexpr static auto value {true};
};
template <typename Prototype, typename ...Prototypes>
struct check_prototypes {
constexpr static auto value {check_prototypes_helper<Prototype, Prototypes...>::value and check_prototypes<Prototypes...>::value};
};
template <typename Prototype>
struct check_prototypes<Prototype> {
constexpr static auto value {true};
};
template <typename ...Prototypes>
struct argument_resolution {
static_assert(check_prototypes<Prototypes...>::value, "There are repetitive types in parameter pack Prototypes!");
public:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
template <typename Prototype, typename RealType>
auto set(RealType &&value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
if constexpr(std::is_pointer_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = static_cast<void *>(value);
}else if constexpr(std::is_lvalue_reference_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = &value;
}else {
try {
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
}catch(...) {
this->destroy();
throw;
}
}
return result;
}
template <typename Prototype>
auto &get() noexcept {
using real_type = typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type;
if constexpr(std::is_pointer_v<real_type>) {
return static_cast<real_type>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else if constexpr(std::is_lvalue_reference_v<real_type>) {
return *static_cast<std::remove_cvref_t<real_type> *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else {
return *static_cast<real_type *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}
}
template <typename Prototype, typename RealType>
constexpr void destroy(value_holder<Prototype, RealType> &arg) noexcept {
if constexpr(not std::is_pointer_v<RealType> and not std::is_lvalue_reference_v<RealType>) {
delete static_cast<RealType *>(arg.value);
}
}
void destroy() noexcept {
(this->destroy(static_cast<value_holder<Prototypes, RealTypes> &>(*this)), ...);
}
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
我們從頭開始重新審視這個程式碼.
perform_real_types_next
是專門為substitute
當K = 0
的偏特製化設計的, 因此可以把它放到substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...>
中, 並且設為私用成員型別. 這裡要把perform_real_types_next
的樣板參數ReturningType
和RealTypesNext
改一下, 因為和substitute
樣板參數中的ReturningType
和RealTypesNext
重名了;type_location
,substitute
和default_returning_type
都是在argument_resolution
中使用的, 因此可以把這兩個放到argument_resolution
中, 並且設為私用成員型別. 這裡要把type_location
的樣板參數Prototypes
改一下, 因為和argument_resolution
的樣板參數Prototypes
重名了;nil
和value_holder
兩個都是作為argument_resolution
中的create_returning_type
的輔助類別樣板, 因此可以把這兩個也放到argument_resolution
中, 也作為私用成員型別;find_real_type
是argument_resolution
中的create_returning_type
所使用的, 因此可以把它放到create_returning_type
中, 並且作為私用成員型別. 這裡要把find_real_type
的樣板參數RealTypes
改一下, 因為和create_returning_type
的樣板參數重名了;check_prototypes_helper
是專門輔助check_prototypes
的超函式, 因此可以把它放到非偏特製化的check_prototypes
中, 並且作為私用型別成員. 這裡要把check_prototypes_helper
的樣板參數Prototype
和Prototypes
都改一下, 因為和check_prototypes
的樣板參數重名了;check_prototypes
是輔助argument_resolution
進行型別檢查的超函式, 因此要把它放到argument_resolution
的靜態斷言static_assert
前面. 但是check_prototypes
的樣板參數Prototypes
的樣板參數和argument_resolution
的樣板參數Prototypes
重名了, 也要改一下;create_returning_type
作為argument_resolution
的成員, 是argument_resolution
的成員函式create
的回傳型別, 並沒有對外公開的需求, 因此可以把create_returning_type
作為argument_resolution
的私用成員;create_returning_type
中, 帶有參數的成員函式destroy
的作用是輔助沒有參數的成員函式destroy
, 它不需要對外公開, 因此可以把帶有參數的成員函式destroy
作為私用成員函式.
這裡提醒一下大家, 如果程式碼沒有完全偵錯, 就不要進行上述調整. 否則, 你就會看到令你崩潰的編碼錯誤...
6. 引數決議
最終, 當所有程式碼都已經完成並且所有程式碼都進行整理之後, argument_resolution
是這樣的 :
#include <type_traits>
template <typename ...Prototypes>
struct argument_resolution {
private:
template <typename Prototype, typename ...PrototypesPack>
struct check_prototypes {
private:
template <typename ...>
struct check_prototypes_helper;
template <typename Check, typename T, typename ...Ts>
struct check_prototypes_helper<Check, T, Ts...> {
constexpr static auto value {std::bool_constant<not std::is_same_v<Check, T> and check_prototypes_helper<Check, Ts...>::value>::value};
};
template <typename Check>
struct check_prototypes_helper<Check> {
constexpr static auto value {true};
};
public:
constexpr static auto value {check_prototypes_helper<Prototype, PrototypesPack...>::value and check_prototypes<PrototypesPack...>::value};
};
template <typename Prototype>
struct check_prototypes<Prototype> {
constexpr static auto value {true};
};
private:
static_assert(check_prototypes<Prototypes...>::value, "There are repetitive types in parameter pack Prototypes!");
private:
template <typename Find, typename Prototype, typename ...PrototypesPack>
struct type_location {
constexpr static auto value {type_location<Find, PrototypesPack...>::value + 1};
};
template <typename Find, typename ...PrototypesPack>
struct type_location<Find, Find, PrototypesPack...> {
constexpr static auto value {0};
};
private:
template <int, typename ...>
struct substitute;
template <int K, typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<K, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
using type = typename substitute<K - 1, Substitution, ReturningType<RealTypesPrevious..., RealType>, RealTypesNext...>::type;
};
template <typename Substitution, template <typename ...> typename ReturningType, typename ...RealTypesPrevious, typename RealType, typename ...RealTypesNext>
struct substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> {
private:
template <typename ...>
struct perform_real_types_next;
template <template <typename ...> typename ReturningTypeTemplate, typename ...RealTypes, typename ...RealTypesNextPack>
struct perform_real_types_next<ReturningTypeTemplate<RealTypes...>, RealTypesNextPack...> {
using type = ReturningTypeTemplate<RealTypes..., RealTypesNextPack...>;
};
public:
using type = typename perform_real_types_next<ReturningType<RealTypesPrevious..., Substitution>, RealTypesNext...>::type;
};
private:
template <int N, template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type {
using type = typename default_returning_type<N - 1, ReturningType, Nil, Ts..., Nil>::type;
};
template <template <typename ...> typename ReturningType, typename Nil, typename ...Ts>
struct default_returning_type<0, ReturningType, Nil, Ts...> {
using type = ReturningType<Ts...>;
};
private:
struct nil {};
template <typename Prototype, typename RealType>
struct value_holder {
void *value;
};
private:
template <typename ...RealTypes>
struct create_returning_type : value_holder<Prototypes, RealTypes>... {
private:
template <int I, typename RealType, typename ...RealTypesPack>
struct find_real_type {
using type = typename find_real_type<I - 1, RealTypesPack...>::type;
};
template <typename RealType, typename ...RealTypesPack>
struct find_real_type<0, RealType, RealTypesPack...> {
using type = RealType;
};
private:
template <typename Prototype, typename RealType>
constexpr void destroy(value_holder<Prototype, RealType> &arg) noexcept {
if constexpr(not std::is_pointer_v<RealType> and not std::is_lvalue_reference_v<RealType>) {
delete static_cast<RealType *>(arg.value);
}
}
public:
template <typename Prototype, typename RealType>
auto set(RealType &&value) {
using result_type = typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type;
result_type result {static_cast<value_holder<Prototypes, RealTypes> &>(*this).value...};
if constexpr(std::is_pointer_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = static_cast<void *>(value);
}else if constexpr(std::is_lvalue_reference_v<std::remove_cv_t<RealType>>) {
static_cast<value_holder<Prototype, RealType> &>(result).value = &value;
}else {
try {
static_cast<value_holder<Prototype, RealType> &>(result).value = new RealType(value);
}catch(...) {
this->destroy();
throw;
}
}
return result;
}
template <typename Prototype>
auto &get() noexcept {
using real_type = typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type;
if constexpr(std::is_pointer_v<real_type>) {
return static_cast<real_type>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else if constexpr(std::is_lvalue_reference_v<real_type>) {
return *static_cast<std::remove_cvref_t<real_type> *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}else {
return *static_cast<real_type *>(static_cast<value_holder<Prototype, real_type> &>(*this).value);
}
}
void destroy() noexcept {
(this->destroy(static_cast<value_holder<Prototypes, RealTypes> &>(*this)), ...);
}
};
public:
static auto create() noexcept {
return typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type {};
}
};
7. 相容性分析
對於 Code 50 中的程式碼, 我們還要進行相容性分析. 這個分析只要是兩個部分, 一個是編碼器相容性, 另外一個是 C++ 版本的相容性.
Code 50 中的實作並沒有任何未定行為或者實作定義行為, 也沒有用到任何編碼器單獨提供的特性 (C++ 標準未要求提供的特性). 因此, 只要支援 C++ 17 的編碼器都可以使用 Code 50.
由於 Code 50 中使用了 if constexpr
, 因此編碼器必須支援 C++ 17. 但是, if constexpr
可以通過函式的特製化或者實作一個仿函式 (functor) 類別樣板並且特製化來替代掉. 那麼如果不採用 if constexpr
, 對於 C++ 版本的要求可以退到 C++ 14. 如果想要退回到 C++ 11, 那麼我們需要把所有回傳型別使用 auto
佔位的函式的回傳型別改為真正的回傳型別. 像 argument_resolution
中的靜態成員函式 create
, 它的回傳型別是 typename default_returning_type<sizeof...(Prototypes), create_returning_type, nil>::type; create_returning_type
中的成員函式 set
, 其回傳型別是 typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type; create_returning_type
中的成員函式 get
, 其回傳型別為 typename find_real_type<type_location<Prototype, Prototypes...>::value, RealTypes...>::type
. 這些我們都已經寫在各個函式裡面了. 此時, 支援 C++ 11 的編碼器就可以編碼 argument_resolution
.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :
牛![:only_this:](https://jonny.vip/wp-content/themes/Grace8/includes/wp-alu/static/img/only_this.png)