標籤:

std::vector會在不同dll中傳遞修改時帶來影響嗎?

我在opencv中的findContour函數上遇到了問題,在vs2015+opencv3.0+x86的環境下,findContour需要傳入vector&&>這樣的結構,下面是測試結果。

在debug下,這個vector如果是棧分配的話,在函數執行完時會掛掉的,大概報錯原因是內存釋放錯誤。如果是在堆上分配的話是ok的,但是delete vector指針時會報同樣的錯誤,這樣的話就會內存泄漏的啊!

在release下都是ok的!

上網查找答案,有人說:因為vector是在主程序中的分配,傳到opencv的dll中後,opencv會再重新分配內存,這樣就會導致和「申請的內存必須要在申請的域中刪除」矛盾了!

這個說法對嗎?是這個原因嗎?


你可以在dll頭文件裡面寫

template __declspec(dllexport) class std::vector&&>;

然後所有用到vector&&>的地方都#include 這個頭文件,就可以了。

這個方法的缺點是,如果你有兩個dll內部都用了vector&&>,那麼將沒有任何辦法可以work around。


回答:對。是。

詳述如下

其實答案在《Imperfect C++》第9章 的 9.5. Resource Ownership

下面回答英文部分都是從書中拷貝的。

首先書中提到,處理這類問題的原則是:

Good resource consumers should always return a resource whence it came.

再強調在動態庫中這個問題特別注意:

This issue is of particular importance when dealing with dynamic libraries, since any static resource manager objects will be local to the dynamic librarys link space.

然後提出了2中方法,其中一個是:9.5.1 Shared Pools

此節提到:

One way to provide safe memory allocation and deallocation between dynamic libraries is to have all libraries themselves depend on a single dynamic library that provides operators new delete and/or the functions malloc()/free(). The Visual C++ runtime library uses this model in the form of MSVCRT.DLL, and other vendors offer similar facilities.

所以也不是不能在不同的模塊傳遞 stl 參數,但是你必須確認兩個模塊使用的是同樣的STL實現,比如,VC是不是同一個版本,編譯選項是不一樣(運行時庫等等)。

估計你在你的程序的 release 與 debug 版中都使用了同一個 opencv 的動態庫,我猜是 release 版的。如果是這樣就能解析為何你 debug 版出問題,release 版沒任何問題。因為 findContour 函數需要 resize 傳入的 vector 參數,這裡會調用 release 版(可能是/MD)運行時庫分配堆上的內存 ,如果你的程序是 debug 版的,那麼 vector 參數析構的時候,會調用 debug 版(可能是/MDd)運行時庫釋放內存。這時就搞了個大新聞。

PS: opencv 沒玩過,也不知道對 findContour 參數的理解對否。如果關於這點理解錯了,不要把我拿出去批判一番。


要跨越DLL傳遞STL對象,你需要在對象分配和釋放內存的時候使用的是同一個CRT的堆(這意味著編譯器版本相同而且不能混用debug和release的DLL,也不能混用MBCS/Unicode)然後你的對象以及對象使用到的類型需要有同一個ABI(調用約定、位元組對齊、函數inline等設置完全相同)參考Potential Errors Passing CRT Objects Across DLL Boundaries。

編譯器版本完全相同這一條,代碼完全自己控制的場合還行,在組件更新周期依賴於其他項目組或者有使用OpenCV等第三方庫的場合未必可行。更加靠譜的辦法是不跨DLL傳遞CRT對象。


個人猜測:

關於 STL 容器跨 DLL 傳遞的問題,如果首先確定了兩邊實現一樣,只是堆不同,可以這麼解決。

C++11/14 用手寫的有狀態分配器 。繼承 std::allocator 後在分配器對象里存指向 ::operator new/::operator delete 的指針。修改 allocate/deallocate/operator== 的實現。使用、比較這些函數指針。

