3

Given the following code

#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

When I try to compile this I get the error saying that I am not allowed to modify a const value in the lambda in main. This means that somehow the templates are both being instantiated in the class and this is causing a hard error since the error is in the body of the lambda.

What are the rules regarding this? Why does overload resolution try to instantiate a template that will never be called? The const one should never be called here so why does it try to fully instantiate it?

However strange thing here is that when I change the definitions to return by decltype(auto) and add the code to do the same thing as the trailing return types suggest, I don't see an error. Indicating that the templates are not fully being instantiated?

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);
}

I guess the compiler doesn't know which function to call before instantiating at least the signature with the passed function. But that doesn't explain why the decltype(auto) version works...

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Curious
  • 20,870
  • 8
  • 61
  • 146

2 Answers2

4

(Apologies for the lack of correct Standard terminology, working on it...)

When something.foo is being invoked, all possible overloads have to be taken into consideration:

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&>())) {}

In order to check whether an overload is viable, the trailing decltype(...) needs to be evaluated by the compiler. The first decltype will be evaluated without errors and it will evaluate to void.

The second one will cause an error, because you're attempting to invoke the lambda with a const T&.

Since the lambda is unconstrained, the error will occur during instantiation of the lambda's body. This happens because (by default) lambdas use automatic return type deduction, which requires instantiation of the lambda's body.

Therefore, the non-viable overload will therefore cause a compilation error instead of getting SFINAEd out. If you constrain the lambda as follows...

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

...no error will occur, as the overload will be deemed non-viable through SFINAE. Additionally, you will be able to detect if the lambda invocation is valid for a particular type (i.e. does T support operator++()?) from Something::foo.


When you change your return type to decltype(auto), the return type is deduced from the body of the function.

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);
}

As your something instance is non-const, the non-const qualified overload will be taken here. If your main was defined as follows:

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

You would get the same error, even with decltype(auto).

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • "*the error will occur during instantiation*" as opposed to when? Is substitution its own step? – Ryan Haining Oct 11 '17 at 14:29
  • 1
    @RyanHaining: during the substitution step, the `decltype(...)` trailing return type instantiates the lambda's body... which causes an error. – Vittorio Romeo Oct 11 '17 at 14:40
  • What is the suggested way to avoid this problem here? Just switch to `decltype(auto)`? I would like the `foo` function to be SFINAE friendly with the return type and also have user code work – Curious Oct 11 '17 at 14:54
  • 1
    @Curious: if you want `foo` to be SFINAE-friendly, then you need the trailing `decltype(...)`. It's the lambda that needs to be constrained, unfortunately... I've encountered the same issue in the past - the provider of the lambda needs to properly constrain it. – Vittorio Romeo Oct 11 '17 at 14:56
  • I think this answer is not correct, because it does not address the crux of the problem, that is if the lambda instantiation is in the immediate context of the substitution or not ... – Massimiliano Janes Oct 11 '17 at 15:05
  • @MassimilianoJanes: I improved the answer. Do you still think that it is "incorrect"? I would agree with you if you think it's not detailed/precise, but I still think that the explanation is correct. – Vittorio Romeo Oct 11 '17 at 15:12
  • ok :) but I still believe that it's worth mentioning precisely when an error occurring during substitution is not just a 'deduction failure' ... – Massimiliano Janes Oct 11 '17 at 15:53
1

actually, I think the gist of the problem is that

Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure. [ Note: The substitution into types and expressions can result in effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such effects are not in the “immediate context” and can result in the program being ill-formed. — end note ]

so, the question is, should the instantiation of a lambda triggered during deduction of its return type considered in its immediate context ?

for example, if the lambda return type is made explicit:

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

the code compiles ( no sfinae, it's just that the non const is a best match ).

but, the OP's lambda has automatic return type deduction hence the lambda is instantiated and the aforementioned rule applies.

Massimiliano Janes
  • 5,524
  • 1
  • 10
  • 22
  • This makes sense, but please note that the lambda is not "SFINAE-friendly" anymore. You might want to check if `++val` is actually a valid operation for `val` from `Something::foo`. If you just say `-> void`, it might look like a valid operation even if it isn't, producing an hard error like in the OP's example. – Vittorio Romeo Oct 11 '17 at 15:08
  • @VittorioRomeo ... uhm, but it doesn't give an hard error with the OP code, that's the point ... – Massimiliano Janes Oct 11 '17 at 15:12
  • this is what I find problematic: https://wandbox.org/permlink/g7fNJgknnVXj7vrt - I would want `/* fallback */` to be chosen there, but `-> void` chooses another overload and **causes an hard error**. Properly constraining the lambda would choose the fallback. – Vittorio Romeo Oct 11 '17 at 15:16
  • In the above link, changing `-> void` with `-> decltype(++val, void())` prevents the "hard error" and invokes the `/* fallback */` overload of `Something::foo`. – Vittorio Romeo Oct 11 '17 at 15:17
  • @VittorioRomeo, ok got it now ( but that's not the OP code ... ) – Massimiliano Janes Oct 11 '17 at 15:18