Check if object has method with C++20 concepts

A task executor is given tasks that it runs. A while ago I designed a small concept of a task executor that replaces dynamic polymorphism with static polymorphism. But while switching from dynamic to static I lost an aspect about the type of the task that is being passed to the executor: its shape.

The dynamic approach requires an interface that describes exactly how a task must look: what method is required and what’s that method’s signature. For the static approach, I had nothing but a compile-time error if the task does not have a required method. The error message is good, I’m OK with what I get. But I don’t have a definition of what my constraints are for the task. I don’t have a concept of my requirement.

Before C++20, things were verbose and somewhat complicated. There are some ways to write requirements using SFINAE. But I feel they are just for the compilation to fail if they are not met. As for a human to understand them, they sure need more than a glance. It feels like before understanding the requirements of a type, you first need to understand how those requirements are implemented.

I tried a few implementations myself, but I could not get them exactly as I would like them to be. I’m having in my mind the simplicity that C++20 has on the concepts topic and I wanted to be around it. So… why not give C++20 a try? I never wrote C++20 more than a few experimental lines, so I wanted to see how I could implement my need.

A stripped off executor just for the sake of the example, with a task to be executed, would be:

int executor(int input, auto&& task)
{
    return task.execute(input);
}

struct Task {
    int execute(int input) { return input + 1; }
};

int main()
{
    executor(1, Task{});
}

 

The requirements that need to be implemented by the task are:

    • It must be a type with a method named execute.
    • The method accepts an integer argument
    • and returns an integer.

And I need to define a C++20 concept that requires a type T representing the task and an int which will be the input: I call the required execute method on the task object, with the integer argument, and I verify that the return type is an integer.

template <typename T>
concept Task = requires(T&& task, int input)
{
    { task.execute(input) } -> std::same_as<int>;
};

 

Then the executor must use the concept:

int executor(int input, Task auto&& task)
{
    return task.execute(input);
}

// or

template<typename T>
requires Task<T>
int executor(int input, T&& task)
{
    return task.execute(input);
}

// or with custom error message
template<typename T>
int executor(int input, T&& task)
{
    static_assert(Task, "the executor requires a task implementing the Task concept");
    return task.execute(input);
}

 

For a task that has a run method instead of execute

struct InvalidTask {
    int run(int input) { return input + 2; }
};

the compilation error is (along with other details):

error : use of function ‘int execute(int, auto : 1 &&)[with auto:1 = InvalidTask]’ with unsatisfied constraints

 

This gives semantic and compile-time enforcement: I can understand what I need and the compiler will let me know if I don’t have it.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.