Search code examples
c++c++11templatesoverloadingtemplate-instantiation

Error in template instantiation before overloading


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...


Solution

  • (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).