現代C++實現多種print


學習C++的朋友會遇到這樣的問題,有char,int,double等對象,我們想把它們打印出來看看,初學者會通過cout或者傳統C語言的printf函數來打印這些對象。

例如:

int i = 1;
char c = 'f';
double d = 3.14;

//cout
cout << i << endl;
cout << c << endl;
cout << d << endl;

//printf
printf("%d\n%c\n%f", i, c, d);

傳統C中的printf 函數,雖然也能達成不定個數的形參的調用,但其並非類別安全,寫輸出格式也不方便,而且支持的是基礎類型。使用cout缺點在於代碼量會比較多,不好看。所以,能不能很簡單地打印出每一個元素呢?

幸運的是,有更好的解決方案,那就是使用C++11引入的variadic template,先來看看第一個版本的print,並介紹variadic template,代碼如下:

#include <iostream>
#include <bitset>

using namespace std;

// version1
void print1() {};

template <typename T, typename... Types>
void print1(const T& firstArg, const Types&... args)
{
    cout << firstArg << endl;
    print1(args...);
}

int main()
{
    print1(7.5, "hello", bitset<16>(377), 42);
    return 0;
}

運行結果:

7.5
hello
0000000101111001
42

Variadic Template: 是指數量不定,類型不定的模板,如上所示的print函數,可以看到接受了不同類型的參數,調用的函數就是擁有Variadic Template的函數,print(7.5, "hello", bitset<16>(377), 42)運行的時候,首先會7.5作為firstArg,剩余部分就是一包,然后在函數內部,繼續遞歸調用print函數,然后把"hello"作為firstArg, 其余的作為一包,一直遞歸直到一包中沒有數據,調用邊界條件的print(空函數)結束。

函數的...表示一個包,可以看到,用在三個地方,

  • 第一個地方是模板參數typename... ,這代表模板參數包。

  • 第二個就是函數參數類型包(Type&...), 指代函數參數類型包。

  • 第三個就是函數參數包args...,指的是函數參數包。

    另外,還可以使用sizeof...(args)得到包的長度。

總的來說,上面是一種遞歸解決方案,依次展開參數包,然后進行操作。

你可能覺得上述邊界條件的print函數有些多余,比version1更簡潔的就是下面的version2:

template < typename T , typename ... Types>
void print2 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl;
    if constexpr ( sizeof ...(args) > 0) print2 (args...) ;
}

上述函數能夠實現version1一樣的功能,通過判斷args的長度來選擇是否結束遞歸,constexpr可以確保在編譯期間去創建空的print邊界條件以及print函數。

除了遞歸,還有一種通過逗號表達式和初始化列表的方式來解開參數包。

template < typename T , typename ... Types>
void print3 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl ;
    initializer_list <T> { ( [&args] {cout << args << endl;}(), firstArg )...};
}

其中的[&args] {cout << args << endl;}()是構建了一個lambda表達式,並直接運行,沒有]{之間省略了(),所謂逗號表達式展開是指initializer_list會將( [&args] {cout << args << endl;}(), firstArg )...展開為([&args1] {cout << args1 << endl;}(), firstArg),.... ,([&argsN] {cout << argsN << endl;}(), firstArg),內部的lambda表達式只會生成臨時對象,所以最后的initializer_list變為了N個firstArg,也就是類型為T,所以initializer_list后面才會接上<T>。當然也可以將initializer_list打印出來看看:

template < typename T , typename ... Types>
void prin3_test (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl ;
    auto i_l = initializer_list <T> { ( [&args] {cout << args << endl;}(), firstArg )...};
    for (auto i : i_l)
        cout << i << endl;
}

另外, 逗號表達式可以接上多個,不限於一個:

initializer_list <T> {
            ( [&args] {cout << args << endl;}(), [&args] {cout << args << endl;}(), firstArg)...};

知道上面的initializer_list的解包方式, 還可以只使用一行實現print:

template <typename ... Types>
void print4 (const Types&... args)
{
    initializer_list <int> { ([&args] {cout << args << endl;}(), 0)...};
}

關鍵在於直接傳遞參數包 , 第一個參數不需要分開 , 如此就可以達到一行實現print的功能.

容器的Print

上述的print只能針對那些基礎類型以及重構了<<操作符的自定義對象, 對於STL中的容器, 則需要自己重載操作符, 下面給出vector的重載操作符函數(當然容器內部的對象也需要重載<<):

template <typename T>
ostream& operator << (ostream& os, const vector<T>& vec){
    os << "{ ";
    for (auto& v : vec)
        os << v << ' ';
    os << "}";
    return os;
}

重載后, 也可以使用上述的print函數了, 除了tuple容器以外, 其他容器的重載操作符與上述類似, 有些許差異.

tuple容器的print

tuple是C++11提出來的, 內部實現使用的是variadic template, 所以需要特別處理. 下面給出tuple一種基於遞歸繼承的簡潔實現:

template <typename ... Values> class mytuple1; //前向申明
template <> class mytuple1<> {}; //遞歸終止類

template <typename Head, typename ... Tail>
class mytuple<Head, Tail...> : private mytuple1<Tail ...> //遞歸繼承
{
    using inherited = mytuple1<Tail...>;
public:
    mytuple1() {}
    mytuple1(Head v, Tail... vtail) : m_head(v), inherited(vtail...) {}
    Head head() {return m_head;}
    inherited& tail() {return *this;}
protected:
    Head m_head;
};
mytuple1<int, float, string> t(41, 6.3, "nico");
print1(t1.head(), t1.tail().head(), t1.tail().tail().head());

上述繼承關系可以表示為如下結構:

1567580434140

tuple還有一種遞歸組合的實現方式, 也列出來, 有興趣的朋友也可以看看:

template <typename ... Values> class mytuple2;
template <> class mytuple2<> {};

template <typename Head, typename ... Tail>
class mytuple2<Head, Tail...>
{
    using composited = mytuple2<Tail...>;
public:
    mytuple2() {}
    mytuple2(Head v, Tail... vtail) : m_head(v), m_tail(vtail...) {}
    Head head() {return m_head;}
    composited& tail() {return m_tail;}
protected:
    Head m_head;
    composited m_tail;
};

結構圖就不是繼承了,而是組合了,與上面類似:

1567581621695

現在來重載tuple容器的操作符, 代碼如下:

template <int IDX, int MAX, typename... Args>
struct PRINT_TUPLE {
    static void print (ostream& os, const tuple<Args...>& t){
        os << get<IDX>(t) << (IDX + 1 == MAX ? "": ",");
        PRINT_TUPLE<IDX+1, MAX, Args...>::print(os, t);
    }
};

template <int MAX, typename... Args>
struct PRINT_TUPLE<MAX, MAX, Args...> {
    static void print (ostream& os, const tuple<Args...>& t){

    }
};

template <typename ... Args>
ostream& operator << (ostream& os, const tuple<Args...>& t) {
    os << "[";
    PRINT_TUPLE<0, sizeof...(Args), Args...>::print(os, t);
    return os << "]";
}

一個技巧是通過sizeof...求出參數包的長度, 然后從建立一個索引, 依次調用get函數打印元素, 直到索引等於包的長度調用遞歸結束函數, 其中PRINT_TUPLE類中的是否打印逗號也是一樣的道理.

結語

最后附上所有代碼, 以供試玩, 建議在C++17環境運行, if constexpr是C++17引入的新功能.

#include <iostream>
#include <bitset>
#include <string>
#include <vector>
#include <tuple>

using namespace std;

// version1
void print1() {};

template <typename T, typename... Types>
void print1(const T& firstArg, const Types&... args)
{
    cout << firstArg << endl;
    print1(args...);
}

// version2
template < typename T , typename ... Types>
void print2 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl;
    if constexpr ( sizeof ...(args) > 0) print2 (args...) ;
}

