Kiruahxh
Kiruahxh

Reputation: 2035

C++ concept to inline lambda parameter

I'm am trying to pass a lambda (that captures local context by reference) to a function and ensure it is inlined. I think a solution would be to use a function template and a C++20 concept, but I don't know how to write the concept of a lambda that takes specific types.

The aim is to factorize a complex loops on raster files, and let the caller execute code for each pixel.
The lambda call must be inlined.

I had bad luck with:

Working code with bad performance:

// loop
template<typename T>
class Raster<T>
{
    template<typename FileDataType>
    void SaveToDisk(std::ostream& file, const std::function<FileDataType(T&)>& callback) const;
    template<typename FileDataType>
    void SaveToDisk(std::ostream& file, AnotherCallbackType...);
}

// call
class GeoRaster<double> : public Raster
{
    void Test()
    {
        // Creates a file containing "short" values, from a GeoRaster<double>
        SaveToDisk<short>(file, [&](double val)
        {
           // this lambda call must be inlined
           if (val == this->nodata)
           {
               return 255;
           }
           else
           {
              return short(val) / 16;
           }
        }
     }
});

Could you help me to write a concept like this:

template<typename T, typename FileDataType>
concept SimpleWriteCallback = requires(T a) {
    { a } -> std::convertible_to<std::function<FileDataType(T&)>>;
};

template<typename FileDataType, SimpleWriteCallback<FileDataType> Callback>
void SaveToDisk(std::ostream& file, Callback& callback) const;

Upvotes: 2

Views: 2587

Answers (2)

mcilloni
mcilloni

Reputation: 743

I think that you are looking for is std::invocable, which is a concept that allows you to enforce that a type can be called with a given set of arguments:

#include <concepts>
#include <cstdio>

void do_stuff(std::invocable<int, int> auto &&fn) {
        fn(3, 4);
}

int main() {
        do_stuff([](const int a, const int b) { std::printf("%d %d\n", a, b); });
        // do_stuff([] { std::puts("hi!"); }); // won't compile

        return 0;
}

If you need to also constrain the return type of Func, you may use a requires() block to add a compound requirement:

template <typename Func>
        requires requires(Func &&fn, std::vector<int> v) {
                std::invocable<Func, const std::vector<int>&, bool>;
                { fn(v, false) } -> std::convertible_to<int>;
        }
int do_stuff(std::vector<int> v, Func &&fn) {
        v.push_back(31);

        return fn(v, true);
}

int main() {
    do_stuff({1, 2 ,3}, [](const auto &, bool) { return 3; }); // ok: right parameters, and return type is convertible to int
    do_stuff({1, 2 ,3}, [](const auto &, bool) { return 3.f; }); // ok: float is also implicitly convertible to int
    //do_stuff({1, 2 ,3}, [](const auto &v) { return 3; }); // won't compile, cannot be invoked with (const vector<int>&, bool)
    //do_stuff({1, 2 ,3}, [](const auto &v, bool) { return ""; }); // won't compile, const char (&)[] is not convertible to int
}

Upvotes: 4

Barry
Barry

Reputation: 303097

First, we have to start with how we're even thinking about the constraint:

template<typename T, typename FileDataType>
concept SimpleWriteCallback = /* ... */;

We're not constraining the callback on a FileDataType, FileDataType is what the callback returns. Rather, we're constraining it on the type that we're passing into it:

template <typename F, typename T>
concept SimpleWriteCallback = std::invocable<F&, T&>;

And you'd then use this concept like:

template <SimpleWriteCallback<T> F>
void SaveToDisk(std::ostream& file, F&& callback) const {
    using FileDataType = std::invoke_result_t<F&, T&>;
    // ...
}

Now, separately, if you want to override the actual return type (as you seem to do, since the lambda returns an int but you want a short), you can provide an overload that lets you provide the type explicitly and have the other overload just forward to it:

template <typename FileDataType, SimpleWriteCallback<T> F>
    requires std::convertible_to<std::invoke_result_t<F&, T&>, FileDataType>
void SaveToDisk(std::ostream& file, F&& callback) const {
    // ...
}

template <SimpleWriteCallback<T> F>
void SaveToDisk(std::ostream& file, F&& callback) const {
    // by default, FileDataType is the actual invocation result
    SaveToDisk<std::invoke_result_t<F&, T&>>(file, callback);
}

Upvotes: 1

Related Questions