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';
}
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.
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 onval
, passed by reference, butT
is deduced asconst 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`
}