摘要訊息 : 如何使用樣板超編程來接管編碼器的函式引數匹配工作?

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 &params) {
    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 &params) {
    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 &params) {
    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 &params) {
    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"}; 是同時適用於 f1f2 的. 我們並不希望這樣.

其實這裡還有一個問題 :

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 都將 str1str2 區分對待並且放入了不同的儲存空間中, 雖然它們內部的值是完全相同的. 但是不排除有編碼器將 str1str2 優化到同一片空間中, 導致上面的程式碼可以編碼通過.

通過上面的討論, 我們否定了使用數字和字面值字串作為影射中的鍵, 仍然需要尋找其它方案. 其實, 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 &params) {
    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_bf_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 包含了兩個成員函式 setget. 除此之外, create_returning_type 還必須負責儲存 argument_resolution 的樣板參數包 Prototypes 對應的實際型別包 RealTypes. 在 Code 9 中, 函式 fPrototypesf_arg_a, f_arg_bf_arg_weight, 這三個鍵影射的實際型別都是 float. 也就是說, Code 9 中的函式 fRealTypesfloat, floatfloat. 而實際型別我們可以放在 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();
};

有人可能注意到了成員函式 setget 的回傳型別都被設為 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, bweight 這樣的順序給定引數的值, 而是首先給定了 b 的值, 再給定 weight 的值, 最後才給定 a 的值. 如果 set 沒有回傳值, 就無法做到引數順序可以交換的要求.

我們繼續針對 Code 15 中的程式碼進行分析. 已經知道的是, 無論引數給定的順序是什麼樣的, 變數 args 的型別必定是 create_returning_type<float, float, float>. 那麼, 現在的問題就是 temp1, temp2temp3 的型別是什麼.

當我們呼叫函式 f_args::create 時, 我們只給定了原型型別 f_arg_a, f_arg_bf_arg_weight. 這個時候, 這些原型型別要影射到的實際型別及其值都沒有給出, 因此這個時候我們還不知道原型型別應該影射到的實際型別是什麼. 由於 create_returning_type 是一個可變參數的類別樣板, 因此一種解決方案是讓 create 直接回傳 create_returning_type<>, 然後接下來向引數列表中不斷添加原型型別引數即可. 這對函式 f 來說確實可行. 因為最終 f_arg_a, f_arg_bf_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> {}. 為此, 我們必須把類別樣板名稱和類別樣板參數分開來 :

Figure 1. 向引數列表中添加新的引數

我們曾在《【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 的樣板引數列表中就會有 NNil.

有了 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 個實際型別. 那麼替換的大致思路就可以這樣來描述 :

  1. 當原型型別 Prototype 和要替換的實際型別 RealType 給定時, 設原型型別包 Prototypes 和實際型別包 RealTypes 中有 n 個型別引數;
  2. 找到 PrototypePrototypes 中的位置, 不妨記為 k;
  3. 記實際型別包 RealTypes 中前 k - 1 個引數組成的引數包為 RealTypesPrevious, 原型型別包中後 n - k - 2 個引數組成的引數包為 RealTypesNext;
  4. 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_locationperform_real_types_next. type_location 的作用是定位 PrototypePrototypes 中的位置並且回傳. 超函式 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_locationperform_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_locationsubstitute 移動到 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;
};

現在, 即使 RealTypenil, 那麼 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>. 那麼 setreturn 陳述式的寫法應該是 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), 這樣的寫法應該對 weight0.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, 我們要做的是

  1. 找到成員函式 set 的樣板引數 Prototype 在型別引數包 Prototypes 中的位置, 不妨記為 k;
  2. values 中的前 k - 1 個引數放入 return 陳述式的初始化列表中展開;
  3. 將函式接收的引數 value 放到初始化列表的最後;
  4. values 中的後 n - k - 2 個引數放入初始化列表的 value 之後, 並且展開.

第一步我們已經在前面實作了 type_location, 這裡可以繼續使用它. 對於第二步和第四步, 我們要在 create_returning_type 中引入一個新的函式 extract, 用於獲取第 ivalue_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 陳述式中, RealTypePrototype 要影射的實際型別. 但是, 這個 RealType 是我們要在 RealTypes 中搜尋的. 而 RealTypeRealTypes 中位置取決於 PrototypePrototypes 中的位置. 為此, 我們還要實作一個超函式 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:
    // ...
};

然而, 我們在 setreturn 陳述式中遇到了問題. 我們先把 setreturn 陳述式的虛擬碼寫出來 :

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, RealTypesPreviousRealTypesNext 怎麼獲得? 我們在上面就已經說過 C++ 不支援超函式直接回傳一個型別引數包. 因此, 我們沒有辦法在 argument_resolutioncreate_returning_type 中獲得這些引數包.

轉換一下思路, 我們也可以這樣去做 :

  1. 找到成員函式 set 的樣板引數 Prototype 在型別引數包 Prototypes 中的位置, 不妨記為 k;
  2. 使用 set 正確的回傳型別 typename substitute<type_location<Prototype, Prototypes...>::value, RealType, create_returning_type<>, RealTypes...>::type 宣告一個變數 result, 把 values 展開到 result 的初始化列表中;
  3. result 的參考轉換為 value_holder<Prototype, RealType>, 並把 value_holder<Prototype, RealType>::value 置換為給定的值
  4. 回傳 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 32set 就不再會有編碼錯誤. 如果要用到 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 &params) {
    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 &&params) {
    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 &&params) 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 &&params) 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 運算子配置另一塊記憶體空間. 為此, 在成員函式 setget 中, 我們要對指標和參考這兩個型別特別關注一下 :

#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 是專門為 substituteK = 0 的偏特製化設計的, 因此可以把它放到 substitute<0, Substitution, ReturningType<RealTypesPrevious...>, RealType, RealTypesNext...> 中, 並且設為私用成員型別. 這裡要把 perform_real_types_next 的樣板參數 ReturningTypeRealTypesNext 改一下, 因為和 substitute 樣板參數中的 ReturningTypeRealTypesNext重名了;
  • type_location, substitutedefault_returning_type 都是在 argument_resolution 中使用的, 因此可以把這兩個放到 argument_resolution 中, 並且設為私用成員型別. 這裡要把 type_location 的樣板參數 Prototypes 改一下, 因為和 argument_resolution 的樣板參數 Prototypes 重名了;
  • nilvalue_holder 兩個都是作為 argument_resolution 中的 create_returning_type 的輔助類別樣板, 因此可以把這兩個也放到 argument_resolution 中, 也作為私用成員型別;
  • find_real_typeargument_resolution 中的 create_returning_type 所使用的, 因此可以把它放到 create_returning_type 中, 並且作為私用成員型別. 這裡要把 find_real_type 的樣板參數 RealTypes 改一下, 因為和 create_returning_type 的樣板參數重名了;
  • check_prototypes_helper 是專門輔助 check_prototypes 的超函式, 因此可以把它放到非偏特製化的 check_prototypes 中, 並且作為私用型別成員. 這裡要把 check_prototypes_helper 的樣板參數 PrototypePrototypes 都改一下, 因為和 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.