I'm working with templates and type deduction in C++ and encountering a type deduction failure when using std::span
but not with raw pointers. Below is a simplified version of my code:
#include <span>
#include <vector>
template <typename T>
void f1(std::span<const T> param)
{}
template <typename T>
void f2(const T* param)
{}
int main()
{
std::vector<int> v{1,2,3};
std::span<int> s{v};
// Uncommenting this line causes a compilation error:
// cannot deduce a type for 'T' that would make 'const T' equal 'int'
// f1(s);
int x = 10;
int* px = &x;
const int* z = px;
f2(px); // Works fine
f2(z); // Works fine
}
When I uncomment the f1(s)
call, I receive a compilation error stating that the compiler cannot deduce a type for T
that would make const T
equal int
. However, similar template functions for pointers, like f2
, compile without any issues when passing both int*
and const int*
.
Why does this error occur with std::span
but not with pointers?
T
in std::span<const T>
cannot be deduced from std::span<int>
because int
is not const
.
There does exist an implicit conversion from any std::span<U>
to std::span<const U>
, but such implicit conversions are not considered during template argument deduction.
See also Why does the implicit type conversion not work in template deduction?
const T* param
is a special case because there are dedicated rules for when deduction wouldn't give you an exact match for const T*
, and const
has to be added (via qualification conversion) ([temp.deduct.call] p4):
In general, the deduction process attempts to find template argument values that will make the deduced A identical to [the argument type] A (after the type A is transformed as described above). However, there are three cases that allow a difference:
- [...]
- The transformed A can be another pointer or pointer-to-member type that can be converted to the deduced A via a function pointer conversion and/or qualification conversion.
- [...]
In your case, the argument type A is int*
, which can be converted to const int*
and thus match const T*
.
In most cases, you should not write templates that take a std::span<T>
due to these deduction issues.
The outcome will always be somewhat confusing and unergonomic.
Since f1
is a template anyway, you don't lose anything by writing:
template <std::ranges::contiguous_range R>
void f1(R&& range);
This would also let you pass in say, std::vector
and std::string_view
without converting them to a span first.
Note: In most cases, a contiguous range is overkill and you could get away with a random access range, or some weaker requirement.