學習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缺點在於代碼量會比較多,不好看。所以,能不能很簡單地打印出每一個元素呢?
Print Version1
幸運的是,有更好的解決方案,那就是使用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 Version2
你可能覺得上述邊界條件的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函數。
Print 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 )...};
}
其中的[&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)...};
Print Version4
知道上面的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());
上述繼承關系可以表示為如下結構:
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;
};
結構圖就不是繼承了,而是組合了,與上面類似:
現在來重載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("到此結束!")😄