我可以在 C++20 项目中使用 std::generator 来生成范围视图吗?

Ben*_*uch 2 c++ c++20 c++-coroutine std-ranges c++23

C++23 引入了范围视图std::generator,用作协程的返回。根据以前的观点,通常很难生成复杂的数据序列。我认为使用协程可以大大简化这一过程。

不幸的std::generator是,目前没有任何标准库实现,并且在许多项目中,C++20 将在一段时间内保持标准。std::generator有没有办法在C++20项目中使用?

Ben*_*uch 8

您可以使用 的参考实现std::generator

以下 C++23 程序打印斐波那契数列的前 10 个数字。

import std;

std::generator<int> fibonacci() {
    int a = 0;
    int b = 1;
    co_yield a;
    while (true) {
        co_yield a = std::exchange(b, a + b);
    }
}

int main() {
    for (auto const& v: fibonacci() | std::views::take(10)) {
        std::print("{} ", v);
    }
    std::print("\n");
}
Run Code Online (Sandbox Code Playgroud)
0 1 1 2 3 5 8 13 21 34
Run Code Online (Sandbox Code Playgroud)

该程序使用了三个需要替换为 C++20 的 C++23 元素:std::generatorstd::printimport std标准库模块)。

以下 C++20 变体使用、 s 和标头包含的参考实现std::generatoriostream

#include "__generator.hpp"
#include <iostream>
#include <utility>
#include <ranges>

std::generator<int> fibonacci() {
    int a = 0;
    int b = 1;
    co_yield a;
    while (true) {
        co_yield a = std::exchange(b, a + b);
    }
}

int main() {
    for (auto const& v: fibonacci() | std::views::take(10)) {
        std::cout << v << ' ';
    }
    std::cout << '\n';
}
Run Code Online (Sandbox Code Playgroud)

也可以用fmt库 ( == )std::print替换。std::print("{}\n", 5)fmt::print("{}\n", 5)

使用 C++20 编译器构建示例

如果将本文的示例保存为,则可以使用main.cpp以下命令使用 C++20 编译器来编译它们。CMakeLists.txt

这只是使用 C++20 编译器编译本文示例的一种解决方法。这种修改源代码的定制很容易出错,不应该在生产代码中使用!

cmake_minimum_required(VERSION 3.12)

file(DOWNLOAD
    "https://raw.githubusercontent.com/lewissbaker/generator/e7c6c1c/include/__generator.hpp"
    "${CMAKE_BINARY_DIR}/external/generator.hpp")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/core.h"
    "${CMAKE_BINARY_DIR}/external/core.h")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/format.h"
    "${CMAKE_BINARY_DIR}/external/format.h")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/format-inl.h"
    "${CMAKE_BINARY_DIR}/external/format-inl.h")
file(WRITE "${CMAKE_BINARY_DIR}/replace.cmake"
[=[
file(READ "${SOURCE}" TEXT)
string(CONCAT HEADERS [==[
#if __has_include(<print>)
#include <print>
#else
#define FMT_HEADER_ONLY
#include "external/core.h"
namespace std { using fmt::print; }
#endif
#if __has_include(<generator>)
#include <generator>
#else
#include "external/generator.hpp"
#endif
#include <array>
#include <ranges>
#include <string_view>
#line 2 ]==] "\"${SOURCE}\"\n")
string(REGEX REPLACE "^ *import +std *; *\n" "${HEADERS}" TEXT "${TEXT}")
file(WRITE "${TARGET}" "${TEXT}")
]=])
add_custom_command(
    OUTPUT "${CMAKE_BINARY_DIR}/out.cpp"
    COMMAND "${CMAKE_COMMAND}"
        "-DSOURCE=${CMAKE_SOURCE_DIR}/main.cpp"
        "-DTARGET=${CMAKE_BINARY_DIR}/out.cpp"
        -P "${CMAKE_BINARY_DIR}/replace.cmake"
    DEPENDS "${CMAKE_SOURCE_DIR}/main.cpp")

project(main)
add_executable(main "${CMAKE_BINARY_DIR}/out.cpp")
target_compile_features(main PUBLIC cxx_std_20)
Run Code Online (Sandbox Code Playgroud)

这些DOWNLOAD行下载 的参考实现std::generator和部分fmtfmt::print

然后replace.cmake创建一个文件。这是作为自定义命令设置的。main.cpp自上次构建以来每当发生更改时都会执行此操作。它替换import std;为以下代码。这会out.cpp在构建目录中创建一个新目录,其中包含 C++20 代码并包含下载的文件(如果需要)。

cmake_minimum_required(VERSION 3.12)

file(DOWNLOAD
    "https://raw.githubusercontent.com/lewissbaker/generator/e7c6c1c/include/__generator.hpp"
    "${CMAKE_BINARY_DIR}/external/generator.hpp")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/core.h"
    "${CMAKE_BINARY_DIR}/external/core.h")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/format.h"
    "${CMAKE_BINARY_DIR}/external/format.h")
file(DOWNLOAD
    "https://raw.githubusercontent.com/fmtlib/fmt/9.1.0/include/fmt/format-inl.h"
    "${CMAKE_BINARY_DIR}/external/format-inl.h")
file(WRITE "${CMAKE_BINARY_DIR}/replace.cmake"
[=[
file(READ "${SOURCE}" TEXT)
string(CONCAT HEADERS [==[
#if __has_include(<print>)
#include <print>
#else
#define FMT_HEADER_ONLY
#include "external/core.h"
namespace std { using fmt::print; }
#endif
#if __has_include(<generator>)
#include <generator>
#else
#include "external/generator.hpp"
#endif
#include <array>
#include <ranges>
#include <string_view>
#line 2 ]==] "\"${SOURCE}\"\n")
string(REGEX REPLACE "^ *import +std *; *\n" "${HEADERS}" TEXT "${TEXT}")
file(WRITE "${TARGET}" "${TEXT}")
]=])
add_custom_command(
    OUTPUT "${CMAKE_BINARY_DIR}/out.cpp"
    COMMAND "${CMAKE_COMMAND}"
        "-DSOURCE=${CMAKE_SOURCE_DIR}/main.cpp"
        "-DTARGET=${CMAKE_BINARY_DIR}/out.cpp"
        -P "${CMAKE_BINARY_DIR}/replace.cmake"
    DEPENDS "${CMAKE_SOURCE_DIR}/main.cpp")

project(main)
add_executable(main "${CMAKE_BINARY_DIR}/out.cpp")
target_compile_features(main PUBLIC cxx_std_20)
Run Code Online (Sandbox Code Playgroud)

第一个和第二个块绑定 C++23 标头或插件替换。如果适用,fmt::print可作为 提供std::print。请注意,您不能向命名空间添加任何内容std!由于这只是一种解决方法,而且我们也知道std::print在这种情况下不存在,因此我们忽略此规则。

然后包含翻译示例所需的正常标题。

最后一行告诉编译器,如果出现任何错误消息,它的行为应该像当前在main.cpp第 2 行中的原始消息一样。

必须add_executable引用生成的out.cpp.

的用法std::generator

正如斐波那契示例中所见,可以使用 生成无限序列std::generator。在下面的示例中,我们使用参数来指定序列的起始值。然后我们std::views::take再次使用来限制序列。

#if __has_include(<print>)
#include <print>
#else
#define FMT_HEADER_ONLY
#include "external/core.h"
namespace std { using fmt::print; }
#endif
#if __has_include(<generator>)
#include <generator>
#else
#include "external/generator.hpp"
#endif
#include <array>
#include <ranges>
#include <utility>
#include <string_view>
#line 2 /path/to/original/main.cpp
Run Code Online (Sandbox Code Playgroud)
import std;

auto day_of_the_week(std::size_t start = 0)
-> std::generator<std::string_view> {
    static constexpr std::array<std::string_view, 7> days =
        {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    for (std::size_t i = start % days.size(); true; i = (i + 1) % days.size()) {
        co_yield days[i];
    }
}

auto main() -> int {
    for (std::string_view day: day_of_the_week(5) | std::views::take(3)) {
        std::print("Today is {}\n", day);
    }
}
Run Code Online (Sandbox Code Playgroud)

为了直接定义一个有限的范围,我们只需让协程的控制流跑完即可。

Today is Sat
Today is Sun
Today is Mon
Run Code Online (Sandbox Code Playgroud)
import std;

auto day_of_the_week(std::size_t start = 0, std::size_t count = 7)
-> std::generator<std::string_view> {
    static constexpr std::array<std::string_view, 7> days =
        {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    for (std::size_t i = 0; i < count; ++i) {
        co_yield days[(i + start) % days.size()];
    }
}

auto main() -> int {
    for (std::string_view day: day_of_the_week(3, 5)) {
        std::print("Today is {}\n", day);
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,这也可以通过显式声明来完成co_return;

Today is Thu
Today is Fri
Today is Sat
Today is Sun
Today is Mon
Run Code Online (Sandbox Code Playgroud)
import std;

auto day_of_the_week(std::size_t start = 0, std::size_t count = 7)
-> std::generator<std::string_view> {
    static constexpr std::array<std::string_view, 7> days =
        {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    for (std::size_t i = 0; true; ++i) {
        if(i >= count) {
            co_return;
        }
        co_yield days[(i + start) % days.size()];
    }
}

auto main() -> int {
    for (std::string_view day: day_of_the_week(3, 5)) {
        std::print("Today is {}\n", day);
    }
}
Run Code Online (Sandbox Code Playgroud)

std::generator是一个输入范围

该方法的局限性std::generator在于它是一个简单的输入范围。另一方面,iota | transform可以生成随机访问范围。

Today is Thu
Today is Fri
Today is Sat
Today is Sun
Today is Mon
Run Code Online (Sandbox Code Playgroud)
import std;

auto main() -> int {
    auto day_of_the_week =
        std::views::iota(std::size_t()) |
        std::views::transform([](std::size_t i){
            static constexpr std::array<std::string_view, 7> days =
                {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
            return days[i % days.size()];
        });

    for(std::size_t i = 1000; i < 1003; ++i) {
        std::print("Day {} is {}\n", i, day_of_the_week[i]);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要随机访问,则不能不使用std::generator.

示例备注

Day 1000 is Sun
Day 1001 is Mon
Day 1002 is Tue
Run Code Online (Sandbox Code Playgroud)

只是另一种表示法

auto function() -> ReturnType {}
Run Code Online (Sandbox Code Playgroud)

我使用这种表示法是因为对于长行,返回数据类型可以以一种漂亮而清晰的方式包装到单独的行中。它避免了示例中的侧滚动。这种表示法不是特定于协程的,它也适用于普通函数。请注意,对于 lambda 函数,返回数据类型甚至总是以这种方式指定。

ReturnType function() {}
Run Code Online (Sandbox Code Playgroud)

整个标准库可以通过import std从 C++23 开始的模块包含。这import比通过#include. 您不再需要记住在哪个标头中声明了什么。