Ошибка в создании шаблона перед перегрузкой

Учитывая следующий код

#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) работает...

2 ответа

Решение

(Извиняюсь за отсутствие правильной стандартной терминологии, работаю над этим...)

когда 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 имеет автоматическое вычитание типа возврата, следовательно, создается лямбда и применяется вышеупомянутое правило.

Другие вопросы по тегам