Reputation: 3186
Considering the following cases, what is the effect of adding a const
as part of the template argument?
void func()
takes a parameter of type T
by copy#include <iostream>
#include <boost/type_index.hpp>
template<typename T>
void func(T val_)
{
using boost::typeindex::type_id_with_cvr;
std::cout<< "type of `val_` is `" << type_id_with_cvr< decltype(val_) >().pretty_name() << "`" <<std::endl;
}
int main()
{
int *f = new int(2);
func<int>(2); // type of `val_` is `int` - OK
func<int*>( f ); // type of `val_` is `int*` - OK
func<const int*>( f ); // type of `val_` is `int const*` - OK
func<const int* const>( f ); // type of `val_` is `int const* const` - OK
}
No questions for the first case... It's clear what is going on.
void func()
takes a parameter of type T
by reference#include <iostream>
#include <boost/type_index.hpp>
template<typename T>
void func(T &val_)
{
using boost::typeindex::type_id_with_cvr;
std::cout<< "type of `val_` is `" << type_id_with_cvr< decltype(val_) >().pretty_name() << "`" <<std::endl;
}
int main()
{
int *f = new int(2);
func<int>(2); // error - OK
func<int*>( f ); // type of `val_` is `int*&` - OK
func<const int*>( f ); // error: no known conversion from 'int *' to 'const int *&' - OK
func<const int* const>( f ); // type of `val_` is `int const* const&` - ???
}
void func()
takes a parameter of type T
by forwarding/universal reference#include <iostream>
#include <boost/type_index.hpp>
template<typename T>
void func(T &&val_)
{
using boost::typeindex::type_id_with_cvr;
std::cout<< "type of `val_` is `" << type_id_with_cvr< decltype(val_) >().pretty_name() << "`" <<std::endl;
}
int main()
{
int *f = new int(2);
func<int>(2); // type of `val_` is `int&&` - OK
func<int*>( f ); // error: no known conversion from 'int *' to 'int *&&' - OK
func<const int*>( f ); // type of `val_` is `int const*&&` - ???
func<const int* const>( f ); // type of `val_` is `int const* const&&` - ???
}
Question #1: In Case #2, why does func<const int* const>( f );
get accepted just by adding the rhs const
?
Question #2: In Case #3, why are the last two calls to func
accepted just by adding a const
?
Question #3: In Case #3, the benefit of using a forwarding reference is that if the function argument is an lvalue, then the type of val_
will be an lvalue reference. If the argument is an rvalue, then the type of val_
will be an rvalue. So, passing f
, which is an lvalue, should make T
an int*&
. This is indeed the case when you don't specify the template argument int*
at function call. However, when you specify the template argument int*
, the result is a int*&&
. Why?
Upvotes: 1
Views: 88
Reputation: 30639
The type of f
is int*
. Only an int*&
or int* const&
can bind directly to that.
For case #2, int*
and int const*
are different types, so a reference to int cont*
cannot bind directly to f
. A conversion from int*
to int const*
exists, but an lvalue-reference-to-non-const can't bind to the resulting object because it is an rvalue. An lvalue-reference-to-const can bind to an rvalue though, so, since there's an implicit conversion from int*
to int const* const
, when you add const
to the type a temporary int const* const
can be created, and the reference can bind to it.
This is the same reason the following works:
void foo(const std::string& str) {}
int main() { foo("hello"); }
While the following doesn't:
void foo(std::string& str) {}
int main() { foo("hello"); }
For case #3, int*
and int const*
are different types. Since implicit conversions exist from int*
to both int const*
and int const* const
, temporary copies are created. Those temporary copies are rvalues, so rvalue-references can bind to them no problem. When you use int*
as the template parameter, the type already matches, so no temporary copy gets made. Since f
is an lvalue, an rvalue-reference can't bind to it and you get an error.
To answer your last question that you edited in: forwarding references only arise if you let template parameters be deduced. It's the template parameter deduction and reference collapsing rules that make them work, so you'll never get a forwarding reference if you specify explicit template parameters.
For example, given the following function template:
template <typename T>
void foo(T&& obj) {}
If you pass an lvalue of type int
(i.e. foo(some_int_var)
), T
will be deduced to be int&
, so the type of obj
is int& &&
. After the reference collapsing rules are applied, that collapses toint&
: an lvalue-reference-to-int. On the other hand, if you pass an rvalue of type int
(i.e. foo(42)
), T
will be deduced to be int
, so the type of obj
will be int&&
: rvalue-reference-to-int.
Upvotes: 1
Reputation: 19981
In case #2, the first question should be: why is func<const int *>(f)
bad? After all, usually you can use an int *
where a const int *
is wanted. The reason why it fails is that even though an int *
is-a const int *
(i.e., a thing you can dereference to get an int
is a thing you can dereference to get a const int
), a reference to an int *
is not a reference to a const int *
because you can assign "through" it. If you were allowed to call func<const int *>(f)
then some different version of func
could e.g. assign something that really is a const int *
to f
, after which you'd be able to modify what it points to, which would be Bad.
And now it should be obvious why the second const
fixes it: you can't change the value of f
by passing it by const
reference.
Case #3 is somewhat similar. Again, the first question to ask isn't why your third and fourth cases work but why your second case fails. It fails because you aren't allowed to take an rvalue reference (&&
) to an lvalue (a thing with a name, like f
). Why not? Because one of the reasons for these things is to be able to get different behaviour depending on whether the thing being passed is a temporary (and hence safe to clobber) or not, and the way that's done is to forbid conversion of an lvalue of type T
to an rvalue reference of type T &&
. (So then you can write one function that takes a T &
and another that takes a T &&
, and make the latter do the more efficient clobbery thing.)
But just about anything you do to that lvalue will stop it being an lvalue and make the invocation of func
legal again. For instance, you could add 0 to it. Or you could cast it to a different type. And, aha!, this is what is happening in your third and fourth examples in case #3: what's being passed to func
is in effect not f
itself but something like const_cast<const int *>(f)
, and that's no longer an lvalue and all is well.
Upvotes: 1