2016-11-15 70 views
12

雖然今天重構一些代碼以將原始指針更改爲std::unique_ptr,但由於order of evaluation錯誤,我遇到了分段錯誤。C++ 17表達式評估順序和std :: move

舊的代碼不喜歡的東西如下:

void add(const std::string& name, Foo* f) 
{ 
    _foo_map[name] = f; 
} 

void process(Foo* f) 
{ 
    add(f->name, f); 
} 

代碼的第一,天真,重構使用std::unique_ptr

void add(const std::string& name, std::unique_ptr<Foo> f) 
{ 
    _foo_map[name] = std::move(f); 
} 

void process(std::unique_ptr<Foo> f) 
{ 
    add(f->name, std::move(f)); // segmentation-fault on f->name 
} 

重構後的代碼會導致分段錯誤,因爲第二個參數(std::move(f))首先被處理,然後第一個參數(f->name)將一個移動的變量取消引用,繁榮!

可以修復這個是獲得一個手柄上Foo::name之前在呼叫移動它add

void process(std::unique_ptr<Foo> f) 
{ 
    const std::string& name = f->name; 
    add(name, std::move(f)); 
} 

或許:

void process(std::unique_ptr<Foo> f) 
{ 
    Foo* fp = f.get(); 
    add(fp->name, std::move(f)); 
} 

這些解決方案都需要額外的行的代碼,並且看起來幾乎不像原來的(雖然UB)調用add

問題:

+1

我會說一個「慣用」的解決方案是重構'add'函數,或者添加一個需要'std :: unique_ptr '對象的重載。 –

+0

@Someprogrammerdude只是將需求從'process'轉移到'add'的權限上,但是對嗎?或者P0145R3解決這個問題? –

+1

國際海事組織通常是一個好主意,儘可能將這些細節推向最下面。畢竟抽象是OO和C++的主要部分之一。 –

回答

6

對我來說這兩個建議看起來不好。在那些你正在給這個動作搬走Foo物體的人中。這意味着你不能再對它的狀態做出任何假設。在第一個參數(對字符串或對象的指針的引用)被處理之前,它可以在add函數內被釋放。是的,它會在當前的實施中發揮作用,但只要有人觸及add或更深層次的任何內容,它就會中斷。

的安全方式:

  • 使字符串的副本,並傳遞作爲第一個參數add
  • 重構您add方法,只需要Foo類型的一個參數,並提取Foo::name內該方法並沒有把它作爲一個參數。但是,您仍然在add方法中遇到同樣的問題。

在第二種方法中,你應該能夠得到解決的評估順序問題,首先創建一個新的映射條目(默認值),並得到一個可變引用它,之後將值:

auto& entry = _foo_map[f->name]; 
entry = std::move(f); 

不確定您的地圖實現是否支持獲取對條目的可變引用,但對於許多應用程序應該可以工作。

如果我再想一想,也可以去「複製名稱」方法。無論如何都需要複製地圖密鑰。如果你手動複製它,你可以將它移動到鍵上,沒有任何開銷。

std::string name = f->name; 
_foo_map[std::move(name)] = std::move(f); 

編輯:

正如在評論中指出,應該可以直接分配_foo_map[f->name] = std::move(f)add函數內部由於評估順序是這裏保證。

+0

是否真的有必要*複製名稱*? '[]'和'='只是函數'operator []'和'operator ='的語法糖。因此,語句'map [f-> name] = std :: move(foo)'將是'map.operator [](f-> name).operator =(std :: move(foo))''正確嗎?而在P0145R3中這幾個函數調用的評估順序是定義行爲的權利? –

+0

不是100%的確定,因爲我沒有想到這個轉變和標準的細節。如果正如你所說,那麼可以避免手動停止。但無論如何,該字符串遲早會被複制,因爲在地圖中新條目被存儲爲鍵/值對。如果您的密鑰類型是std :: string,則密鑰是字符串的副本。 – Matthias247

+4

您應該仍然可以執行'_foo_map [f-> name] = std :: move(f)',因爲即使'f'首先變爲r值引用,它也不會被移動直到有東西被移入,這是'_foo_map [f-> name]'的結果' – Altainia

1

你可以做add採取f參照,避免了不必要的複製/移動(同樣的複製/移動它象垃圾一樣清除f->name):

void add(const std::string& name, std::unique_ptr<Foo> && f) 
{ 
    _foo_map[name] = std::move(f); 
} 

void process(std::unique_ptr<Foo> f) 
{ 
    add(f->name, std::move(f)); 
} 

foo_map[name]必須operator=之前評估被稱爲上,所以即使name指的是取決於f的東西,也沒有問題。

+0

的慣用方式,它不能解決'add()'一定不能接觸名稱的問題'f'已被消耗。它已經是'add(f-> name,std :: move(f))'這行是邪惡的,必須修復。 – cmaster

+0

@cmaster'f'在'add'內調用'operator ='之前不消耗。 'std :: move'不會消耗任何東西。這是保證讀取'name'後,因爲'operator ='不能被調用,直到它的操作數已被評估。 –

+0

問題是,使用兩個糾纏參數調用'add()':修改一個(移動構造/賦值消耗)會導致另一個狀態發生更改(引用變爲無效)。如果你希望你的代碼保持可維護性,這永遠不會好,必須不惜一切代價避免。 – cmaster