日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++中stack的pop()函數返回值解析_C 語言

作者:code的魅力 ? 更新時間: 2022-09-17 編程語言

stack的pop()函數返回值

    int temp = s.pop();
    cout<<temp<<endl; 

運行代碼會提示錯誤:error C2440: “初始化”: 無法從“void”轉換為“int”

全部demo

#include <iostream>
#include <stack>
 
using namespace std;
 
int main()
{
	stack<int> s;
	if(s.empty())
		cout<<"empty"<<endl;   //empty
	s.push(1);
	s.push(6);
	s.push(66);
	cout<<s.size()<<endl;   //3
	int temp = s.pop();
	cout<<temp<<endl;	//66
	cout<<s.size()<<endl;	//2
	cout<<s.top()<<endl;	//6
	cout<<s.size()<<endl;	//2
	system("pause");
	return 0;
 
}

分析

C++中stack,其中有兩個方法:

  • pop(), 返回void,
  • top(),返回棧頂的引用。

所以想要提取棧頂元素,直接用s.top()

C++的返回值優化

大家都知道“過早的優化是萬惡之源”這句話,然而我相信其中的大多數人都不知道自己是不是在做過早的優化。我也無法準確的定義什么叫做“過早的優化”,但我相信這“過早的優化”要么是得不償失的,要么干脆是有害無利的。今天我就想舉個我認為是“過早的優化”的例子。

從函數返回值

為了從一個函數得到運行結果,常規的途徑有兩個:通過返回值和通過傳入函數的引用或指針(當然還可以通過全局變量或成員變量,但我覺得這算不上是什么好主意)。

通過傳給函數一個引用或指針來承載返回值在很多情況下是無可厚非的,畢竟有時函數需要將多個值返回給用戶。除了這種情況之外,我覺得應當盡量做到參數作為函數輸入,返回值作為函數輸出(這不是很自然的事情嗎?)。然而,我們總能看到一些“突破常規”的做法:

首先定義Message類:

struct Message
{
? ? int a;
? ? int b;
? ? int c;
? ? int d;
? ? int e;
? ? int f;
};

為了從某個地方(比如一個隊列)得到一個特定Message對象,有些人喜歡寫一個這樣的getMessage:

void getMessage(Message &msg); // 形式1

雖然只有一個返回值,但仍然是通過傳入函數的引用返回給調用者的。

為什么要這樣呢?“嗯,為了提高性能。你知道,要是這樣定義函數,返回Message對象時必須要構造一個臨時對象,這對性能有影響?!?/p>

Message getMessage(); // 形式2

我們先不討論這帶來了多少性能提升,先看看形式1相對形式2帶來了哪些弊端。我認為有兩點:

1. 可讀性變差

略(我希望你能和我一樣認為這是顯而易見的)。

2. 將對象的初始化劃分成了兩個步驟

調用形式1時,你必然要這樣:

Message msg; ? ? // S1
getMessage(msg); // S2

這給維護者帶來了犯錯的機會:一些需要在S2語句后面對msg進行的操作有可能會被錯誤的放在S1和S2之間。

如果是形式2,維護者就不可能犯這種錯誤:

Message msg = getMessage();

好,現在我們來看性能,形式2真的相對形式1性能更差嗎?對于下面的代碼:

#include <stdio.h>
?
struct Message
{
? ? Message()
? ? {?
? ? ? ? printf("Message::Message() is called\n");?
? ? }
? ? Message(const Message &)
? ? {
? ? ? ? printf("Message::Message(const Message &msg) is called\n");
? ? }
? ? Message& operator=(const Message &)
? ? {
? ? ? ? printf("Message::operator=(const Message &) is called\n");
? ? }
? ? ~Message()
? ? {
? ? ? ? printf("Message::~Message() is called\n");
? ? }
? ? int a;
? ? int b;
? ? int c;
? ? int d;
? ? int e;
? ? int f;
};
?
Message getMessage()
{
? ? Message result;
? ? result.a = 0x11111111;
?
? ? return result;
}
?
int main()
{
? ? Message msg = getMessage();
? ? return 0;
}

你認為運行時會輸出什么呢?是不是這樣:

Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called

并沒有像預期的輸出那樣。

如果使用MSVC2017編譯,且關閉優化(/Od),確實可以得到預期輸入,但是一旦打開優化(/O2),輸出就和GCC的一樣了。

我們看看實際上生成了什么代碼(使用GCC編譯):

