0

I would like to use my user defined concept as a template type of std::span but template argument deduction does not work as I expected. When I try to pass a "std::array of char" to template function; compiler shows an error "error: no matching function for call to 'print'" and warns me when I hover over the template definition with mouse as "note: candidate template ignored: could not match 'span' against 'array'".

Here is the concept definition and function template:

#include <concepts>
#include <span>

template <typename  T>
concept OneByteData = sizeof(T) == 1;

template<OneByteData T>
void print(std::span<const T> container)
{
    for(auto element : container)
    {
        //Do Some Work
    }
}

And the user code that doesn't work as I expected:

int main()
{
    std::array<char, 6> arr = {1, 2, 3, 4, 5, 6};
    print(arr);
    return 0;
}

The user code that works and does not produce error:

int main()
{
    std::array<char, 6> arr = {1, 2, 3, 4, 5, 6};
    print<char>(arr);
    return 0;
}

Is there way to call this template function without specializing the type of array. How should I change the template function definition to make the function call the way I mentioned (print(arr)) ?

Edit: I would like to be able to use the benefits of std::span and be able to call the template function with std::array, std::vector and plain C-style arrays.

sakcakoca
  • 9
  • 2

2 Answers2

1

A possible solution is receive (and deduce) a generic type and after check if it is convertible to a std::span with an element_type of size 1.

I mean

template <typename  T>
concept OneByteData = sizeof(T) == 1;

template <typename T>
void print (T container) 
   requires OneByteData<typename decltype(std::span{container})::element_type>
{
  std::span cnt {container};
  
    for(auto element : cnt )
    {
        //Do Some Work
    }
}

// extra print for C-style cases
template <typename T, std::size_t N>
void print (T(&arr)[N])
{ print(std::span{arr}); }

Another possible solution is a print(), similar to your original, that receive a std::span and do the concept check, and a couple of additional print() (one specific for C-style arrays) that convert the deduced type to std::span

template <typename  T>
concept OneByteData = sizeof(T) == 1;

template<OneByteData T, std::size_t N>
void print(std::span<T, N> container)
{
    for(auto element : container)
    {
        //Do Some Work
    }
}

template <typename T, std::size_t N>
void print (T(&arr)[N])
{ print(std::span{arr}); }

template <typename T>
void print (T container)
 { print(std::span{container}); }
max66
  • 65,235
  • 10
  • 71
  • 111
  • Thanks @max66. The code you shared works with std::array and std::vector but when I try to pass plain C-style arrays it produces compiler error. `:31:5: error: no matching function for call to 'print' print(rawArray); ^~~~~ :11:6: note: candidate template ignored: substitution failure [with T = unsigned char *]: no viable constructor or deduction guide for deduction of template arguments of 'span' void print (T container) ` – sakcakoca May 02 '21 at 13:01
  • @sakcakoca - Unfortunately C-style arrays are usually deduced as pointers, so the deduced type loose the dimension, so the deduced type can't be converted to `std::span` using CTAD. I propose to add an intermediate `print()`, specifically designed for C-style arrays (deducing also the size) that call the final `print()` with the required span. See the modified answer. – max66 May 02 '21 at 13:43
  • @sakcakoca - Added a variant of the proposed solution. – max66 May 02 '21 at 13:57
  • The first propesed solution with the expansion of supporting C-style arrays works just like I wanted, it produces correct behaviour for `std::array`, `std:: vector` and C-style arrays of types which meet the requirement of `OneByteData`. **Thanks**. However, the second one looked syntactically more simple and I wanted to try to use it but it doesn't produce any compile error when I pass a `std::array` in the client code and it just throws an exception at runtime. Which it should produce compile error because `int` does not meet the requirement of `OneByteData` concept. – sakcakoca May 02 '21 at 14:36
  • And in the first solution what about using `value_type` instead of `element_type`. Which one would be more correct? – sakcakoca May 02 '21 at 20:09
  • @max66: `template void print (const T& container) { print(std::span{container}); }` should handle both containers and C-array (and would avoid copy) – Jarod42 May 03 '21 at 10:11
0

From my understanding this is how you should actually use it:

// Second template parameter for size
template<OneByteData T, std::size_t N>
void print(std::span<T, N> container) {
  // Do something
  return;
}

// Create a span from a plain array
print(std::span{arr});

Try it here.


If you do not want your user to do them by themselves, you could

  • Write an overload for print that handles the conversion from std::array to std::span such as (Wandbox)

    template<OneByteData T, std::size_t N>
    void print(std::array<T,N> const& container) {
      return print<T const>(std::span{container});
    }
    
  • Otherwise you could rewrite the interface of print to take any container, enforce your constraints for the underlying element type with std::enable_if or concepts such as user max66 has proposed and convert this general container to an std::span internally.

  • For a class you could write a template deduction guide which decides which constructor should be used (Wandbox)

    template <typename T, std::size_t N>
    Print(std::array<T,N>) -> Print<T,N>;
    

Edit

In the case of an operator like in your case as discussed in the comments I would actually use the combination of a templated function and a templated operator. The function append does the work using a generic std::span for the OneByteData data type while the templated operator converts the allowed data types to std::span and calls the function. OneByteData<typename decltype(std::span{cont})::element_type> makes sure that the data structure can be converted to a std::span with the correct data type. You might add additional or different constraints to it or combine these constraints to your own concept.

template<OneByteData T, std::size_t N>
void append(std::span<T,N> const& sp) {
  // Do something
}

template <typename Cont>
MyString& operator += (Cont const& cont) 
requires OneByteData<typename decltype(std::span{cont})::element_type> {
  this->append(std::span{cont});
  return *this;
}

Try it here and here!

2b-t
  • 2,414
  • 1
  • 10
  • 18
  • It will probably work in the way you mentioned but I want client code to be able to call the template function just without any cast or creating a new type. If possible client code should be like `print(arr)`. @2b-t – sakcakoca May 02 '21 at 12:19
  • @sakcakoca I see. And it is a function no class, right? Else you could specify a deduction guide for it. – 2b-t May 02 '21 at 12:27
  • 1
    Actually the template function is member function of a class. It is actually a `operator+=` function which I should be able to pass `std::array`, `std::array`, `std::array`, `std::vector`, `std::vector`, `std::vector`, `plain C-style arrays of char, unsigned char, std::byte` etc. But I simplified the sample code to take attention to my real problem. – sakcakoca May 02 '21 at 13:08
  • In the second solution that you mentioned I guess I should write all the overloads of `print` function for `std::array`, `std::vector` and plain C-style arrays. – sakcakoca May 02 '21 at 13:10
  • @sakcakoca Hmmm, I see... I can't think of another solution other than writing a function `add(std::span)` which holds the implementation and defining the operator for these types separately which converts all the said data types to a `std::span`. In order to reduce code you could write a templated operator which uses `std::enable_if` so that it only works for all the said data types. – 2b-t May 02 '21 at 13:22
  • @sakcakoca If I was you then I would update the description to mention that it is an operator because I think that changes quite a bit the possible solutions. – 2b-t May 02 '21 at 13:23
  • @sakcakoca I added a solution which works for C-style arrays as well as `std::array` and `std::vector`. – 2b-t May 02 '21 at 14:31
  • @sakcakoca I mean of course instead of having a function append you could also create an operator for `std::span`. Convert the data a `std::span` and then call that operator... – 2b-t May 02 '21 at 14:49
  • Thanks. The solution with operator+= function works for the gcc and msvc compilers but it produces compile error on clang compiler when I try to pass C-style arrays. I just used the link you shared and change the compiler to clang 11.0. Also I tried it on godbolt with clang but it also produces compile error. – sakcakoca May 02 '21 at 14:56
  • @sakcakoca Interesting... `OneByteData` like max66 pointed out should [do the job as well](https://wandbox.org/permlink/wH5qc09ICvdiGqvf). Actually having all the three requirements is verbose. – 2b-t May 02 '21 at 15:03
  • Yes, [this one](https://wandbox.org/permlink/wH5qc09ICvdiGqvf) works like max66's first solution. It compiles on both gcc and clang and produces correct behavior. – sakcakoca May 02 '21 at 15:48
  • Ah, yeah, it is similar very similar to his. Didn't notice that he modified his. – 2b-t May 02 '21 at 17:57