鳴謝
感謝PiggyXP兄的雄文《手把手叫你玩轉網絡編程系列之三——完成端口(Completion Port)詳解》提供的思路
C++11標準提出來有些年頭了,十一放假沒事研究了一下IOCP,想著能不能用C++11實現一個高性能的服務器。當然,目前有許多十分成熟的C++網絡庫,比如ACE,asio等等。但是如果想深入了解其本質,在Windows平臺下就必須了解Socket結合IOCP的使用原理。
本文盡可能把筆者在使用C++11實現IOCP服務器的過程中遇到的困難和問題展現給大家,讓大家學習起來少走些彎路。由于代碼比較底層,所以有些細節(jié)希望大家在看本文和代碼的時候能夠揣摩和理解。本文假定讀者總體把握了PiggyXP原文的相關內容并具有相當的Window編程的相關知識(熟悉WinSock2庫基本函數的使用,Windows多線程的基本概念等)、C++11/03編程基礎(STL,仿函數等)。
在每一節(jié)標題后都有箭頭指向目錄,文檔某些位置可能會有返回箭頭(返回到可能你在閱讀的地方),希望能幫助大家更好的理解本文。
本文代碼遵循Apache License 2.0協議,歡迎各位大神拍磚。分享帶來進步,如需轉載請標明作者和出處,謝謝!
溫馨提示:由于筆者水平有限,雖經過仔細調試,但本文代碼仍然可能存在筆者未知的Bug或者性能缺陷。請大家發(fā)現問題后能夠及時聯系我,讓我們共同進步。
軟件/系統(tǒng) | 版本 |
---|---|
操作系統(tǒng) | Windows 10 v1607 x64 |
IDE/編譯器 | Visual Studio 2015/CL 19 |
Win SDK | 10.0.10240 |
編程語言 | C++11 |
本節(jié)參考文獻
Nasarre C, Richter J. Windows? via C/C++[M]. Pearson Education, 2007: 291-316.
在生活中,異步的概念是很常見的。比如你洗衣服時突然女朋友(程序員有女朋友?)來了,你從洗衣間出去招待,而洗衣機則按照你的指令繼續(xù)在工作。當你招呼完女朋友回到洗衣間的時候,衣服已經洗好了。也就是在女朋友來的時間點,你與洗衣機分離,它按照你的指令在完成工作,而你卻可以處理其他更需要處理的事情。當你處理完回來后,洗衣機可能早已經完成了它的工作,你只需要將衣服取出晾起來就可以了。而同步就是你家沒有洗衣機,當女朋友來的時候要么中斷洗衣服去招待女朋友,要么讓女朋友等待自己把衣服洗完,一件事情只能在另一件事情之后發(fā)生。這樣,大家就能明顯看出來有臺洗衣機的好處了。
不過如何知道衣服洗完了呢?Windows牌洗衣機給我們提供了這么四種方式:
方式 | 解釋 | 相關技術 |
---|---|---|
LED燈 | 洗完一件衣服就亮燈,但只有一個燈,其他人可以幫忙處理 | 觸發(fā)設備內核對象 |
高級LED燈 | 洗完一件衣服就亮燈,可以有多個燈,其他人可以幫忙處理 | 觸發(fā)事件內核對象 |
發(fā)送短信 | 洗完一件衣服就發(fā)送一條短信,有一個短信列表,但只有你能夠處理 | 可提醒IO(APC) |
群發(fā)短信 | 洗完一件衣服就發(fā)送一條短信,有一個短信列表,其他人可以幫忙處理 | IO完成端口(IOCP) |
這樣,大家就很明白IOCP的好處了:不需要去時刻看著燈亮不亮;短信到了可以去處理也可以不去處理;不僅你能處理,還有家人也能幫你處理。
觸發(fā)設備內核對象、觸發(fā)事件內核對象和可提醒IO就不展開討論了,有興趣的朋友可以查閱本節(jié)列出的參考文獻,下面進入正題。
這一小節(jié)可能比較難,希望大家能夠耐心看下去,因為要真正掌握IOCP就必須弄清楚它內在的原理。先給出IOCP的狀態(tài)機,如圖1所示:
下面給出圖中各組件的相關說明:
組件 | 簡要解釋 |
---|---|
等待隊列 | 當線程池中的某線程在等待IO操作時(調用GetQueuedCompletionStatus 函數),IOCP將線程加入等待隊列。IOCP在IO操作完成后將返回結果加入完成隊列,由等待隊列中的最后一個加入的線程處理。 |
已釋放列表 | 當等待的線程處理完IO操作后或是從暫停狀態(tài)被喚醒都會加入此列表。 當線程再次調用 GetQueuedCompletionStatus 函數將使自己再次加入等待隊列;將自身掛起將加入已暫停列表。 |
已暫停列表 | 當已釋放列表中的線程掛起時將加入已暫停列表;當掛起線程被激活時線程加入已釋放列表。 |
完成隊列 | IOCP完成指定IO操作后將執(zhí)行結果插入完成隊列。這個隊列時先進先出的。 |
IOCP設備列表 | 即要進行異步IO操作的設備列表(可以是文件,也可以是套接字),所有的IO操作都圍繞這些設備進行。 |
這樣,整個IOCP服務器創(chuàng)建的流程就很明了了:?
- 創(chuàng)建一個新的完成端口,處理所有的IO請求。
- 創(chuàng)建一個線程池,此時線程處于
已釋放列表
。- 創(chuàng)建一個
Socket
并將其綁定在創(chuàng)建的完成端口上,作為IO操作的實體。利用這個套接字進行Listen
操作,并向第1步創(chuàng)建的完成端口中投遞Accept
消息,將第2步創(chuàng)建線程置于等待隊列
中等待客戶端連接。- 當客戶端連接后,IOCP將在
IO完成隊列
插入Accept
,等待隊列
中的線程將得到Accept
,并創(chuàng)建新的Socket
作為與客戶端通信的套接字,并將其綁定在第1步創(chuàng)建好的完成端口上。- 此后,無論是
Recv
,Send
都照此步驟進行即可。
這里有幾個細節(jié)需要注意:
1. 最合適的線程數應當是多于處理器核心數的
多線程優(yōu)化理論告誡我們,為了避免ring0
與ring3
之間的上下文切換,我們應當將線程數設置為處理器核數。但是微軟在設計IOCP的時候想到了這樣一個問題:考慮到線程掛起,如果按照理論值設置線程數,將有可能出現實際工作線程數小于CPU所能接受的最大工作線程數,這樣就無法有效發(fā)揮多線程的優(yōu)勢。因此,最理想的線程數量應當多于處理器核心數的,經驗值為兩倍核心數。
2. 等待隊列是后入先出的
之所以這樣設計也是出于性能調優(yōu)的考慮。當某線程處理完某批IO數據后重新加入等待隊列,由于LIFO機制,當完成隊列中又存在有新的IO數據時,該線程將會優(yōu)先處理數據。這樣可能會導致某些線程一直處于等待狀態(tài),這樣Windows就可以將其換出內存節(jié)約空間。
3. 投遞
所謂投遞其實就是利用AcceptEx
,WSARecv
和WSASend
等函數在IO完成端口中進行異步操作。形象來說就是你向洗衣機輸入參數的過程,后續(xù)工作由洗衣機(WinSock2)完成。
本節(jié)參考文獻
Microsoft. I/O Completion Ports[EB/OL]. https://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx
Microsoft. Windows Sockets 2[EB/OL]. https://msdn.microsoft.com/en-us/library/windows/desktop/ms740673(v=vs.85).aspx
Russinovich M E, Solomon D A, Ionescu A. Windows internals[M]. Pearson Education, 2012: 56-58.
關于常規(guī)的IO完成端口API主要有以下三個:
創(chuàng)建和關聯IO完成端口函數CreateIoCompletionPort
,該函數在創(chuàng)建完成端口和關聯設備(文件設備,套接字等)時使用。
HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads );
獲取完成隊列狀態(tài)函數GetQueuedCompletionStatus
,該函數在線程池線程函數中使用。?
BOOL WINAPI GetQueuedCompletionStatus( _In_ HANDLE CompletionPort, _Out_ LPDWORD lpNumberOfBytesTransferred, _Out_ PULONG_PTR lpCompletionKey, _Out_ LPOVERLAPPED * lpOverlapped, _In_ DWORD dwMilliseconds );
在完成隊列中插入消息函數PostQueuedCompletionStatus
,該函數在給線程傳遞退出參數時使用。?
BOOL WINAPI PostQueuedCompletionStatus( _In_ HANDLE CompletionPort, _In_ DWORD dwNumberOfBytesTransferred, _In_ ULONG_PTR dwCompletionKey, _In_opt_ LPOVERLAPPED lpOverlapped );
以上函數的詳細用法在參考文獻及piggyXP的文章中可以找到,故不再贅述。
在編程過程中主要考慮以下幾個問題:
1.
CreateIoCompletionPort
函數的設計問題
按照設計模式最基礎的原則即單一職責原則,這個函數設計是存在缺陷的。事實上很多Windows API都或多或少存在此問題,筆者印象比較深刻的是NetBIOS的系列函數。理想的設計是自己再抽象兩個函數,即創(chuàng)建完成端口一個函數,綁定完成端口一個函數。可以這樣設計:
創(chuàng)建一個新的完成端口函數CreateNewIoCompletionPort
,該函數在初始化時使用。
/*** Create completion port*/inline auto CreateNewIoCompletionPort( DWORD NumberOfConcurrentThreads = 0 ) { return CreateIoCompletionPort( INVALID_HANDLE_VALUE, nullptr, 0, NumberOfConcurrentThreads );}
設備與完成端口綁定函數AssociateDeviceWithCompletionPort
,該函數在完成端口建立后與IO設備綁定時使用。?
/*** Associate device with completion port*/inline auto AssociateDeviceWithCompletionPort( HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey ) { return CreateIoCompletionPort( hDevice, hCompPort, dwCompKey, 0 ) == hCompPort;}
2. 線程池線程退出問題
由于在程序中使用了線程池,對于每一個線程而言如何不留痕跡地結束是一個很有技巧性的問題。一種優(yōu)雅的方法是使用PostQueuedCompletionStatus
函數給完成端口傳遞退出完成鍵(CompletionKey)。由于線程只有可能在等待隊列、已釋放列表和已暫停列表中,且設計線程函數時均會循環(huán)調用GetQueuedCompletionStatus
函數,因此最終所有線程都會轉移到等待隊列中去。
有的讀者會考慮到等待隊列的LIFO特性,其實只要我們設計線程函數時首先判斷傳入的完成鍵是否為退出的特定信號,檢測到自行退出即可。我們在主線程退出時在完成端口中傳入創(chuàng)建線程數量個推出信號,由于是完成隊列是順序存取,只要線程函數設計合理,可以保證每一個線程函數都可以收到退出消息。不會發(fā)生piggyXP考慮的收不到信息的情況。
更深入的討論高級程序員參考
筆者深入分析了GetQueuedCompletionStatus
函數(由Kernel32.dll
轉發(fā),在KernelBase.dll
中實現),發(fā)現其內部準備好各項參數后調用了NtRemoveIoCompletion
函數(由ntdll.dll
轉發(fā),在內核ntoskrnl.exe
中實現)。這樣就很明白了,其實就是在完成隊列中取出一個數據。繼續(xù)對
NtRemoveIoCompletion
函數進行分析,發(fā)現在內部調用了IoRemoveIoCompletion
,繼續(xù)深究下去發(fā)現其主要功能調用了KeRemoveQueueEx
函數,而在該函數內部進行了無鎖同步:
if ( _interlockedbittestandset( ... ) ) { do { do KeYieldProcessorEx( ... ); while ( ... ); } while ( _interlockedbittestandset( ... ) );}
這樣就能保證APC交付時,只有一個線程可以訪問到完成隊列。因此,只要在設計過程中一次只取出一個完成的數據,就不會出現問題。當然,如果想更高效的處理數據(比如調用
GetQueuedCompletionStatusEx
)又想通過PostQueuedCompletionStatus
方式退出的話,就可能需要特殊處理。比如像piggyXP一樣設計一個信號量,或者接收到退出信號后在退出之前向完成隊列中再Post一個退出信號等等。如果想要更加深入的了解其中的運作機理,大家可以去看看WRK或者是React OS的源碼。當然,這些代碼時代都比較久遠了,可能細節(jié)上和現在的Windows實現不太一樣,但是也能說明問題。
P.S.
在Windows Vista以上操作系統(tǒng),將完成端口的句柄直接關閉將取消所有關聯的IO操作,關聯IO端口的所有線程調用GetQueuedCompletionStatus
會放棄等待并立即返回FALSE
,這時調用GetLastError
獲取錯誤碼時,會返回ERROR_INVALID_HANDLE
。檢測到這一情況就可以退出了。小插曲
在分析Windows 10內核的時候在Explorer中可以看到ntoskrnl,而在IDA中看不到。最后只得將其復制到其他地方才進行了分析,感嘆一句微軟套路深。
3. 完成鍵(CompletionKey)和重疊結構(Overlapped)的設置問題?
這里可能是理解完成端口的一個難點,至少筆者在學習的時候在這里停頓了一段時間。
首先說說完成鍵。這個參數是為了給線程池中的線程通信而設計的,也就是說當調用前文所述AssociateDeviceWithCompletionPort
時傳入的完成鍵將會傳給調用GetQueuedCompletionStatus
的線程。這樣,主線程就可以通過這兩個函數與線程池中的線程進行通信。同樣注意到完成鍵是一個DWORD類型,也可以給它傳入一個結構體的地址。
而重疊結構是在IO處理時傳遞給相應IO函數的數據載體。這個結構很有用,但本文不再展開說明,有興趣的朋友可以查看參考文獻相應部分。C/C++程序員應該都知道這樣一個事實:結構體的第一個成員的地址和結構體的地址是相同的。所以,我們可以定義一個結構體(或者是一個C++類),將重疊結構作為第一個成員,在IO處理時,將我們定義的結構傳入。這樣,IO函數處理它自身需要的重疊結構信息,而我們可以在其中夾帶私貨。為什么要這么做呢?因為在我們在線程函數中可能需要一些其他的數據,這樣就可以通過這種辦法傳進去。
于是我們就明白了:完成鍵與線程有關而重疊結構與IO有關。我們需要完成鍵給線程傳遞參數,需要重疊結構(以及夾帶的私貨)來完成IO操作。
至于這些怎樣與Socket
結合,請瀏覽下一節(jié)內容。
更深入的討論高級程序員參考
在piggyXP的博文中提到了一個“神奇的宏”:CONTAINING_RECORD
。這個宏廣泛應用于驅動編程中,用于獲取在知道結構體某成員地址的情況下推知整個結構體地址的場景中。具體定義如下:
/*** Calculate the address of the base of the structure given its type, and an* address of a field within the structure.*/#define CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field)))
這個是帶有濃郁C風格、充滿trick的一個宏。能進行深入討論的朋友一看就明白,就不班門弄斧了。值得注意的是,使用這個宏的時候對成員是否是結構體的第一個成員沒有限制。
主要使用的API有如下6個:
創(chuàng)建套接字函數WSASocket
,在創(chuàng)建OVERLAPPED
套接字時使用。
注意
WSASocket
是一個宏定義,在MBCS
環(huán)境下定義為WSASocketA
,在UNICODE
環(huán)境下定義為WSASocketW
。
SOCKET WSAAPI WSASocketW ( // WSASocketA for MBCS _In_ int af, _In_ int type, _In_ int protocol, _In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo, // LPWSAPROTOCOL_INFOA for MBCS _In_ GROUP g, _In_ DWORD dwFlags );
綁定函數bind
,在服務器初始化時使用。
int WSAAPI bind( _In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen );
監(jiān)聽函數listen
,在等待客戶端連接監(jiān)聽時使用。
int WSAAPI listen( _In_ SOCKET s, _In_ int backlog );
控制套接字函數WSAIoctl
,在獲取函數指針時使用。
int WSAAPI WSAIoctl( _In_ SOCKET s, _In_ DWORD dwIoControlCode, _In_reads_bytes_opt_(cbInBuffer) LPVOID lpvInBuffer, _In_ DWORD cbInBuffer, LPVOID lpvOutBuffer, _In_ DWORD cbOutBuffer, _Out_ LPDWORD lpcbBytesReturned, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
微軟擴展的accept
函數AcceptEx
,用于接受用戶接入并獲取第一組傳輸的數據,代替accept
使用。?
BOOL PASCAL AcceptEx ( _In_ SOCKET sListenSocket, _In_ SOCKET sAcceptSocket, PVOID lpOutputBuffer, _In_ DWORD dwReceiveDataLength, _In_ DWORD dwLocalAddressLength, _In_ DWORD dwRemoteAddressLength, _Out_ LPDWORD lpdwBytesReceived, _Inout_ LPOVERLAPPED lpOverlapped );
微軟擴展的配合解析AcceptEx
函數返回值使用的函數GetAcceptExSockaddrs
,需要獲取第一組數據的時候使用。
void GetAcceptExSockaddrs ( _In_ PVOID lpOutputBuffer, _In_ DWORD dwReceiveDataLength, _In_ DWORD dwLocalAddressLength, _In_ DWORD dwRemoteAddressLength, _Out_ LPSOCKADDR *LocalSockaddr, _Out_ LPINT LocalSockaddrLength, _Out_ LPSOCKADDR *RemoteSockaddr, _Out_ LPINT RemoteSockaddrLength);
異步接受數據函數WSARecv
,在接收數據時使用。?
int WSAAPI WSARecv( _In_ SOCKET s, _In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_opt_ LPDWORD lpNumberOfBytesRecvd, _Inout_ LPDWORD lpFlags, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
異步接受數據函數WSASend
,在接收數據時使用。?
int WSAAPI WSASend( _In_ SOCKET s, _In_reads_(dwBufferCount) LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_opt_ LPDWORD lpNumberOfBytesSent, _In_ DWORD dwFlags, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
以上函數的詳細用法在參考文獻及piggyXP的文章中可以找到,故不再贅述。
在編程過程中主要考慮以下幾個問題:
1.
AcceptEx
和GetAcceptExSockaddrs
函數的調用問題
在實際使用中我們可以發(fā)現,調用這兩個函數無一不是利用了WSAIoctl
返回的函數指針。筆者在MSDN中也找到了這樣的說法:
“The function pointer for the AcceptEx / GetAcceptExSockaddrs function must be obtained at run time by making a call to the WSAIoctl function with the SIO_GET_EXTENSION_FUNCTION_POINTER opcode specified. “
因此,我們在使用這兩個函數之前必須通過WSAIoctl
來獲取這兩個函數的指針加以調用。
更深入的討論高級程序員參考
事實上筆者發(fā)現,在mswsock.dll
中是導出了這兩個函數的。那為什么微軟在MSDN中沒有說到呢,非要用如此麻煩的方式去調用AcceptEx
和GetAcceptExSockaddrs
這兩個函數?
mswsock.dll
其實也只是一個轉發(fā)器,真實的函數在另外的地方。在AcceptEx
函數內部也會調用WSAIoctl
(在ws2_32.dll
中實現)來獲取真實的函數地址。有一個非常有意思的地方,
AcceptEx
函數除了尋找自己的真實函數地址以外,還回去尋找GetAcceptExSockaddrs
函數的地址,同時進行設置;在導出的GetAcceptExSockaddrs
函數內部不會再去尋找自身實現的地址,而是使用AcceptEx
函數設置的地址,如果地址為空則將后四個傳入的參數全部置零,有興趣的朋友可以嘗試一下。所以使用導出的
AcceptEx
而不通過指針從理論上也是可以的,在使用導出的GetAcceptExSockaddrs
之前務必要使用導出的AcceptEx
來設置內部指針,而且并不是說使用導出的函數效率低才使用函數指針獲取函數實現地址??赡艿脑蚴敲總€Windows版本的實現驅動可能不同,對上的接口需要mswsock.dll
來保持一致。另外,在使用這兩個函數時要注意在傳遞
SOCKADDR_IN
結構體大小時要加上16,與具體實現相關,原因不明。
2. 設置各函數完成鍵和重疊結構體的問題
接完成鍵(CompletionKey)和重疊結構(Overlapped)的設置問題討論。在本文程序中,要設置完成鍵和重疊結構體的主要有以下6個函數,如表4所示:
函數 | 需要設置的內容 | 相關解釋 |
---|---|---|
AssociateDeviceWithCompletionPort | 完成鍵 | 初始化時將完成端口和線程綁定時需要使用 |
GetQueuedCompletionStatus | 完成鍵和重疊結構 | 線程獲取參數與IO狀態(tài)時使用 |
PostQueuedCompletionStatus | 完成鍵和重疊結構 | 傳遞線程參數與設置IO狀態(tài)時使用 |
AcceptEx | 重疊結構 | 在異步接受客戶端接入時使用 |
WSARecv | 重疊結構 | 在異步接收消息時使用 |
WSASend | 重疊結構 | 在異步發(fā)送消息時使用 |
大家一看就明白了,AssociateDeviceWithCompletionPort
是主線程將創(chuàng)建好的完成端口與IO設備綁定時調用的,只需要完成鍵;GetQueuedCompletionStatus
函數是線程池中工作線程調用的,因此要獲取完成鍵和重疊結構;PostQueuedCompletionStatus
函數要傳遞參數和設置IO狀態(tài)到完成隊列中去,因此也需要兩個;AcceptEx
、WSARecv
和WSASend
函數是用來進行IO操作(網絡操作)的,因此只需要和網絡IO設備打交道,只需設置重疊結構。
注意到前述討論中的問題,可以設計這樣一個結構體充當重疊結構夾帶私貨:
using IO_CONTEXT = struct _IO_CONTEXT { /** * data section */ OVERLAPPED m_olOverLapped; /**< Windows overlapped structure */ SOCKET m_sAssociatedSocket; /**< context associated socket */ WSABUF m_wsaBuffer; /**< the buffer to recieve WSASocket data */ CHAR m_cBuffer[MAX_BUFFER_SIZE]; /**< message buffer */ enum class Flag : unsigned char { Read, /**< read( recv ) */ Write, /**< write( send ) */ Accept /**< accept socket( for AcceptEx API ) */ } m_bFlag; /**< rw flag */ /** * operation section */ ...}using PIO_CONTEXT = IO_CONTEXT*;
注意到完成鍵可以傳入某結構體或類的地址,因此可以設計這樣一個結構體充當完成鍵傳遞給線程池中線程:
using HANDLE_CONTEXT = struct _HANDLE_CONTEXT { /** * data section */ SOCKET m_hClientSocket; /**< socket in thread to handle */ SOCKADDR_IN m_sClientAddr; /**< sockaddr_in in thread to handle */ std::vector<PIO_CONTEXT> m_vIoContext; /**< vector of IoContext pointer */ bool m_bFinished; /**< is process finished */ /** * operation section */ ...}using PHANDLE_CONTEXT = HANDLE_CONTEXT*;
結構定義和piggyXP大同小異,主要差別就在于HANDLE_CONTEXT::m_bFinished
項,在PostQueuedCompletionStatus
傳遞時將其置為true
,讓線程池中線程退出即可。
上下文大致運行流程如圖2所示,聰明的你一定一下就明白,就不贅述了??梢詤⒖?a rel="nofollow" target="_self">上一節(jié)所述流程,也可以參照代碼理解:
本節(jié)參考文獻
ISO. IEC14882:2011 Information technology – Programming languages – C++ [S]. Geneva, Switzerland: International Organization for Standardization, 2011.
Meyers S. Effective modern C++: 42 specific ways to improve your use of C++ 11 and C++ 14[M]. ” O’Reilly Media, Inc.”, 2014.
提示
這一節(jié)內容和本文主體關系不大,內容也不深,對本節(jié)不感興趣的朋友可以跳過。
例如piggyXP給出了如下的函數樣式的宏:
// 釋放指針宏#define RELEASE(x) {if(x != NULL ){delete x;x=NULL;}}
而筆者在定義時選擇了內聯函數:
/*** Release memory*/template<typename _T>inline void ReleaseMemory( _T*& pMemory ) { if ( pMemory != nullptr ) { delete pMemory; pMemory = nullptr; }}
主要代碼是差不多的,但是能夠完成的操作是不一樣的,聰明的你應該可以看出來。這個例子不一定好,那就再舉一個常見的:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) // oopsint result_oops = MAX(i++, j);
選用function-like macro
的好處只有一條:簡單方便,效率高(空間換時間),缺點就不多說了,看著就明白。選用inline function
最主要的好處就是:類型檢查,效率高(可能空間換時間)。
在編程過程中請盡可能減少預處理器的使用(尤其是函數樣式的宏)。
我們可能習慣于這樣定義“常量”:
#define MAX_BUFFER_SIZE 8192
當然,這是一個宏,在使用的時候替換為8192這一個字面量??紤]這樣的代碼:
#define N 2 + 3// oopsint oops = N / 2; // 3
當然你也可以這樣定義,不過總覺得這樣定義很別扭:
#define N ( 2 + 3 )
結果不用多說。采用宏常量的理由還是:方便、效率高(字面值,在代碼中成為立即數),但是沒有類型檢查(預處理器管理),有時候用著很麻煩。
而以往的常量const
又占用了存儲空間,而且畢竟存儲在內存中,也是可以變化的??紤]以下代碼:
const int constant = 0;int* evil_ptr = ( int* )&constant;*evil_ptr = 1;...
這樣,一個常量就變化了。
更深入的討論高級程序員參考
事實上筆者在測試的時候發(fā)現如果對constant
進行輸出,會得到結果為0。反匯編后發(fā)現VS直接給輸出函數賦的是0,沒有從地址取值,優(yōu)化的還是可以。
在C++11中引入了常量表達式constexpr
的概念,它是一個編譯期的常量(字面量),由編譯器負責執(zhí)行。這樣,又可以進行類型檢查,又可以提高效率,減少資源占用,好處還是很多的。其中一個:
constexpr std::size_t N = 2 + 3;// no oopsauto normal = N / 2; // 2