Search code examples
c++templatestype-deductionstd-span

Why can I not pass std::span<int> to a function template taking std::span<const T>?


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?


Solution

  • 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*.

    Solution

    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.