(gdb) disassemble main
Dump of assembler code for function main():
? ?0x0000000000000776 <+0>:?? ?push ? %rbp
? ?0x0000000000000777 <+1>:?? ?mov ? ?%rsp,%rbp
? ?0x000000000000077a <+4>:?? ?push ? %rbx
? ?0x000000000000077b <+5>:?? ?sub ? ?$0x28,%rsp
? ?0x000000000000077f <+9>:?? ?mov ? ?%fs:0x28,%rax
? ?0x0000000000000788 <+18>:?? ?mov ? ?%rax,-0x18(%rbp)
? ?0x000000000000078c <+22>:?? ?xor ? ?%eax,%eax
? ?0x000000000000078e <+24>:?? ?lea ? ?-0x30(%rbp),%rax ? ? ? ? ? ? #將棧上地址-0x30(%rbp)傳給getMessage函數
? ?0x0000000000000792 <+28>:?? ?mov ? ?%rax,%rdi
? ?0x0000000000000795 <+31>:?? ?callq ?0x72a <getMessage()>
? ?0x000000000000079a <+36>:?? ?mov ? ?$0x0,%ebx
? ?0x000000000000079f <+41>:?? ?lea ? ?-0x30(%rbp),%rax
? ?0x00000000000007a3 <+45>:?? ?mov ? ?%rax,%rdi
? ?0x00000000000007a6 <+48>:?? ?callq ?0x7e4 <Message::~Message()>
? ?0x00000000000007ab <+53>:?? ?mov ? ?%ebx,%eax
? ?0x00000000000007ad <+55>:?? ?mov ? ?-0x18(%rbp),%rdx
? ?0x00000000000007b1 <+59>:?? ?xor ? ?%fs:0x28,%rdx
? ?0x00000000000007ba <+68>:?? ?je ? ? 0x7c1 <main()+75>
? ?0x00000000000007bc <+70>:?? ?callq ?0x5f0 <__stack_chk_fail@plt>
? ?0x00000000000007c1 <+75>:?? ?add ? ?$0x28,%rsp
? ?0x00000000000007c5 <+79>:?? ?pop ? ?%rbx
? ?0x00000000000007c6 <+80>:?? ?pop ? ?%rbp
? ?0x00000000000007c7 <+81>:?? ?retq ??
End of assembler dump.
(gdb) disassemble getMessage?
Dump of assembler code for function getMessage():
? ?0x000000000000072a <+0>:?? ?push ? %rbp
? ?0x000000000000072b <+1>:?? ?mov ? ?%rsp,%rbp
? ?0x000000000000072e <+4>:?? ?sub ? ?$0x20,%rsp
? ?0x0000000000000732 <+8>:?? ?mov ? ?%rdi,-0x18(%rbp) ? ? ? ? ? ? ? ? #將main函數傳入的棧上地址保存到-0x18(%rbp)處
? ?0x0000000000000736 <+12>:?? ?mov ? ?%fs:0x28,%rax
? ?0x000000000000073f <+21>:?? ?mov ? ?%rax,-0x8(%rbp)
? ?0x0000000000000743 <+25>:?? ?xor ? ?%eax,%eax
? ?0x0000000000000745 <+27>:?? ?mov ? ?-0x18(%rbp),%rax ? ? ? ? ? ? #將main函數傳入的棧上地址傳給Message::Message()函數
? ?0x0000000000000749 <+31>:?? ?mov ? ?%rax,%rdi
? ?0x000000000000074c <+34>:?? ?callq ?0x7c8 <Message::Message()>
? ?0x0000000000000751 <+39>:?? ?mov ? ?-0x18(%rbp),%rax
? ?0x0000000000000755 <+43>:?? ?movl ? $0x11111111,(%rax)
? ?0x000000000000075b <+49>:?? ?nop
? ?0x000000000000075c <+50>:?? ?mov ? ?-0x18(%rbp),%rax
? ?0x0000000000000760 <+54>:?? ?mov ? ?-0x8(%rbp),%rdx
? ?0x0000000000000764 <+58>:?? ?xor ? ?%fs:0x28,%rdx
? ?0x000000000000076d <+67>:?? ?je ? ? 0x774 <getMessage()+74>
? ?0x000000000000076f <+69>:?? ?callq ?0x5f0 <__stack_chk_fail@plt>
? ?0x0000000000000774 <+74>:?? ?leaveq?
? ?0x0000000000000775 <+75>:?? ?retq ??
End of assembler dump.

可以看出來,在getMessage函數中構造的對象實際上位于main函數的棧幀上,并沒有額外構造一個Message對象。這是因為開啟了所謂的返回值優化(RVO,Return Value Optimization)的緣故。你想得到的效果編譯器已經自動幫你完成了,你不必再犧牲什么。

RVO

對于我們這些用戶來說,RVO并不是什么特別復雜的機制,主流的GCC和MSVC均支持,也沒什么特別需要注意的地方。它存在的目的是優化掉不必要的拷貝復制函數的調用,即使拷貝復制函數有什么副作用,例如上面代碼中的打印語句,這可能是唯一需要注意的地方了。從上面的匯編代碼中可以看出來,在GCC中,其基本手段是直接將返回的對象構造在調用者棧幀上,這樣調用者就可以直接訪問這個對象而不必復制。

RVO是有限制條件的,在某些情況下無法進行優化,在一篇關于MSVC2005的RVO技術的文章中,提到了3點導致無法優化的情況:

1. 函數拋異常

關于這點,我是有疑問的。文章中說如果函數拋異常,開不開RVO結果都一樣。如果函數拋異常,無法正常的返回,我當然不會要求編譯器去做RVO了。

2. 函數可能返回具有不同變量名的對象

Message getMessage_NoRVO1(int in)
{
? ? Message msg1;
? ? msg1.a = 1;
?
? ? Message msg2;
? ? msg2.a = 2;
?
? ? if (in % 2)
? ? {
? ? ? ? return msg1;
? ? }
? ? else
? ? {
? ? ? ? return msg2;
? ? }
}

經過驗證,在GCC上確實也是這樣的,拷貝構造函數被調用了。但這種情況在很多時候應該都是可以通過重構避免的。

Message::Message() is called
Message::Message() is called
Message::Message(const Message &msg) is called
Message::~Message() is called
Message::~Message() is called
Message::~Message() is called

3. 函數有多個出口

Message getMessage_NoRVO2(int in)
{
? ? Message msg;
? ? if (in % 2)
? ? {
? ? ? ? return msg;
? ? }
? ? msg.a = 1;
? ? return msg;
}

這個在GCC上驗證發現RVO仍然生效,查看匯編發現只有一個retq指令,多個出口被優化成一個了。

原文鏈接:https://blog.csdn.net/wx1458451310/article/details/88042718

欄目分類
最近更新