2014-10-01 42 views
15

香草薩特的回到基礎! CppCon的現代C++演示文稿討論了傳遞參數的不同選項,並比較了它們的性能與易於編寫/教學。 「高級」選項(在所有情況下提供最佳的性能測試,但太難對於大多數開發者編寫)是完美轉發,給定(PDF, pg. 28)的例子:完美轉發設置器上的'enable_if`約束是什麼?

class employee { 
    std::string name_; 

public: 
    template <class String, 
       class = std::enable_if_t<!std::is_same<std::decay_t<String>, 
                std::string>::value>> 
    void set_name(String &&name) noexcept(
     std::is_nothrow_assignable<std::string &, String>::value) { 
     name_ = std::forward<String>(name); 
    } 
}; 

的示例使用轉發的模板函數參考,其中使用enable_if約束了模板參數String。但是這個約束似乎是不正確的:似乎是說這種方法只有在String類型不是std::string時纔可以使用,這是沒有意義的。這意味着std::string成員可以使用設置,除std::string之外的任何值。

using namespace std::string_literals; 

employee e; 
e.set_name("Bob"s); // error 

一種解釋我考慮的是,有一個簡單的錯字和約束的用意是std::is_same<std::decay_t<String>, std::string>::value而不是!std::is_same<std::decay_t<String>, std::string>::value。然而,這意味着制定者不適用於(例如)const char *,並且顯然它打算使用這種類型,因爲這是在演示文稿中測試的案例之一。

在我看來,正確的約束更像是:

允許任何可以被分配給成員與二傳手使用。

我得到了正確的約束嗎?還有其他可以改進的地方嗎?對原始約束是否有任何解釋,或許它是脫離了語境?


另外我想知道這個聲明中複雜的,「不可測試」的部分是否真的有益。因爲我們沒有使用過載,我們可以簡單地依靠正常的模板實例:

template <class String> 
void set_name(String &&name) noexcept(
    std::is_nothrow_assignable<decltype((name_)), String>::value) { 
    name_ = std::forward<String>(name); 
} 

當然還有的在noexcept是否真正重要的一些爭論,有的說不要擔心太多關於它除了移動/交換原語:

template <class String> 
void set_name(String &&name) { 
    name_ = std::forward<String>(name); 
} 

也許有了概念,限制模板並不是不合理的困難,只是爲了改進錯誤信息。

template <class String> 
    requires std::is_assignable<decltype((name_)), String>::value 
void set_name(String &&name) { 
    name_ = std::forward<String>(name); 
} 

這仍然會對它不能是虛擬的缺點,它必須在頭(雖然希望模塊將最終渲染沒有實際意義),但這似乎相當受教。

+0

嗯,房間裏有人說它的約束不正確,不接受字符串文字 - 也許我們可以cc @HowardHinnant?它也不適合我。 [視頻的談話](http://youtu.be/xnqTKD8uD64?t=1h15m21s) – dyp 2014-10-01 19:26:59

+0

@PiotrS .: MSVC沒有別名,所以我忘了:/ – 2014-10-01 21:10:34

+0

@MooingDuck嘿,VS2013支持別名和他們的stdlib工具C++ 14 type_trait別名。 – bames53 2014-10-01 21:17:21

回答

3

我認爲你所擁有的可能是正確的,但爲了不寫一個簡單的「我同意」的答案,我會提出這個建議,而不是根據正確的類型檢查分配 - 無論是lval中,RVAL,常量,不管:

template <class String> 
auto set_name(String&& name) 
-> decltype(name_ = std::forward<String>(name), void()) { 
    name_ = std::forward<String>(name); 
} 
+0

當你添加'noexcept'子句時,它會變得有些醜陋。 – 2015-04-12 17:55:58

4

一種解釋我考慮的是,有一個簡單的錯字和約束的用意是std::is_same<std::decay_t<String>, std::string>::value而不是!std::is_same<std::decay_t<String>, std::string>::value

是的,這是呈現在屏幕上的權利限制是is_same,不!is_same。看起來你的幻燈片上有一個錯字。


但是這將意味着制定者不與,例如工作,const char *

是的,我相信這是故意的。當文字等"foo"一個字符串傳遞給接受通用參考一個函數,那麼推導出的類型不是指針(因爲數組衰減到由值在模板參數捕獲僅當指針),更確切地說,它是一個const char(&)[N]。這就是說,每到set_name調用與字符串字面不同長度的將實例化一個新的set_name專業化,如:

void set_name(const char (&name)[4]); // set_name("foo"); 
void set_name(const char (&name)[5]); // set_name("foof"); 
void set_name(const char (&name)[7]); // set_name("foofoo"); 

約束應該定義通用參考,使其接受並推導出只要麼std::string類型對於左值參數,或cv-std::string&用於左值參數(這就是爲什麼它在std::is_same條件下與std::string比較之前爲std::decay)。


它顯然是打算用這種類型考慮到這是在展示測試的情況下一個工作。

我認爲測試版(4)沒有限制(注意它被命名爲String&& +完美轉發),所以它可能是那樣簡單:

template <typename String> 
void set_name(String&& name) 
{ 
    _name = std::forward<String>(name); 
} 

,這樣,當一個字符串字面它在函數調用之前並不構造std::string實例,就像非模板化版本會這樣做(不必要地在被調用者的代碼中分配內存只是爲了構建一個std::string臨時文件,最終將被移動到可能的預分配目標,如_name):

void set_name(const std::string& name); 
void set_name(std::string&& name); 

在我看來,正確的約束更像是:​​

沒有,因爲我寫的,我不認爲目的是爲了約束set_name接受類型可分配std::string。再一次 - 這原來std::enable_if是那裏有一個單一的實現set_name功能採取的通用參考只接受std::string的右值和左值(僅此而已,雖然這是一個模板)。在您的std::enable_if版本中,傳遞任何不可分配給std::string的東西都會產生錯誤,無論試圖這樣做時是否存在常量。請注意,我們最終可能只是移動該name參數_name如果是非const右值引用,因此檢查可轉讓的時候,我們不使用SFINAE排除有利於其他超負荷的過載分辨率功能,除了從的情況下毫無意義。


什麼就完美了轉發二傳手正確enable_if約束?

template <class String, 
      class = std::enable_if_t<std::is_same<std::decay_t<String>, 
               std::string>::value>> 

或根本沒有約束可言,如果它不會造成任何性能損失(就像傳遞字符串文字)。

+0

*「我不認爲其意圖是將set_name限制爲接受可分配給'std :: string'的類型」*如果我們將它與其他解決方案進行比較,它應該至少可以隱式轉換爲'std :: string '以允許與其他解決方案相同的轉換。但是這看起來不合適,因爲我們不希望在分配之前執行該隱式轉換。 – dyp 2014-10-01 21:22:04

+0

@dyp:這就是爲什麼類型僅限於'std :: string's,而不是類型* assignable *到字符串的一個很好的理由,那是什麼意思? – 2014-10-01 21:36:37

+0

嗯,我的意思是其他的解決方案允許'x.set_name(「hello」);',而這個不 - '「hello」'不是'std :: string',但它可以隱式轉換爲'std :: string &&'和'std :: string const&'和'std :: string'。 – dyp 2014-10-01 21:42:32

1

我想在一個評論,以適應這些想法,但是他們將不適合。正如我在上面的評論和赫伯的偉大的演講中提到的那樣,我想這樣寫。

對不起,是遲到了。我已經審查了我的筆記,事實上我對向Herb建議選項4的原始約束減去錯誤的!感到內疚。沒有人是完美的,因爲我妻子肯定會證實,至少我所有。 :-)

提醒:香草的點(我同意)是開始與簡單的C++ 98/03的建議

set_name(const string& name); 

和僅在需要時有移動。選項#4有相當的動作。如果我們正在考慮選項#4,我們只能在應用程序的關鍵部分對負載,存儲和分配進行計數。我們需要儘可能快地將name分配給name_。如果我們在這裏,代碼可讀性遠不如性能重要,儘管正確性仍然是重要的。

根本沒有提供約束(對於選項#4),我認爲有些不正確。如果一些其他的代碼試圖與它是否能夠調用employee::set_name來約束自己,就可能得到錯誤的答案,如果set_name完全不受限:

template <class String> 
auto foo(employee& e, String&& name) 
-> decltype(e.set_name(std::forward<String>(name)), void()) { 
    e.set_name(std::forward<String>(name)); 
    // ... 

如果set_name不受約束且String推導出一些完全無關的類型X,對foo的上述限制在過載集合中不正確地包括foo的此實例化。正確性仍然是國王...

如果我們想要分配一個字符到name_怎麼辦?說A。應該允許嗎?它應該是快速的邪惡?

e.set_name('A'); 

那麼,爲什麼不呢?! std::string便是這樣一個賦值操作符:

basic_string& operator=(value_type c); 

但是請注意,有沒有相應的構造函數:

basic_string(value_type c); // This does not exist 

因此is_convertible<char, string>{}false,但is_assignable<string, char>{}true

這是一個邏輯錯誤嘗試設置與char一個string的名字(除非你想文檔添加到employee說是這麼說的)。因此,即使原來的C++ 98/03的實施並沒有讓語法:

e.set_name('A'); 

它確實允許在一個不太有效的方式相同的邏輯操作:

e.set_name(std::string(1, 'A')); 

而且我們正在處理選項#4,因爲我們迫切希望儘可能地優化這個東西。

由於這些原因,我認爲is_assignable是限制此功能的最佳特徵。風格上我發現Barry's technique for spelling this constraint quite acceptable。因此這是我投票的地方。

另請注意,employeestd::string這裏只是Herb的演講中的例子。他們是處理在你的代碼的替代品。此建議旨在推廣到您必須處理的代碼。

+0

這對於需要一個約束來防止將一個int分配給'name_'很好,但是在我看來,這是一個特殊情況,因爲應用了從int到'char'的隱式轉換。理想情況下,字符串類可以在這種情況下禁止隱式轉換,但我認爲至少我們可以在'employee :: set_name'中執行某些操作。那麼[這](http://coliru.stacked-crooked.com/a/b25e0861fbeb7c00)?或者,也許使用大括號這是太微妙,因此就像SFINAE約束一樣無法達到? – bames53 2015-04-12 18:16:52

+0

@ bames53:在我的例子中,我的錯誤是使用'int'作爲例子「other type」。我應該使用'class rut​​abaga'並演示'rutabaga'沒有轉換爲'std :: string'。然而,有時候'員工'吃'大頭菜',所以可以想象'foo'在'員工'和'大頭菜'上超載,或者'大頭菜'隱式地轉換爲'蔬菜'或模板化的東西代表「蔬菜」的參數。即使用擴展的SFINAE,*任何*表達式都可以用作約束,甚至可以使用'e.set_name'。 – 2015-04-12 20:45:02

+0

但是,如果用'is_assignable'約束,'String = int'的'foo'將仍然在重載集中,因爲'int'(和'double'等)可以轉換爲'char'。 – 2015-04-12 22:14:28