程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> (原創)舌尖上的c++--相逢

(原創)舌尖上的c++--相逢

編輯:C++入門知識

引子

  前些時候,我在群裡出了一道題目:將變參的類型連接在一起作為字符串並返回出來,要求只用函數實現,不能借助於結構體實現。用結構體來實現比較簡單:

template<typename... Args> struct Connect;

template< typename First, typename... Rest>
struct Connect<First, Rest...>
{
    static string GetName()
    {
        return typeid(First).name()  + string(" ") +  Connect<Rest...>::GetName();
    }
};

template<typename Last> struct Connect<Last>
{
    static string GetName()
    {
        return typeid(Last).name();
    }
};

測試代碼:

auto str = Connect<int, double>::GetName();
//str為int double

如果改成函數的話,試著這樣寫:

template<typename T>
string GetNameofTypes()
{
    return typeid(T).name();
}

template<typename T, typename... Rest>
string GetNameofTypes()
{
    string str += GetNameofTypes<T>() + " " + GetNameofTypes<Rest...>();

    return str;
}

很遺憾,這樣是編譯不過的,因為編譯器不知道選擇哪個。有兩個方法可以解決這個問題,這裡主要來介紹通過逗號表達式來解決這個問題。

逗號表達式和變參的相逢

  前段時間播放的舌尖上的中國第二季中有一集為相逢,當南北不同風味的普通食材放到一起時,立刻化腐朽為神奇變成難得的美味了,這正是食材相逢組合而成的天作之合。那麼在c++中,不同的特性相逢在一起又是什麼滋味呢?一定很奇妙。下面來看看舌尖上的c++中兩個普通食材吧。

食材一:逗號表達式

  我們知道逗號表達式會按順序執行逗號前面的表達式,比如:
d = (a = b, c);
  這個表達式會按順序執行的:a先會被賦值b,接著d會被賦值c。

食材二:可變參數模板

c++11的可變參數模板增強了模板功能,在c++11之前,類模板和函數模板只能含有固定數量的模板參數,現在c++11中的新特性可變參數模板允許模板定義中包含0到任意個模板參數。可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,可變參數模板聲明時需要在typename或class後面帶上省略號“...”。
省略號(...)的作用有兩個

  • 聲明一個參數包,這個參數包中可以包含0到任意個模板參數;
  • 在模板定義的右邊,可以將參數包展開成一個一個獨立的參數。

逗號表達式和變參的相逢

  來看看逗號表達式和變參的相逢會產生什麼奇妙的效果。

template <class T>
void printarg(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(printarg(args), 0)...};
  cout<<sizeof(arr)<<endl; }

測試代碼:

expand(1,2,3,4);
//將輸出1 2 3 4

  上面的例子將分別打印出1,2,3,4,將可變參數模板就地展開了。是的,是通過一個沒有引用的數組來就地展開的,看到這種寫法不要驚訝,這就是它們相逢產生的神奇效果,獨有的味道。
  我們來看看這種奇妙的效果是如何產生的:

  我們知道逗號表達式會按順序執行逗號前面的表達式,比如:
  d = (a = b, c);
  這個表達式會按順序執行的:a先會被賦值b,接著d會被賦值c。
  expand函數中的逗號表達式:(printarg(args), 0),也是按照這個執行順序,是先執行printarg(args),再得到逗號表達式的結果0。同時還用到了c++11的另外一個特性:初始化列表,通過初始化列表來初始化一個變長數組, {(printarg(args), 0)...}將會展開成((printarg(arg1),0), (printarg(arg2),0),printarg(arg3),0), etc... );最終會創建一個元素值都為0的數組int arr[sizeof...(Args)],由於是逗號表達式,在創建數組的過程中會先執行逗號表達式前面的部分printarg(args)打印出參數,也就是說在構造int數組的過程中就將參數包展開了,這個數組的目的純粹是為了在數組構造的過程展開參數包。

  再回過頭來說說本文開頭提到的那個問題,通過函數將變參類型作為字符串返回出來。通過逗號表達式可以很輕松完成這個任務:

