【问题标题】:C++ concept to inline lambda parameter内联 lambda 参数的 C++ 概念
【发布时间】:2021-04-28 16:41:47
【问题描述】:

我正在尝试将 lambda(通过引用捕获本地上下文)传递给函数并确保它是内联的。 我认为一个解决方案是使用函数模板和 C++20 概念,但我不知道如何编写采用特定类型的 lambda 概念。

目的是分解光栅文件上的复杂循环,并让调用者为每个像素执行代码。
必须内联 lambda 调用。

我运气不好:

  • std::function:使用 lambda 的性能比在调用者代码中编写整个循环差 2 倍。
  • 函数指针:带捕获(this 或局部变量)的 lambda 无法转换为函数指针
  • 模板方法:我不能重载它,也不能像以前的解决方案那样容易地检查 lambda 类型。

性能不佳的工作代码:

// 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;
           }
        }
     }
});

你能帮我写一个这样的概念吗:

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;

【问题讨论】:

  • 为什么std::function?您需要可以使用给定参数调用的东西,而不是 std::function
  • "确保它是内联的" 你为什么在乎?为什么函数是否声明为inline 是你的事?此外,至少就 C++ 标准而言,无法确保 anything 是“内联”的。
  • 强制内联将需要编译器特定的方法来执行此操作,因为这不是标准正式涵盖的内容。对于 MSVC,您可以在较新的编译器版本中使用 (IIRC) lambda 上的 [[msvc::forceinline]] 属性(因为 __forceinline 不起作用)。对于 GCC/Clang,__attribute__((always_inline)) 适用于 lambdas IIRC,但 [[gnu::always_inline]] 不适用
  • 我只需要它非常高效。这个问题可以改写为:我可以将循环和单次迭代逻辑分开并保持相同的性能水平吗?如果是这样:如何?我需要适当优化循环,但由于程序中有很多次相同类型的代码,我想将其分解。
  • function_view 你可能会感兴趣。

标签: c++ templates lambda c++20 c++-concepts


【解决方案1】:

首先,我们必须从我们如何考虑约束开始:

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

我们没有限制FileDataType 上的回调,FileDataType 是回调返回。相反,我们将它限制在我们传递给它的类型上:

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

然后你会像这样使用这个概念:

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

现在,单独地,如果您想覆盖实际的返回类型(正如您所做的那样,因为 lambda 返回一个 int 但您想要一个 short),您可以提供一个重载,让您提供显式输入并让其他重载转发给它:

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);
}

【讨论】:

    【解决方案2】:

    我认为您正在寻找的是std::invocable,这是一个允许您强制使用给定参数集调用类型的概念:

    #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;
    }
    

    如果您还需要限制Func 的返回类型,您可以使用requires() 块添加复合要求

    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
    }
    

    【讨论】:

    • 谢谢,我试过这个:void WriteDataFile(std::ostream&amp; file, std::invocable&lt;T&gt; auto&amp;&amp; callback) const { using FileDataType = decltype(callback(T())); } 我用 g++10 编译得很好。我仍然没有得到与手写循环相同的性能水平。 (手写:1.25s,概念:1.4s,std::function:2.5s)我忘了我可以用-&gt;指定回调的返回类型
    • 如果我希望调用者指定模板参数,此语法也适用:template&lt;typename FileDataType&gt; bool LoadDataFromFile(const std::string&amp; filePath, std::invocable&lt;FileDataType, T&amp;&gt; auto&amp;&amp; callback)
    • 用这个语法 & g++10 进行了大量测试后,我的结论是没有回调的性能总是更好。我用一个没有概念的简单模板参数做了同样的测试,结论是一样的。我想我会到处复制逻辑-_-
    • @Kiruahxh 您使用的是什么优化级别?您是否也尝试过使用 Clang/LLVM 或 GCC 11 进行测试?
    • O2O3unroll-loops 和 optionnaly flto。我不知道g++11已经发布,我可以试一试。稍后可能会使用 MSVC 编译该程序。速度在调试模式下也很重要
    猜你喜欢
    • 2015-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多