動機

一切的開始,只是想要個reversed而已,就順便把tmp補完

tips when using template

別用float

因為浮點數的結果不一致

func<1/3.f> (); 
func<2/6.f> ();

原本函數的宣告與實作會分成header與source code,但是template的code不能放在source code中。

因為compiler要先看有template的code,之後才能填充type,在生出真正的code,專業一點叫 實體化(instantiation)

some tools

  • constexpr
const int SOME_INT_WILL_NOT_BE_MODIFIED = some_func();

constexpr int INT_IS_COMPUTED_IN_COMPILCATION = 1 + 5;
// #define MACRO_INT 1 + 5
// 這只是會展開成 1+5但不會算成6,但constexpr會,同時template可以拿到他

constexpr int foo(int i)
{
    return i + 5;
}

int main()
{
    int i = 10;
    std::array<int, foo(5)> arr; // OK
    // as if 5 + 5, and computed in complication
    
    foo(i); // Call is Ok
    // This is usual function
    
    // But...
    std::array<int, foo(i)> arr1; // Error
    // Compiler cant know what the value is foo(i), cuz i is determinated in run-time
   
}
  • using
typedef int A;
// <=>
int typedef A;
// <=>
using A = int;
  • decltype
struct A { double x; };
const A* a;
 
decltype(a->x) y;       // type of y is double (declared type)
decltype((a->x)) z = y; // type of z is const double& (lvalue expression)
  • typename 把typename後面的東西當成type來看

  • function only with types 只要type對了就好

void f(int,char[0]) {
   std::cout << "only types\n";
}

char wtf[0];
f(10,NULL), f(123,wtf);
  • Template specialization template的參數先填掉
template<int n>
struct fact {
        enum { val = n*fact<n-1>::val };
};

template<>
struct fact<0> { 
        enum { val = 1 };
};
  • 0是誰? 0可以當成
  1. 整數
  2. null ptr
  3. bool的false
  • compiler自己推type
template<typename T>
T id(T x) { return x; }

int main(int argc, char *argv[]) {
    std::cout << id(123) << ',' << id("123") << '\n';
}

basic (很像functional programming)

fact

template<int n>
struct fact {
        enum { val = n*fact<n-1>::val };
};

template<>
struct fact<0> {
        enum { val = 1 };
};

int main() {
        //cout << Min<int, 1,2,3,4>::val << endl; //下面的
        cout << fact<10>::val << endl;
}

Min

目標: Min<int, 1,2,3,4>::val拿到1

  1. 宣告
template<typename T, T ...> // T...是type,像是va_list
struct Min;
  1. case1: 如果有不只一個數字
template<typename T, T v,T ... args> // typename就是說這是一個type
struct Min<T,v,args> { // 要填<...>符合原本的宣告 // destruct pack
        //constexpr static auto val = min(v, hi<T, args...>::val);
        enum { val = (v < (Min<T, args...>::val) ? v : (Min<T, args...>::val) };
        // Min<T, args...>::val 就是遞迴剩下的部分
  1. case2: 如果只有一個數字
template<typename T, T v>
struct Min<T,v> { enum { val = v }; };

turing-complete

template是turing-complete,可以用template來實做lambda calculus 也不意外,template的參數帶入就是lambda calculus的beta reduction

詳細的說明blog

code

Solution to Exercise

template <typename A, typename B>
struct add {};

template <typename A_s, typename B>
struct add<Succ<A_s>, B> {
  enum { value = 1+add<A_s,B>::value };
};

template <typename B>
struct add<Zero, B> {
  enum { value = B::value };
};
template <typename lhs, typename rhs>
struct Add {};

template <typename Lhs, typename Rhs, typename Env>
struct Eval<Add<Lhs, Rhs> , Env> {
  typename Apply<Add<typename Eval<Lhs,Env> :: result ,
                    typename Eval<Rhs,Env> :: result >, Zero> :: result 
           typedef result ;
} ;

template <typename N, typename M>
struct Apply<Add<N, M>, Zero> {
  add<N,M> typedef result ;
} ;

trait

可以理解成type的interface,描述這個type會在編譯期這個type有什麼屬性

那怎麼描述(寫下)有這些屬性?