C++17 可以用 std::pmr:: 下的多態分配器。(這種分配器實現是存一個指針,指向多態的內存資源管理類對象,由這些類的虛函數管理內存)

然後你要用的模板特化是 std::vector&&> (可以考慮用別名模板)或 std::pmr::vector&

另外跨堆傳遞的 unique_ptr 需要用手寫的刪除器。原理類似上面,存個指向 ::operator delete 或 ::operator delete[] 的指針,通過這些指針解分配內存。 shared_ptr 在管理塊里維護正確的刪除函數調用,所以不用額外處理。

有人提議用 C 介面傳,但這不能解決跨堆傳遞的問題,仍然要傳正確的刪除器(指向自己 free 的指針等)。

最後是這個問題:

你是願意堅持使用較為複雜的傳遞策略,還是堅持不要跨模塊傳遞內存資源的所有權?


使用STL元素作為dll介面參數一直是大忌。因為C++的binary interface無論是紙面標準,還是實際實現中,都是四分五裂。VC換一個版本,編譯出來的layout不一樣的事也是屢見不鮮。要不然每代VC怎麼會都有自己的CRT。

另外STL本身也是千差萬別。以最常用的vector為例,C++03里,你建一個長度為n的vector,系統的行為是先建一個元素,然後將這個元素copy到vector中的每一個位置上。而在C++11里,系統的行為是為vector中的每個位置新建一個元素。當你的default constructor和copy constructor設計時沒有考慮時,這兩種情況下得到的vector的內容有可能不一樣!

而這還僅僅是標準C++,要是再數數各路實現的私貨……

據稱,VC的STL的一些組件,debug和retail的實現都完全不一樣……

所以對於C++來說,要麼在dll里使用C介面,要麼拿到dll源碼,將其和exe用同樣的環境編譯。只有這兩種辦法能保證不會因為binary interface和STL版本出問題。對於opencv這樣的開源軟體,還是用自己的VC重新編譯一遍吧。


個人認為不是這種原因。

我也用過findcontours,vector是局部變數,opencv3.1,靜態庫動態庫都用過,但並沒有出現這種問題。

會不會是用了非vs2015的dll或者是bug?


傳入的dll的stl容器,在dll內部只讀,在dll內部創建的在外部只讀,我在vs2015裡面操作的時候,只要不會讓stl容器跨dll修改就沒問題。

另外可以使用/md 試試。當然要保證所有的代碼在同一個機器上編譯用的是相同的stl庫。


你試試一次reserve夠足夠的內存。


《程序員的自我修養-鏈接、裝載與庫》裡面的關於Windows的動態鏈接庫的設計。看完你就知道原因了


用這種複雜技術就是挖坑。


dll都是有自己單獨的堆棧管理的。vector裡面的內存是在堆上申請的,那麼哪個模塊申請的就得這個模塊的堆棧管理來釋放。不僅僅vector,所以堆上申請的內存都要遵循這個原則。


先說解決辦法:

//100是預估值

vector& contours(100);

Mat hierarchy;//這句寫成Vector& hierarchy(100)也行

findContours( BW, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE );

dll中給vector分配成員空間,無論你是傳引用,還是指針,只要vector的初始化容器大小,放不下成員,在dll中就會給vector增加空間,故預先在自己程序中分配好。


第一個問題:棧申請的內存請不要在dll中保存,因為調用完棧回收後就是野指針。

第二個問題:誰申請內存誰釋放,不要在dll中釋放其他內存!


推薦閱讀:

在 Windows 下鍵入 Enter 鍵,是在鍵盤緩衝區中存入
還是
兩個?

C++ 中對 main 函數的地址賦值會怎樣?
全局指針變數指向棧上的對象的問題?
寫程序時中間變數用cnm,是什麼心態?
C++里一個帶有返回值的函數在沒有return語句的情況下在GCC里編譯通過是否可看作GCC的bug?

TAG:OpenCV | CC |