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.
An std::span<T>
is implicitly convertible to a std::span<const T>
. So, if the T
from your function was an int
, your parameter of std::span<int>
could be converted to the std::span<const int>
the function would want. However, that doesn't mean the compiler will be able to figure that out in its template argument deduction process.
The compiler's template argument deduction process doesn't care what is convertible to what. It wants some argument for T
such that std::span<const T>
is the same as std::span<int>
, and as it said in the error message, there wasn't one, because there isn't. If T
is an int
, as I say above, std::span<const T>
would be a std::span<const int>
, implicitly convertible from a std::span<int>
. But it is still not exactly the same type, like the compiler wants, and so that option is excluded.
You can work around this two ways:
f1<int>(s)
instead of f1(s)
. This avoids any template argument deduction.s
a std::span<const int>
instead of being a std::span<int>
as it currently is. Now there does exist a type T
such that std::span<const T>
is a std::span<const int>
, and the compiler will correctly identify and appropriate it.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With