  1. 寫到定義中 (這比較像type屬性的getter)
template <typename Iterator>
struct iterator_traits {

    using value_type = typename Iterator::value_type;

    using pointer = typename Iterator::pointer;

    using const_pointer = typename Iterator::const_pointer;

    using reference = typename Iterator::reference;

    using const_reference = typename Iterator::const_reference;

    using rvalue_reference = typename Iterator::rvalue_reference;

    using iterator_category = typename Iterator::iterator_category;

};

template <typename T>

class vector_iterator {

public:

    using value_type = T;

    using pointer = T *;

    using const_pointer = const T *;

    using reference = T &;

    using const_reference = const T &;

    using rvalue_reference = T &&;

    using iterator_category = std::random_access_iterator_tag;

    //...

};
  1. 特化 (這比較像描述一個type)
template <typename Iterator>
struct iterator_traits {};

template <typename T>
struct iterator_traits<vector_iterator<T>> {

    using value_type = T;

    using pointer = T*;

    using const_pointer = const T *;

    using reference = T &;

    using const_reference = const T &;

    using rvalue_reference = const T &;

    using iterator_category = std::random_access_iterator_tag;

};

SFINAE

就是compiler怎麼淘汰候選人的過程

整個過程是有同樣名字的struct或是function都選出來,分別一個一個帶入,保留沒出事的,最後只剩下一個就用他

最頭痛的是有同樣名字的struct或是function都選出來,分別一個一個帶入

這樣子變成要確保沒每個template涵蓋到的部分不能與其他的宣告重疊,故個寫起來的感覺是

if n == 1:
        return 'hi'
if n != 1:
        return 'wow' 

不能像

if n == 1:
        return 'hi'
else:
        return 'wow'

簡而言之, 我們沒有failover的宣告,每個都要描寫出來,n為一,n不為一, 不能是 n為一,其他

要怎麼讓compiler淘汰候選人?

  • template列表 (template那一行)
  • 特殊化列表 (function或是struct旁邊的角括號)
  • 參數列表 (function的參數列表)

搞出奇怪的東西,像存取不存在的屬性、長度為零的array等等

詳細的case見這裡 還有可以看看type_traits.hpp怎麼實作那些神奇的traits

用enable_if試試看

enable_if會吃一個條件式,如果true吐一個type,如果false就會整個壞掉

下面用enable_if來寫一個判斷int的函數

f_if_integral_weak

f_if_integral_weak沒辦法推出參數在此的type,故報錯

如果要過,就要自己補type

#include <iostream>
#include <type_traits>

template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
        using type = T;
};


template <typename T>
struct is_int {
        constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
        constexpr static inline auto value {true};
};

template <typename T>
void f_if_integral_weak(typename enable_if<is_int<T>::value, T>::type) {
        std::cout << "a: is int\n";
}

int main(int argc, char *argv[]) {
        f_if_integral_weak<int>(10);
        //f_if_integral_weak(0); // 0是誰?
        //f_if_integral_weak(0.0); // 沒有type
        f_if_integral_weak<int>(0);
}

f_if_integral_strong

手動打type在此蠻沒意義的,所以試著讓compiler自己推,原本的enable_if用預設參數補

#include <iostream>
#include <type_traits>

template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
        using type = T;
};


template <typename T>
struct is_int {
        constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
        constexpr static inline auto value {true};
};

template <typename T>
void f_if_integral_strong(T, typename enable_if<is_int<T>::value, T>::type* = 0) {
        std::cout << "b: is int\n";
}

int main(int argc, char *argv[]) {
        f_if_integral_strong(0);
        //f_if_integral_strong(0.0); // 沒有type阿
}

