2017-10-11 79 views
3

考慮下面的代碼錯誤模板實例超載

#include <type_traits> 
#include <utility> 

template <typename T> 
class Something { 
public: 
    template <typename F> 
    auto foo(F&&) 
     -> decltype(std::declval<F>()(std::declval<T&>())) {} 
    template <typename F> 
    auto foo(F&&) const 
     -> decltype(std::declval<F>()(std::declval<const T&>())) {} 
}; 

int main() { 
    auto something = Something<int>{}; 
    something.foo([](auto& val) { 
     ++val; 
    }); 
} 

https://wandbox.org/permlink/j24Pe9qOXV0oHcA8

當我嘗試編譯此我得到的錯誤,說不準我在修改的拉姆達一個const值之前主要。這意味着模板在某種程度上都是在類中實例化的,並且由於錯誤出現在lambda體中,所以導致了一個硬性錯誤。

這是什麼規則?爲什麼重載決議嘗試實例化一個永遠不會被調用的模板? const函數不應該在這裏被調用,爲什麼它會試圖完全實例化它?

然而,奇怪的是,當我通過decltype(auto)更改定義並返回並添加代碼以執行與尾隨返回類型相同的操作時,我看不到錯誤。表明模板沒有完全實例化?

template <typename F> 
decltype(auto) foo(F&& f) { 
    auto t = T{}; 
    f(t); 
} 
template <typename F> 
decltype(auto) foo(F&& f) const { 
    const auto t = T{}; 
    f(t); 
} 

我猜編譯器不知道在使用傳遞函數實例化至少簽名之前要調用哪個函數。但是,這並不能解釋爲什麼decltype(自動)版本的作品...

回答

4

(道歉缺乏正確的標準術語,它的工作...)

當被調用something.foo,所有可能的過載必須被考慮:

template <typename F> 
auto foo(F&&) 
    -> decltype(std::declval<F>()(std::declval<T&>())) {} 

template <typename F> 
auto foo(F&&) const 
    -> decltype(std::declval<F>()(std::declval<const T&>())) {} 

爲了檢查過載是否是可行的,後decltype(...)需要由編譯器進行評估。第一個decltype將被評估沒有錯誤,它將評估爲void

第二個會導致一個錯誤,因爲你試圖用const T&來調用lambda。

由於lambda是無約束的,所以在lambda體的實例化過程中會發生錯誤。發生這種情況是因爲(默認情況下)lambda使用自動返回類型扣減,這需要lambda身體的實例化。

因此,不可行的重載將因此導致編譯錯誤,而不是獲取SFINAEd。如果約束拉姆達如下...

something.foo([](auto& val) -> decltype(++val, void()) { 
    ++val; 
}); 

...不會發生錯誤,因爲超載將被視爲非可行通過SFINAE。此外,您將能夠檢測lambda調用是否對特定類型有效(即T支持operator++()?)Something::foo


當你改變你的返回類型decltype(auto),返回類型從函數體推斷。

template <typename F> 
decltype(auto) foo(F&& f) { 
    auto t = T{}; 
    f(t); 
} 

template <typename F> 
decltype(auto) foo(F&& f) const { 
    const auto t = T{}; 
    f(t); 
} 

正如你something實例是非const,非const合格超載將在這裏拍攝。如果您main定義如下:

int main() { 
    const auto something = Something<int>{}; 
    something.foo([](auto& val) { 
     ++val; 
    }); 
} 

你會得到同樣的錯誤,即使decltype(auto)

+0

「*實例化過程中會出現錯誤*」而不是何時?替代是自己的一步? –

+1

@RyanHaining:在替換步驟中,'decltype(...)'尾隨返回類型實例化lambda的body ...導致錯誤。 –

+0

這裏有什麼建議避免這個問題的方法?只需切換到'decltype(auto)'?我希望'foo'函數對於返回類型是SFINAE友好的,並且還有用戶代碼工作 – Curious

1

實際上,我認爲這個問題的要點是,

只有無效的類型和表達式中的函數類型和它的模板參數類型的直接背景可能導致扣失敗。 [注意:對類型和表達式的替換可能會導致類效應,例如類模板特化和/或函數模板特化的實例化,隱式定義函數的生成等。這些效應不在 「直接上下文」中可能會導致程序不合格。 - 結束註釋]

所以,問題是,如果lambda的實例化在其直接上下文中考慮的返回類型被扣除時觸發?

例如,如果拉姆達返回類型作出了明確:

something.foo([](auto& val) -> void { 
    ++val; 
}); 

代碼編譯(無SFINAE,它只是非const爲最佳匹配)。

但是,OP的lambda具有自動返回類型演繹,因此lambda被實例化並應用上述規則。

+0

這是有道理的,但請注意,lambda不再是「SFINAE友好」的。您可能想要檢查'++ val'是否是'Something :: foo'的'val'的有效操作。如果你只是說' - > void',它可能看起來像一個有效的操作,即使它不是,產生了一個像OP中例子那樣的硬錯誤。 –

+0

@VittorioRomeo ...呃,但它並沒有給OP代碼帶來一個硬錯誤,這是關鍵...... –

+0

這是我發現的問題:https://wandbox.org/permlink/g7fNJgknnVXj7vrt - 我會希望在那裏選擇'/ * fallback * /',但' - > void'選擇另一個重載並且**導致硬錯誤**。正確地約束lambda將選擇回退。 –