Учитывая следующий код
#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 в лямбда-выражении в main. Это означает, что каким-то образом оба экземпляра создаются в классе, и это вызывает серьезную ошибку, поскольку ошибка находится в теле лямбды.
Каковы правила относительно этого? Почему разрешение перегрузки пытается создать экземпляр шаблона, который никогда не будет вызываться? Константный никогда не должен вызываться здесь, так почему он пытается полностью его создать?
Однако странная вещь в том, что когда я меняю определения 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 (auto) работает …
(Извиняюсь за отсутствие правильной стандартной терминологии, работаю над этим …)
когда 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&
,
Поскольку лямбда не ограничена, ошибка будет возникать во время создания тела лямбды. Это происходит потому, что (по умолчанию) лямбды используют автоматическое возвращение типа вычета, что требует создания экземпляров лямбда-тела.
Следовательно, нежизнеспособная перегрузка будет вызывать ошибку компиляции, а не выводить SFINAEd. Если вы ограничите лямбду следующим образом …
something.foo([](auto& val) -> decltype(++val, void()) {
++val;
});
…ошибки не возникнет, так как перегрузка будет считаться нежизнеспособной через SFINAE. Кроме того, вы сможете определить, действителен ли лямбда-вызов для определенного типа. (то есть делает 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)
,
на самом деле, я думаю, суть проблемы в том, что
Только недопустимые типы и выражения в непосредственный контекст типа функции и ее типов параметров шаблона может привести к ошибке вывода. [Примечание: замена на типы и выражения может привести к таким эффектам, как создание экземпляров специализаций шаблонов классов и / или специализаций шаблонов функций, генерация неявно определенных функций и т. Д.
«Непосредственный контекст» и может привести к некорректной работе программы. — конец примечания]
Итак, вопрос заключается в том, должна ли инстанцирование лямбды, запускаемой во время удержания возвращаемого типа, рассматриваться в непосредственном контексте?
например, если лямбда-тип возврата сделан явным:
something.foo([](auto& val) -> void {
++val;
});
код компилируется (без sfinae, просто неконстантность является лучшим соответствием).
но лямбда OP имеет автоматическое вычитание типа возврата, следовательно, создается лямбда и применяется вышеупомянутое правило.