f_if_integral_stronger

為什麼不用typename enable_if<!is_int<T>::value, T>::type來加不是int的case? 因為typename enable_if<!is_int<T>::value, T>::typetypename enable_if<is_int<T>::value, T>::type最後推出來的type是一樣的 這樣compiler分不出來!!

但可以把另一個case放到別的地方,來區分兩個case

#include <iostream>
#include <type_traits>

template <bool, typename>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
        using type = T;
};


template <typename T>
struct is_int {
        constexpr static inline auto value {false};
};
template <>
struct is_int<int> {
        constexpr static inline auto value {true};
};

template <typename T, typename = typename enable_if<is_int<T>::value, T*>::type>
void f_if_integral_stronger(T) {
        std::cout << "c: is int\n";
}

template <typename T>
void f_if_integral_stronger(T, typename enable_if<!is_int<T>::value, T>::type* = 0) {
        std::cout << "c: not int\n";
}

int main(int argc, char *argv[]) {
        f_if_integral_stronger(0.0);
        f_if_integral_stronger(0);
}

其實可以簡單一點

用特別化就好

template <typename T>
void f(T) { std::cout << "not int\n"; }

template <>
void f<int>(int) { std::cout << "is int\n"; }

int main(int argc, char *argv[]) {
    f(1);
    f(1.1);
    f("1234");
}

and_in_template

這裡模擬&&,但要怎麼判斷true與false?

有沒有壞掉!!

#include <iostream>
#include <type_traits>

template <typename ...>
struct and_in_template {
        using type = void;
};

template <typename S>
void has_A_B(S,typename and_in_template<typename S::A, typename S::B>::type* = 0) {
        std::cout << "get it\n";
}

struct Ans {
        struct A{};
        struct B{};
};

int main(int argc, char *argv[]) {
        Ans a;
        has_A_B(a);
}

concept

C++20的東西,可以對type的要求寫下來成為concept

之後就可以用require搭配其他concept一起用,

  1. Conjunction (AND)
  2. Disjunction (OR)

有一個名詞是Atomic constraint,其實就是會回傳bool的type_trait,只是原本type_trait要自己訂欄位放結果,但concept可以直接return

template<typename T>
struct S {
    constexpr operator bool() const { return true; }
};
 
template<typename T>
    requires (S<T>{})
void f(T);

所以concept其實就是使用SFINAE的template程式碼的語法糖?

template<typename T>
concept Hashable = requires(T a) {
    a.keep_walking
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
 
struct meow {};
 
template<Hashable T>
void f(T); // constrained C++20 function template
 
// Alternative ways to apply the same constraint:
// template<typename T>
//    requires Hashable<T>
// void f(T); 
// 
// template<typename T>
// void f(T) requires Hashable<T>; 
 
int main() {
  f("abc"); // OK, std::string satisfies Hashable
  f(meow{}); // Error: meow does not satisfy Hashable
}

Ref

C++ Core Guidelines: Rules for Template Metaprogramming 【C++ Template Meta-Programming】認識樣板超編程 (TMP)

decltype

【C++ Template Meta-Programming】參數列表

C++ templates: Creating a compile-time higher-order meta-programming language

Why can’t I use float value as a template parameter? Why can templates only be implemented in the header file? C++ template function compiles in header but not implementation

细说 C++ Traits Classes 【C++ Template Meta-Programming】Traits 技巧 C++ TUTORIAL - TRAITS : A TEMPLATE SPECIALIZATION - 2020

SFINAE SFINAE(替換失敗不是錯誤) 認識SFINAE Substitution failure is not an error 【C++ Template Meta-Programming】函式多載與 SFINAE 初步 【C++ Template Meta-Programming】SFINAE 【C++ Template Meta-Programming】到處 SFINAE

说说 C++ 的 Concept Constraints and concepts (since C++20)