0

What I'm trying to do is find a clean way to implement a concept for a callable object that takes in a single parameter of type either int or long.

My first attempt was to create a single concept with a secondary template parameter to ensure the parameter type is either int or long. The problem with this approach, as seen in the example below, is that applications of this concept can't infer template parameters. For example, the usages of call() below require that template parameters be explicitly listed out.

// https://godbolt.org/z/E519s8Pso
//
#include <concepts>
#include <iostream>

// Concept for a callable that can take a single parameter or either int or long.
template<typename T, typename P>
concept MySpecialFunction =
    (std::same_as<P, int> || std::same_as<P, long>)
    && requires(T t, P l) {
        { t(l) } -> std::same_as<decltype(l)>;
    };


// T must be callable with 1 parameter that is either int or long!
template<typename T, typename P>
    requires MySpecialFunction<T, P>
decltype(auto) call(T t) {
    return t(2);
}


// Test
int square_int(int num) {
    return num * num;
}

long square_long(long num) {
    return num * num;
}

int main() {
    std::cout << call<decltype(square_int), int>(square_int) << std::endl;
    std::cout << call<decltype(square_long), long>(square_long) << std::endl;
    return 0;
}

My second attempt was to explode out the concept to one for int and one for long, then combine them together in a third concept. In this version, the usages of call() below don't require that template parameters be explicitly listed out, but the concept is more verbose. Imagine how something like this would look if there were more than 20 types instead of just 2.

// https://godbolt.org/z/hchT11rMx
//
#include <concepts>
#include <iostream>

// Concept for a callable that can take a single parameter or either int or long.
template<typename T>
concept MySpecialFunction1 = requires(T t, int i) {
    { t(i) } -> std::same_as<decltype(i)>;
};

template<typename T>
concept MySpecialFunction2 = requires(T t, long l) {
    { t(l) } -> std::same_as<decltype(l)>;
};

template<typename T>
concept MySpecialFunction = MySpecialFunction1<T> || MySpecialFunction2<T>;


// T must be callable with 1 parameter that is either int or long!
template<MySpecialFunction T>
decltype(auto) call(T t) {
    return t(2);
}


// Test
int square_int(int num) {
    return num * num;
}

long square_long(long num) {
    return num * num;
}

int main() {
    std::cout << call(square_int) << std::endl;
    std::cout << call(square_long) << std::endl;
    return 0;
}

Is there anyway to have the conciseness / easy of understanding that the first example gives without the compiler losing the ability to infer template parameters as happens in the second example?

offbynull
  • 381
  • 3
  • 16
  • "*a concept for a callable object that takes in a single parameter of type either int or long.*" Why do you care if it takes those types *specifically*? Why not just require that you can call it with an `int`, even if its parameter list converts it to something else? If you're trying to spell out a specific parameter list in a concept, you are using concepts wrong. – Nicol Bolas Aug 01 '22 at 20:49
  • @NicolBolas My goal here is to be able to cleanly apply concepts to the parameters of a callable. So rather than `int` or `long`, imagine that I wanted a concept that would check that a callable's first parameter parameter must be a type that support std::integral. – offbynull Aug 02 '22 at 12:59
  • Why do you care if it is a `std::integral` or not? Would it be so terrible if a user provided a function whose first parameter was convertible *from* an integral type? The code would work just fine. And to check for that you just use a requires clause that calls the function, passing it an `int`. That's *all* you need to know. It's as simple as `requires(Fn fn, int i) { fn(i); }` – Nicol Bolas Aug 02 '22 at 13:18
  • @NicolBolas The reasons why I care aren't important. The long and short of it is that I want to use concepts to check type traits of the parameters of a callable, plain and simple. – offbynull Aug 02 '22 at 13:40
  • And my point is that this is a misuse of the feature. You shouldn't want to constrain an interface to a specific type in such a way. You should only care if it can take an `int` or a `long`, not whether the function *specifically* uses that type in its interface. It represents an over-constrained interface, an interface that cares too much about things that are not its business. Let the user implement the needed functionality however *they* want. – Nicol Bolas Aug 02 '22 at 13:42
  • When the standard library checks that a random-access iterator can be added to an integer, it does *not* try to check to see if the `operator+` specifically takes any `std::integral`. It [just says](https://en.cppreference.com/w/cpp/iterator/random_access_iterator) `i + n`, where `n` is the iterator difference type. It *does not char* whether your `operator+` specifically takes that type, so long as the expression is valid. Don't build a concept interface the way you would build a `virtual` one, where you enforce a specific set of parameters. – Nicol Bolas Aug 02 '22 at 13:44

2 Answers2

2

Since you have the types int and long baked into the concept, why don't you use something like this:

//true if Fn accepts one parameter (int or long) and have the same type as the result
template <typename Fn>
concept MySpecialFunction = requires (Fn fn) {
    requires 
        std::same_as<int,  decltype(fn(0))> ||
        std::same_as<long, decltype(fn(0L))>;
};

template <MySpecialFunction Fn>
decltype(auto) call(Fn fn) 
{
    //think about what happens if Fn is square_long
    //doesn't the type of the value (int) gets converted to long?
    //that raises the question what your main purpose is
    return fn(2);
}

See: https://godbolt.org/z/Y7vTrPoP5

Maybe you also want to have a look at:

Erdal Küçük
  • 4,810
  • 1
  • 6
  • 11
  • Why the casts? `fn(0)` and `fn(0L)` should be enough. – HolyBlackCat Aug 01 '22 at 21:13
  • @HolyBlackCat Yes, you're right, i've updated it accordingly. – Erdal Küçük Aug 01 '22 at 21:15
  • @ErdalKüçük "that raises the question what your main purpose is" -- I guess my overall goal here is to be able to cleanly apply concepts to the parameters of a callable. So for example, a callable whose first parameter must be a type that support std::integral. – offbynull Aug 02 '22 at 12:00
  • 1
    @offbynull Well, with an requires expression, you're able to test if a call (expression) is valid (as shown in my example). You will need two template-paramters if you don't want to bake the types into the concept, in order to be more generic. – Erdal Küçük Aug 02 '22 at 13:13
  • @ErdalKüçük: Note that by testing against the literal 0, you would also pick up any function that uses a pointer or any other Nullable type. You should just test against an `int`. – Nicol Bolas Aug 02 '22 at 13:20
  • @ErdalKüçük I came across https://stackoverflow.com/a/43526780/1196226 and I thought maybe I could apply it in my concept https://godbolt.org/z/oEbvE8jM4 but the compiler doesn't like it. Using in a static_assert() rather than concept checks seems fine though. I'm not 100% sure sure what the issue is but if I can get it to work I think it'd be an okay solution. – offbynull Aug 02 '22 at 13:50
  • @NicolBolas Good point. – Erdal Küçük Aug 02 '22 at 14:32
  • @offbynull I need more time to inspect the provided examples. – Erdal Küçük Aug 02 '22 at 14:38
0

After some browsing around, I came across https://stackoverflow.com/a/43526780/1196226 and https://stackoverflow.com/a/22632571/1196226. I was able to utilize these answers to build out a solution that can apply concepts to parameters concisely and without the compiler losing the ability to infer template parameters.

// https://godbolt.org/z/nh8nWxhzK
//
#include <concepts>
#include <iostream>




template <std::size_t N, typename T0, typename ... Ts>
struct typeN { using type = typename typeN<N-1U, Ts...>::type; };

template <typename T0, typename ... Ts>
struct typeN<0U, T0, Ts...> { using type = T0; };


template <std::size_t, typename F>
struct argN;

template <std::size_t N, typename R, typename ... As>
struct argN<N, R(*)(As...)> { using type = typename typeN<N, As...>::type; };  // needed for std::integral<>

template <std::size_t N, typename R, typename ... As>
struct argN<N, R(As...)>  { using type = typename typeN<N, As...>::type; };  // needed for std::is_integeral_v<>


template <typename F>
struct returnType;

template <typename R, typename ... As>
struct returnType<R(*)(As...)> { using type = R; };  // works for std::integral<> / std::same_as<>

template <typename R, typename ... As>
struct returnType<R(As...)> { using type = R; };    // needed for std::is_integeral_v<>




template<typename Fn>
concept MySpecialFunction =
    (std::same_as<typename argN<0U, Fn>::type, int> || std::same_as<typename argN<0U, Fn>::type, long>)
    && std::same_as<typename returnType<Fn>::type, typename argN<0U, Fn>::type>;


template<MySpecialFunction Fn>
decltype(auto) call(Fn fn) {
    return fn(2);
}


// Test
int square_int(int num) {
    return num * num;
}

long square_long(long num) {
    return num * num;
}

static_assert( std::is_integral_v<typename argN<0U, decltype(square_int)>::type> );
static_assert( std::is_integral_v<typename returnType<decltype(square_int)>::type> );
static_assert( std::is_integral_v<typename argN<0U, decltype(square_long)>::type> );
static_assert( std::is_integral_v<typename returnType<decltype(square_long)>::type> );

int main() {
    std::cout << call(square_int) << std::endl;
    std::cout << call(square_long) << std::endl;
    return 0;
}
offbynull
  • 381
  • 3
  • 16