template<typename T>
string GetName()
{
    return typeid(T).name();
}

template<typename... Rest>
string GetNameofTypes()
{
    string str;
    int arr[] = { (str +=" "+ GetName<Rest>(), 0)... };
   cout<<sizeof(arr)<<endl; return str; } //測試代碼: auto s = GetNameofTypes<int, double, char>(); //s將為 int double char

  嘗了上面的逗號表達式和可變參數模板的相逢產生奇妙的味道,一定還在回味之中,意猶未盡吧。我還沒說其實還有一樣食材沒說呢,如果將逗號表達式和這種食材相逢在一起便又是另外一種獨特的味道了。再來看看另外一種食材吧。

食材三:decltype表達式類型推斷

  C++11新增了decltype關鍵字,用來在編譯時推導出一個表達式的類型。它的語法格式如下:
decltype(exp)
  其中exp表示一個表達式(expression)。
它可以用來推導表達式標示符和表達式的類型,比如:

const int bar();
int i;
struct A { double x; };
const A* a = new A();
decltype(bar()) x2; // 類型為const int
decltype(i) x3; // 類型為int
decltype(a->x) x4; // 類型為double
decltype((a->x)) x5; // 類型為const double&

  我們一般用decltype來推斷函數的返回類型,和auto結合起來,組成一種返回值類型後置的語法,比如下面的例子:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
    return t + u;
}

  為什麼要返回類型後置呢?因為返回類型要依賴於模板參數,如果將decltype(t + u)放到函數的前面則無法編譯通過,因為在定義函數返回值的時候,模板參數變量都還不存在呢。所以就借助auto來將返回類型占位住,等decltype推導之後再給auto初始化,從而獲取了函數的返回值。

逗號表達式和decltype的相逢

  來看看逗號表達式和decltype的相逢又會產生什麼奇妙的效果。比如有這樣一個需求,我需要在編譯期判斷某個類是否存在void Reserve(int i)函數。

template<typename T>
struct Has_Reserve_Method
{
private:
    typedef std::true_type True;
    typedef std::false_type False;

    template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve

(0), True());

    template<typename> static False Check(...);

public:
    enum
    {
        value = std::is_same<decltype(Check<T>(0)), True>::value
    };
};

struct AA
{
    void Reserve(int i)
    {
        cout << "ok" << endl;
    }
};

測試代碼:
if (Has_Reserve_Method<AA>::value)
        cout << "ok" << endl;

//將輸出OK

  這裡利用了SFINAE特性,它的全稱是Substitution failure is not an error,匹配失敗並不是錯誤,意思是用函數模板匹配規則來判斷類型的某個屬性是否存在,也就是說SFINAE可以作為一種編譯期的不完整內省方法。

template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve(0), True());
template<typename> static False Check(...);

  這兩行代碼是關鍵,當Check匹配不上時,返回False;當匹配上之後,通過逗號表達式返回True,這樣外面就可以根據True和False來檢查是否存在該函數了。

  怎麼樣這道菜的味道也相當好吧。其實還有很多c++特性的相逢產生的奇妙味道我們還沒有發現,正等待著我們去發現呢。

後記

  由於typeid可能會丟失一些類型信息,要獲取准確的類型名稱還需要做一些專門的處理,這裡為了簡單起見,忽略了typeid獲取類型名可能不准確的影響。另外,有童孩說逗號表達式來展開變參的代碼可讀性很差,這裡我也不推薦大家在實際的代碼中也這樣寫,還是老老實實的用結構體來展開變參吧,其實通過函數來展開變參(不用逗號表達式)還有種寫法:

string GetNameofMsgType()
{
    return "";
}

template <typename First>
string GetNameofMsgType()
{
    return std::string(typeid(First).name());
}

template <typename First, typename Second, typename... Args>
string GetNameofMsgType()
{
    return GetNameofMsgType<First>() + " " + GetNameofMsgType<Second, Args...>();
}

 

如果你覺得這篇文章對你有用,可以點一下推薦,謝謝。

c++11 boost技術交流群:296561497,歡迎大家來交流技術。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved