Awaitables / tmc::spawn()#

From within a tmc::task, you can co_await any awaitable (including other tmc::task) directly.

You can also customize and/or combine awaitables using these utility functions:

Awaitable Composition#

Since the output of the spawn_*() family of functions is an awaitable, they can be composed by passing them into each other.

// This example constructs several different awaitable groups using different methods.
// At the end, they are all composed into a spawn_tuple.

tmc::task<Model> load_model(std::string filename); // a coroutine
TextureLoadAwaitable load_texture(std::string filename); // a non-coroutine awaitable
Shader compile_shader(ShaderSource src); // a regular function

struct EntityInfo {
  std::string modelFile;
  std::vector<std::string> textureFiles;
  std::vector<ShaderSource> shaderSources;

  // This function is not a coroutine - it just returns the awaitable group.
  // It could be made into a coroutine that awaits the awaitable group and returns
  // the result, and the caller's syntax would be the same either way.
  auto load() {
    auto textureTasks = tmc::spawn_many(
      textureFiles | std::ranges::views::transform(load_texture)
    );

    auto shaderTasks = tmc::spawn_many(
      shaderSources | std::ranges::views::transform([](ShaderSource src) {
        // make the regular function into an awaitable
        return tmc::spawn_func(compile_shader, src);
      })
    );

    return tmc::spawn_tuple(
      load_model(modelFile),
      std::move(textureTasks),
      std::move(shaderTasks)
    );
  }
};

tmc::task<void> entity_loader(EntityInfo& e) {
  // Run all of the subtasks (and sub-subtasks) concurrently and wait for the results.
  // The return type is `std::tuple<Model, std::vector<Texture>, std::vector<Shader>>`
  auto [model, textures, shaders] = co_await e.load();
}

Rvalue-Only Awaitables#

Most TMC awaitables (including tmc::task) are single-use / “rvalue-only awaitables” / linear types. They must be used as temporaries, or passed as rvalue references, and then ultimately consumed.

  • They cannot be copied.

  • They must be moved into a consuming operation exactly once before they go out of scope. The consuming operations are co_await, spawn_*(), fork(), or detach().

  • Moved-from awaitables must not be used afterward.

  • The result of a co_await expression must be stored in a value (not a reference).

Note that the awaitables produced by spawn_*() and fork() are also subject to these rules. So if you move a task into spawn(), the task is considered consumed, but spawn() returns a new awaitable object, which must itself be consumed.

The purpose of these rules is to help you prevent leaks / use-after-free issues. Where possible, these rules are enforced at compile time. Additionally, in Debug builds, there are runtime asserts that verify that you have not violated the above preconditions. In Release builds, these runtime checks are inactive for performance reasons.

The easiest way to use rvalue-only awaitables is to use them as temporaries in immediately-awaited expressions:

// temporary rvalue
co_await expr();

// explicit rvalue cast required
auto t = expr();
co_await std::move(t);

// wrappers have the same rules - temporary rvalue
co_await spawn_tuple(expr());

// explicit rvalue cast required
auto t = expr();
co_await tmc::spawn_tuple(std::move(t));

Lvalue-Only Awaitables#

A small number of TMC awaitables are “lvalue-only awaitables”. For example,the awaitable produced by result_each() must be awaited multiple times to produce a sequence of values. These types cannot be awaited as rvalues or xvalues - you must first assign them to a named variable before awaiting them.

// temporary rvalue not allowed, an lvalue must be created
auto t = expr();
co_await t;
co_await t;

// wrappers have the same rules
auto t = expr();
co_await tmc::spawn_tuple(t);
co_await tmc::spawn_tuple(t);

3rd Party / Unknown Awaitables - It Just Works#

When you await or spawn an awaitable of a type that is unknown to TMC, that awaitable will automatically be wrapped into a tmc::task. This provides full compatibility for all awaitables, at a small performance cost. If you want to remove this overhead, see Integrating External Awaitables.