// version3
template < typename T , typename ... Types>
void print3 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl ;
    initializer_list <T> {
        ( [&args] {cout << args << endl;}(), firstArg)...};
}

// version4
template <typename ... Types>
void print4 (const Types&... args)
{
    initializer_list <int> { ([&args] {cout << args << endl;}(), 0)...};
}

template < typename T , typename ... Types>
void print3_test1 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl ;
    auto i_l = initializer_list <T> {
            ( [&args] {cout << args << endl;}(), firstArg)...};
    for (auto i : i_l)
        cout << i << endl;
}

template < typename T , typename ... Types>
void print3_test2 (const T& firstArg , const Types&... args)
{
    cout << firstArg << endl ;
    auto i_l = initializer_list <T> {
            ( [&args] {cout << args << endl;}(), [&args] {cout << args << endl;}(), firstArg)...};
    for (auto i : i_l)
        cout << i << endl;
}

template <typename T>
ostream& operator << (ostream& os, const vector<T>& vec){
    os << "{ ";
    for (auto& v : vec)
        os << v << ' ';
    os << "}";
    return os;
}

template <typename ... Values> class mytuple1;
template <> class mytuple1<> {};

template <typename Head, typename ... Tail>
class mytuple1<Head, Tail...> : private mytuple1<Tail ...>
{
    using inherited = mytuple1<Tail...>;
public:
    mytuple1() {}
    mytuple1(Head v, Tail... vtail) : m_head(v), inherited(vtail...) {}
    Head head() {return m_head;}
    inherited& tail() {return *this;}
protected:
    Head m_head;
};

template <typename ... Values> class mytuple2;
template <> class mytuple2<> {};

template <typename Head, typename ... Tail>
class mytuple2<Head, Tail...>
{
    using composited = mytuple2<Tail...>;
public:
    mytuple2() {}
    mytuple2(Head v, Tail... vtail) : m_head(v), m_tail(vtail...) {}
    Head head() {return m_head;}
    composited& tail() {return m_tail;}
protected:
    Head m_head;
    composited m_tail;
};

template <int IDX, int MAX, typename... Args>
struct PRINT_TUPLE {
    static void print (ostream& os, const tuple<Args...>& t){
        os << get<IDX>(t) << (IDX + 1 == MAX ? "": ",");
        PRINT_TUPLE<IDX+1, MAX, Args...>::print(os, t);
    }
};

template <int MAX, typename... Args>
struct PRINT_TUPLE<MAX, MAX, Args...> {
    static void print (ostream& os, const tuple<Args...>& t){

    }
};

template <typename ... Args>
ostream& operator << (ostream& os, const tuple<Args...>& t) {
    os << "[";
    PRINT_TUPLE<0, sizeof...(Args), Args...>::print(os, t);
    return os << "]";
}

int main()
{
    print1(7.5, "hello", bitset<16>(377), 42);
    print2(7.5, "hello", bitset<16>(377), 42);
    print3(7.5, "hello", bitset<16>(377), 42);
    print4(7.5, "hello", bitset<16>(377), 42);
    print1(vector<int> {1, 2, 3, 4});
    print2(vector<int> {1, 2, 3, 4});
    print3(vector<int> {1, 2, 3, 4});
    print4(vector<int> {1, 2, 3, 4});
    mytuple1<int, float, string> t1(41, 6.3, "nico");
    print1(t1.head(), t1.tail().head(), t1.tail().tail().head());
    mytuple2<int, float, string> t2(41, 6.3, "nico");
    print1(t2.head(), t2.tail().head(), t2.tail().tail().head());
    cout << make_tuple(41, 6.3, "nico");
    return 0;
}

Print("到此結束!")😄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM