how to specify C++ callable concept when the callable parameters types are deduced

980 Views Asked by At

I invoke a templated lambda from a templated function, the lambda parameters type are deduced. If the type of the lambda if auto, it works : https://godbolt.org/z/WYxj5G8vx

#include <iostream>
#include <cstdint>
#include <array>
#include <functional>
#include <numeric>
#include <concepts>


template <typename T>
int testf2(T, auto fun) {
  std::array<std::uint8_t, sizeof(T)> ar{};
  std::iota(ar.begin(), ar.end(), 0);
  return fun(ar);
}


int main() { 
    auto f2 = []<size_t S> (std::array<uint8_t, S> arr) -> int  {
       return arr[S -1];
   };

   std::cout << "R = " << testf2(5, f2) << std::endl;
}

I wanted to use std::invocable concept to specialize the auto fun parameter of testf2, to be anything but a callable that take std::array<std::uint8_t, N> as parameter.

Using gcc11.2 or clang13, when I try

template <typename T, size_t S>
int testf2(T, std::invocable<std::array<uint8_t, S>> auto fun) {
  std::array<std::uint8_t, sizeof(T)> ar{};
  std::iota(ar.begin(), ar.end(), 0);
  return fun(ar);
}

I get error :

candidate template ignored: couldn't infer template argument 'S' int testf2(T, std::invocable<std::array<uint8_t, S>> auto fun) {

I don't understand why the compiler can infer type when only auto is used, but not with a constraining concept.

What is the correct way to use concept in this situation ?

This is a simplified version of the code, in reality the signature of testf2 is testf2(auto fun, ARGS... args) and the size of the array is calculated upon the parameter pack types.

============ EDIT 03/03/2022 ==================

Thanks for the correct answers, but I have oversimplified the code and the question, so I get right answer to a wrong question.

You need more context, I work with MCUs, and want to make a function that abstract some kind of spi,i2c,modbus, etc transaction where one send buffer to the slave peripheral and receive buffer in response. The function calculate write and read buffer length, serialise (doing endianness conversion if needed), call a lambda to do the actual transaction depending on the transport mechanism, deserialise and return. So the buffers lengths cannot be calculated with a (sizeof(Ts) + ...) as suggested.

I made a more realistic example :live example


// return empty array whose size is the sum of the two arrays given as parameters
template<typename T, std::size_t LL, std::size_t RL>
constexpr std::array<T, LL+RL> join(std::array<T, LL>, std::array<T, RL>)
{
    return std::array<T, LL+RL>{};
}


// return an array of size sizeof(T) if T is arithmetic, otherwise an empty array
template <typename T>
constexpr auto count_ari(T) {
      if constexpr (std::is_arithmetic_v<T>) {
      return std::array<uint8_t, sizeof(T)>{};
  } else {
      return std::array<uint8_t, 0>{};
  }
}

// return empty array whose size is the sum of all parameter which are arithmetic
template <typename HEAD, typename... TAIL>
constexpr auto count_ari(HEAD h, TAIL... tail) {
    return join(count_ari(h), count_ari(tail...));
}

// create a iota filled array whose size is sum of all arithmetic parameters
// call a lambda given in parameter on this array
// return what has done the lambda
// it's here that I want to constrain parameter "auto fun" 
template </*size_t S,*/ typename... ARGS>
int testf2(/*std::invocable<std::array<uint8_t, S>>, */ auto fun, ARGS... args) {
  auto ar = count_ari(args...);
  std::iota(ar.begin(), ar.end(), 1);
  return fun(ar);
}


int main() { 
    auto f2 = []<size_t S> (std::array<uint8_t, S> arr) -> int  {
       return arr[S -1];
   };

  std::cout << "R = " << testf2(f2, 'a') << std::endl;
  std::cout << "R = " << testf2(f2, 6, 7l, "foobar") << std::endl;
}

Question remains the same : is there a way to add constrain on the auto fun parameter of function testf2

2

There are 2 best solutions below

4
On

Concepts (and requires clauses in general) do not participate in template argument deduction. Since your S in this case is just sizeof(T), you should use that.

the size S is the sum of all the sizes of the types of a parameter pack

Then make it sizeof(Args) + ....

0
On

Nicol Bolas helps me to find the solution which was to make a constexpr function taking no argument that calculate the size, and specify the exact type of the callable with std::function instead of trying to specialise auto with invocable concept.

template <typename T>
constexpr size_t sizeof_ari() {
    if constexpr (std::is_arithmetic_v<T>) 
      return sizeof(T);
   else 
      return 0;
}

template <typename... ARGS>
constexpr size_t sizeof_aris() {
    return (sizeof_ari<ARGS>() + ...);
}

// create a iota filled array whose size is sum of all arithmetic parameters
// call a lambda given in parameter on this array
// return what has done the lambda
template <typename... ARGS>
using lambda_param = std::array<uint8_t, sizeof_aris<ARGS...>()>;
template <typename... ARGS>
int testf2(std::function<int(lambda_param<ARGS...>)>  fun, ARGS... args) {
  auto ar = make_buf(args...);
  std::iota(ar.begin(), ar.end(), 1);
  return fun(ar);
}

demo