c++constantslanguage-lawyermembertemplate-argument-deduction

Why is decltype(member) not const while decltype((member)) is for const object and how does it impact function calls


I don't understand the behavior of the following snippet, which is an MRE derived from actual code:

#include <iostream>
#include <type_traits>

// It merely returns its input, which is a reference
// could mutualize code for const and non const member function below
template <typename T>
T& Getter(T& i) {
    std::cout << std::boolalpha << "Getter, const int: "
              << std::is_same<T, const int>::value
              << " / int: " << std::is_same<T, int>::value << '\n';
    return i;
}

struct S {
    int val = -1;

    // Read/write access to the data for mutable object
    int& Get() {
        std::cout << std::boolalpha << "non-const Get, val const int: "
                  << std::is_same<decltype(val), const int>::value
                  << " / int: " << std::is_same<decltype(val), int>::value
                  << '\n';
        std::cout << std::boolalpha << "const Get, (val) const int&: "
                  << std::is_same<decltype((val)), const int&>::value
                  << " / int&: " << std::is_same<decltype((val)), int&>::value
                  << '\n';
        return Getter(val);
    }

    // Read-only access to the data for const object
    int const& Get() const {
        std::cout << std::boolalpha << "const Get, val int const: "
                  << std::is_same<decltype(val), const int>::value
                  << " / int: " << std::is_same<decltype(val), int>::value
                  << '\n';
        std::cout << std::boolalpha << "const Get, (val) int const&: "
                  << std::is_same<decltype((val)), const int&>::value
                  << " / int&: " << std::is_same<decltype((val)), int&>::value
                  << '\n';
        return Getter(val);
    }
};

int main() {
    std::cout << "---------------------\nconst\n";
    S const ks;
    std::cout << ks.Get() << '\n';
}

Live

The output is as follows:

---------------------
const
const Get, val const int: false / int: true
const Get, (val) const int&: true / int&: false
Getter, const int: true / int: false

ks is const thus I'm calling S::Get() const: OK
Inside S::Get() const decltype(val) is int but decltype((val)) is const int&: I find it peculiar.
This has been addressed in:

This behavior is clearly defined in the standard (see the human-friendly version, especially 2-b)).

But I don't get the rationale behind this.

Getter is called on val, passed by reference, but T is deduced as const int, as if the argument was a reference to const lvalue (const int &).

I would have expected that it would be decltype(val) which will the "input" of the template argument deduction.
In most situations, it seems that the difference is invisible, but when dealing with constness it's becoming critical, as in the provided snippet.

What detail am I missing? When passing an expression (possibly a single variable) to a function, what is the type of the expression "seen" by the function (which will impact template argument deduction but, also, possibly, overload resolution)?

It's obviously a basic question, but these subtleties nether occurred to me so far. The linked posts above are revolving about it, but I'm still missing what is actually happening at the function call.


Solution

  • What detail am I missing ? When passing an expression (possibly a single variable) to a function, what is the type of the expression "seen" by the function (which will impact template argument deduction but, also, possibly, overload resolution)? I'm still missing what is actually happening at function call. Getter is called on val, passed by reference, but T is deduced as const int, as if the argument was a reference to const lvalue (const int &).

    You're missing that when val is passed as a function argument in the function call Getter(val) is an expression with a value category lvalue with type const int. Note also that in the call Getter(val) the expression val has type const int because we're inside a const member function.

    This is why T is deduced as const int.


    Here is a contrived example to make this clear:

    template<typename T> void f(T&){}
    int main()
    {
        const int i = 0;
        f(i); //T is deduced as const int. Note the argument `i` in `f(i)` is an expression with value category lvalue and is of type `const int`
    }