1. 程式人生 > >C++構造函數和編譯器自動生成代碼的陷阱

C++構造函數和編譯器自動生成代碼的陷阱

log bug () 很好 style 自動 pub 為我 ret

最近在項目中debug各種access violation的,其中這個問題比較有代表性,並且能夠被規範的代碼標準解決。

問題可以總結為以下的代碼:

 1 class TestString
 2 {
 3 public:
 4     TestString(const char* input) : m_value(input) {}
 5     TestString(const TestString& input) : m_value(input.m_value) {}
 6     operator const char*() const { return m_value.c_str(); }
7 string m_value; 8 }; 9 10 void main() 11 { 12 TestString testStr("StringA"); 13 const char* stringB = "StringB"; 14 const char* result = true ? testStr : stringB; 15 // Will result point to "StringA"? 16 assert(result == testStr.m_value.c_str()); 17 }

以上代碼裏面,你可以定認為`result`會指向`testStr.m_value.c_str()`吧,因為我們重載了`operator const char*()`,

其實不然,如果你運行以上的代碼,你會發現`result`最後指向的是一個“隨機”的內存地址。

我在排除了周圍沒有任何問題之後,打開了匯編代碼的瀏覽器:

    const char* result = true ? testStr : stringB;
00CE9585  mov         eax,1  
00CE958A  test        eax,eax  
00CE958C  je          main+0B0h (0CE95D0h)  
00CE958E  lea         ecx,[testStr]  
00CE9591  push        ecx  
00CE9592  lea         ecx,[ebp
-138h] 00CE9598 call TestString::TestString (0CE1659h) ... 00CE95DA call TestString::TestString (0CE14DDh) ... 00CE9625 call TestString::operator char const * (0CE105Ah) ... 00CE964C call TestString::~TestString (0CE14A1h) ... 00CE9670 call TestString::~TestString (0CE14A1h)

在這裏,你能夠清楚的看到編譯器把`testStr`和`stringB`都準換成了類型為`TestString`的臨時對象,然後調用`operator const char*()`來吧結果轉換為`const char*`,不過之後這2個臨時對象都被自動銷毀了,所以你得到的結果也成為了Dangling pointer。

至於解決方案,你估計可以想到這樣改:

    const char* result = true ? testStr.m_value.c_str() : stringB;
0008504C  mov         eax,1  
00085051  test        eax,eax  
00085053  je          main+55h (085065h)  
00085055  lea         ecx,[testStr]  
00085058  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::c_str (081370h)  
0008505D  mov         dword ptr [ebp-104h],eax  
00085063  jmp         main+5Eh (08506Eh)  
00085065  mov         ecx,dword ptr [stringB]  
00085068  mov         dword ptr [ebp-104h],ecx  
0008506E  mov         edx,dword ptr [ebp-104h]  
00085074  mov         dword ptr [result],edx  

通過顯式的調用`test.m_value().c_str()`來避免編譯器生成預期之外的類型轉化。

不過記得我在本文開始說的,這個問題可以通過很好的代碼規範來避免,這裏我們需要用到的方法是`explicit`。通過把帶一個參數的構造函數定義為`explicit`,我們可以避免編譯器對被標記的構造函數的隱性調用。

所以這裏我所建議的fix是,這樣定義你的TestString:

class TestString
{
public:
    explicit TestString(const char* input) : m_value(input) {}
    explicit TestString(const TestString& input) : m_value(input.m_value) {}
    operator const char*() const { return m_value.c_str(); }
    string m_value;
};

然後我們來看看編譯器生成的新代碼:

    const char* result = true ? testStr : stringB;
00F23C3B  mov         eax,1  
00F23C40  test        eax,eax  
00F23C42  je          main+74h (0F23C54h)  
00F23C44  lea         ecx,[testStr]  
00F23C47  call        TestString::operator char const * (0F21604h)  
00F23C4C  mov         dword ptr [ebp-110h],eax  
00F23C52  jmp         main+7Dh (0F23C5Dh)  
00F23C54  mov         ecx,dword ptr [stringB]  
00F23C57  mov         dword ptr [ebp-110h],ecx  
00F23C5D  mov         edx,dword ptr [ebp-110h]  
00F23C63  mov         dword ptr [result],edx  

case close. :)

C++構造函數和編譯器自動生成代碼的陷阱