Search code examples
c++clangc++23compiler-bugstd-span

Can you convert int*[N] to std::span<const int * const>?


I have the following code:

#include <span>

int* pointers[3];
std::span<const int* const> s = pointers;

GCC accepts this, but Clang (compiling libstdc++) rejects it, stating:

<source>:4:29: error: no viable conversion from 'int *[3]' to 'std::span<const int *const>'
    4 | std::span<const int* const> s = pointers;
      |                             ^   ~~~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:230:7: note: candidate constructor not viable: no known conversion from 'int *[3]' to 'const span<const int *const> &' for 1st argument
  230 |       span(const span&) noexcept = default;
      |       ^    ~~~~~~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:192:2: note: candidate template ignored: could not match 'const int *' against 'int *'
  192 |         span(type_identity_t<element_type> (&__arr)[_ArrayExtent]) noexcept
      |         ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:199:2: note: candidate template ignored: could not match 'array<_Tp, _ArrayExtent>' against 'int *[3]'
  199 |         span(array<_Tp, _ArrayExtent>& __arr) noexcept
      |         ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:206:2: note: candidate template ignored: could not match 'array<_Tp, _ArrayExtent>' against 'int *[3]'
  206 |         span(const array<_Tp, _ArrayExtent>& __arr) noexcept
      |         ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:218:2: note: candidate template ignored: constraints not satisfied [with _Range = int *(&)[3]]
  218 |         span(_Range&& __range)
      |         ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:213:8: note: because '!is_array_v<remove_cvref_t<int *(&)[3]> >' evaluated to false
  213 |           && (!is_array_v<remove_cvref_t<_Range>>)
      |               ^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/15.0.0/../../../../include/c++/15.0.0/span:238:2: note: candidate template ignored: could not match 'span<_OType, _OExtent>' against 'int *[3]'
  238 |         span(const span<_OType, _OExtent>& __s) noexcept
      |         ^

See Compiler Explorer

I was under the impression that std::span can be constructed from ranges if only a qualification conversion (such as int* to const int * const) is required, so which compiler is correct here?

This is quite likely just a Clang/GCC interoperability quirk, considering that the code also compiles with -stdlib=libc++.


Solution

  • Barry's reduced example shows the relevant difference between Clang and GCC:

    #include <cstddef>
    
    template <size_t N>
    void f(const int* const (&)[N]);
    
    int main() {
      int* pointers[3];
      f(pointers);  // accepted by GCC, rejected by Clang
    }
    

    This deduction is governed by [temp.deduct.call]. According to p3

    [...] If P is a reference type, the type referred to by P is used for type deduction.

    Here, P is the parameter type, const int* const (&)[N], so it is replaced by const int* const [N].

    Then, p4 instructs as follows:

    In general, the deduction process attempts to find template argument values that will make the deduced A identical to A (after the type A is transformed as described above). However, there are three cases that allow a difference:

    • If the original P is a reference type, the deduced A (i.e., the type referred to by the reference) can be more cv-qualified than the transformed A.
    • 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.
    • [...]

    The "deduced A" means P after template arguments have been substituted. So in other words the compiler will try to deduce N that makes const int* const [N] identical to the argument type, int* [3]. Obviously that is not possible, so we need to check if one of the bullets allows the deduction to succeed.

    The original P is a reference type, so the deduced A could be more cv-qualified than A, that is, the deduced A could be int* const [3]. But arguably const int* const [3] is not allowed by this bullet because that type is not just more cv-qualified than int* [3]; it has "deep const" added to it as well.

    The second bullet also doesn't apply here because the "transformed A" is the original A, the type int* [3]. According to p2, A is transformed if the original P is not a reference type. Such a transformation would have made A into int**. But since the original P was a reference type, the transformations don't occur. That means the second bullet here doesn't apply because the transformed A is not a pointer type, it's still the original type int* [3]. The second bullet would apply to a case such as

    template <class T>
    void g(const T*);
    
    int main() { int* p = nullptr; g(p); }
    

    In Barry's example, Clang is right according to the letter of the law but I think GCC is right according to the spirit. It seems that this case falls into a gap in the wording but there is no reason why I would expect it to be rejected, given that this reference binding (if the deduction were to succeed) would be analogous to a qualification conversion (see [over.ics.ref]/1.3). I think this should be reported as a CWG issue.