Skip to content

Latest commit

 

History

History
983 lines (726 loc) · 32.9 KB

reference.adoc

File metadata and controls

983 lines (726 loc) · 32.9 KB

Library Reference

This document offers a complete reference for all public types and functions in Copper. All elements are contained within the namespace copper, which is implicitly omitted in this reference documents.

Function signatures shown in this document are not necessarily the actual implementation in code. They only represent their behavior and their usage, while striving to be more readable as a reference compared to the actual lengthy definitions.

Channels

Channels are the core type of Copper. They represent a queue objects, sending (or "pushing") messages of type T from one direction to another, where they are received (or "popped").

template<
    bool is_buffered,
    typename T,
    template<typename...> typename BufferQueue = std::queue,
    template<typename...> typename OpQueue = std::deque
>
class channel;

is_buffered distinguishes buffered and unbuffered channels. There are type aliases copper::buffered_channel and copper::unbuffered_channel expecting the same template arguments, except for is_buffered. See the sections below for more details on their differences.

T is the type of the messages that are passed by this channel. It needs to be default constructible, move constructible, move assignable, and destructible. The only exception is T = void. See Void channels for more details.

BufferQueue is the type used for the internal buffer of messages. It is only used when is_buffered is true and T is not void. It needs to satisfy basic operations on a queue like std::queue does; specifically, it needs to support push, pop, and front.

OpQueue is the type of the container used to store pending operations on a channel. In addition to being used as a queue, it also needs to support erasing elements at arbitrary positions. It defaults to std::dequeue.

Unbuffered & Buffered

Buffered channels contain an internal queue to store messages for later use. That means a message can be pushed into the channel at an arbitrary point before it will be returned by a pop again later. They are the more "general use" choice of the two and provide speed comparable to that of a simple std::queue in combination with a mutex.

// Create a new channel with an unlimited buffer.
auto chan = buffered_channel<int>();

// Create a new buffered channel that can store up to 10 messages.
auto chan = buffered_channel<int>(10);

Unbuffered channels do not store any T elements within themselves. Rather, a push or pop operation in one thread blocks until its counterpart is executed at the same time by another thread. Unbuffered channels are more difficult to use and slower than buffered channels in general. See docs/motivation.adoc for information on what advantages an unbuffered channel can offer, besides the obvious smaller memory footprint.

// Create a new channel without a buffer.
auto chan = unbuffered_channel<int>();

Void channels

A void channel is a channel with message type T = void. They are used as "signals", similar to std::future<void>. Pushing into a void channel can be seen as emitting a signal and popping from it then consumes the signal.

Void channels can be buffered or unbuffered and support all the same operations that normal channels support, with a few differences. In general, any function that would return std::optional<T> returns bool instead. Any function that has a T parameter is missing that parameter instead. The same is true for callables passed to copper functions, such as select or push_func.

States of a channel

A channel can have two states, open and closed, always starting in the former when constructed. A channel that is open can be freely used to pass messages. A closed channel does not allow any further push operations but if it is buffered, the remaining messages can still be popped.

Sending and receiving messages

Most operations on channels belong into one of the "push family" or the "pop family", which are used to send and receive messages respectively. In its most basic form, these two families are represented by the functions push and pop:

// Send the message "1" over this channel "chan".
const bool push_successful = chan.push(1);

// Consume the message.
const std::optional<int> pop_result = chan.pop();

push accepts a value that can be converted to T and returns a bool that is true if the operation could be completed successfully. This is not the case if the channel is closed, in which case nothing can be pushed into it and false is returned.

pop returns an std::optional<T> which contains the next message from the queue if it could be executed successfully. If the channel is closed and there are no messages in the buffer of the channel, pop will return std::nullopt.

Both push and pop will block the caller as long as needed if they cannot complete their operation immediately. This could be because the internal buffer is full / empty or the channel is simply unbuffered. Only when the operation can be completed or the channel is closed and the operation has to be canceled will the call unblock.

Copper ensures that blocked operations are treated in a FIFO order. For example, if two or more "push" operations are pending on a channel with a full buffer, as soon as a "pop" operation is run, the "push" that has been waiting the longest is guaranteed to be executed first.

Both push and pop offer three variants for non-blocking access.

bool push(T);
bool try_push(T);
bool try_push_for(T, const std::chrono::duration&);
bool try_push_until(T, const std::chrono::time_point&);

std::optional<T> pop();
std::optional<T> try_pop();
std::optional<T> try_pop_for(std::chrono::duration&);
std::optional<T> try_pop_until(const std::chrono::time_point&);

This pattern of the four variants of one operation can be found in several places in Copper. They always have the same behavior:

  • X() blocks the caller until the operation either can be completed successfully or a state is reached in which it is impossible to be completed anymore.

  • try_X() does not block the caller at all. It only executes the operation if it can be done immediately.

  • try_X_for(const std::chrono::duration&) behaves like X() with the addition of a timeout. If the call could not execute the operation and has blocked the caller for at least the given duration, it is cancelled and returns to the caller.

  • try_X_until(const std::chrono::time_point&) behaves like try_X_for, only that instead of a duration, a fixed point in time is passed. The operation is cancelled as soon as the system clock passes that point.

Like push, the other three "push" functions return true only if the operation could be completed successfully. In their case, that means false can be returned even if the channel is still open, when a timeout occurs. The same is true for the "pop" functions and std::nullopt.

Sending and receiving with functions

push and pop provide easy access to the basic functionality of a channel while sacrificing some efficiency and power. Full access is gained by using push_func / pop_func and their variants.

channel_op_status push_func(const std::function<T()>&);
channel_op_status try_push_func(const std::function<T()>&);
channel_op_status try_push_func_for(const std::function<T()>&, const std::chrono::duration&);
channel_op_status try_push_func_until(const std::function<T()>&, const std::chrono::time_point&);

channel_op_status pop_func(const std::function<void(T&&)>&);
channel_op_status try_pop_func(const std::function<void(T&&)>&);
channel_op_status try_pop_func_for(const std::function<void(T&&)>&, const std::chrono::duration&);
channel_op_status try_pop_func_until(const std::function<void(T&&)>&, const std::chrono::time_point&);

Instead of expecting a T object, push_func uses a callable argument which is used as a generator for the message to be pushed. If the push operation can be completed successfully, and only then, is the passed function called and its return value is sent over the channel.

Instead of returning a T object, pop_func also uses a callable argument which handles the popped message. If the pop operation can be completed successfully, the argument is called with the received message.

All functions return a channel_op_status value, which is a scoped enum.

enum class channel_op_status {
    success,     /** The operation was completed successfully. */
    unavailable, /** The operation could not be completed due to resources being unavailable. */
    closed,      /** The operation could not be completed, as the channel object is already closed. */
};

Instead of a binary value (success / no success), this represents a ternary result, where "no success" is split into closed, meaning the channel is closed, and the operation cannot be executed on that channel anymore, or unavailable, meaning the operation could not be executed at the moment and caused a timeout.

Important
The channel object is locked while a callable is executed. This can cause unexpected slowdowns or even deadlocks in certain cases. See docs/techdetails.adoc for details.

Templated sending and receiving

Copper also defines the following scoped enum, which represents the four variants of each operation seen so far:

enum class wait_type {
    forever, /** Wait for however long it takes to complete. */
    none,    /** Do not wait at all, if the operation cannot be completed immediately. */
    for_,    /** Wait for a set amount of time. */
    until,   /** Wait until a certain point in time. */
};

Instead of distinguishing the variant by name, a channel also offers functions to access the different behavior by template parameter.

template<wait_type>
std::optional<T> pop_wt(Args&&...);

template<wait_type>
bool push_wt(T, Args&&...);

template<wait_type>
channel_op_status pop_func_wt(const std::function<T()>&, Args&&...);

template<wait_type>
channel_op_status push_func_wt(const std::function<void(T&&)>&, Args&&...);

In general, you probably do not want to use these "_wt" functions for direct calls, as they are much less readable than the named operations. They can be useful however when another templated function calls one of these operations.

chan.template push_wt<wait_type::for_>(123, 200ms);

Other channel operations

Channels offer a few functions apart from the "push" and "pop" family.

channel_pop_iterator<channel> begin();
channel_pop_iterator<channel> end();

channel_push_iterator<channel> push_iterator();

channel_read_view<channel> read_view();
channel_write_view<channel> write_view();

The functions begin, end, push_iterator, read_view, and write_view provide easy access to iterators and views on this channel. See Views and Iterators for more information on that.

void close();

bool is_read_closed();
bool is_write_closed();

size_t clear();

The close function does just that: it closes the channel, preventing any further "push" operations.

is_write_closed returns true if and only if the channel is closed, meaning that no more messages can be written to it. is_write_closed returns true if and only if the channel is closed and empty, meaning that no more messages can be read from it.

clear empties the buffer of the channel and returns the number of messages that were cleared. This is probably the most situational of all functions in channel. It is recommended to not use clear on an open channel, as pending "push" operations are not cancelled and the behavior might be unexpected. It can be used after closing a buffered channel to instantly stop all consumers, instead of them processing the rest of the buffer.

Select

While channels by themselves can suffice to provide communication between threads, the "select" functions add to their potential. They allow you to combine the handling of multiple channels into a single thread, thus reducing the total number of threads and synchronisation objects and making your code more cohesive.

channel_op_status select(Ops&&...);
channel_op_status try_select(Ops&&...);
channel_op_status try_select_for(Ops&&..., const std::chrono::duration&);
channel_op_status try_select_until(Ops&&..., const std::chrono::time_point&);

A "select" can be seen as the parallel execution of one or multiple "push_func" and/or "pop_func" operations. The Ops type parameter defines that group of operations, which are created by the shift operators << and >> (inspired by Go’s -> and <-).

auto operator>>(channel&, const std::function<void(T&&)>&);
auto operator<<(channel&, const std::function<T()>&);

>> ("something being moved from the channel") represents the "pop". << ("something being moved into the channel") represents the "push".

// These two statements are equivalent:
chan.pop_func([](int i) { consume(i); });
try_select(chan >> [](int i) { consume(i); });

While a single-op select can be represented by channel member functions, that is not possible for multi-op selects anymore.

// Consume a message from either chan1 or chan2. (not both!)
try_select_for(
    1s,
    chan1 >> [](std::string s) { consume(std::move(s)); },
    chan2 >> [](int i) { consume(i); }
);

You can also use the same channel multiple times and mix pushes and pops within one "select".

select(
    chan1 << [] { return std::string("Hello World!"); },
    chan1 >> [](std::string s) { consume(std::move(s)); },
    chan2 >> [](int i) { chan1.push(std::to_string(i)); }
);

A "select" guarantees that:

  • No deadlocks occur, even if a consumer or producer function accesses the channel itself again. Note that this only holds true as long as the passed callable does not cause deadlocks itself.

  • At most one select operation will be chosen and executed for each "select" statement.

  • If a "select" statement is called when at least one operation can be executed immediately, the operation that will be executed is chosen uniform randomly from all ready operations to ensure fairness.

A select statement returns

  • channel_op_status::success, if any one of the operations could be executed.

  • channel_op_status::closed, if the channels of all operations are closed and the channels of all push operations are empty.

  • channel_op_status::unavailable otherwise.

Important
The channel object is locked while a callable is executed. This can cause unexpected slowdowns or even deadlocks in certain cases. See docs/techdetails.adoc for details.

Due to the locking behavior described above, there is an alternative "vselect" family which can prove to be faster when a lot of time is spent within the "select" callbacks.

A "vselect" operation runs the push and pop on values rather than functions, similar to channel::push and channel::pop compared to their related "*_func" functions. To be able to be used similar to a switch-case statement, a "vselect" also accepts callback functions. The difference to "select" is that the channel can be unlocked while these are executed.

vselect(
    chan1 << 1,  // Push "1" into chan1, if possible.
    [] {},

    chan2 << std::move(x),  // Push the value of "x" into chan2, if possible.
    [&x] { x = compute_next_value(); },  // If chan2 was pushed into, "x" is re-computed.

    chan3 >> y,  // Pop the next message of chan3 into "y".
    [] {}
);

For void channels, there is a special placeholder value to replace the variables and values. The placeholder can be accessed under the name copper::_ or copper::voidval.

using copper::_;

vselect(
    chan1 << _,  // Send a message over the void channel if possible.
    [] {},

    // If "chan2" contained a message, the application exists.
    chan2 >> _,
    [] { exit(1); },
);

Views

Views are references to existing channels that allow only a subset of all operations. channel_read_view only allows "pop" functions as well as begin() and end(). channel_write_view only allows "push" functions as well as push_iterator(), close(), and clear().

auto read_view1 = channel_read_view(chan);
// equivalent to:
auto read_view2 = chan.read_view();

auto write_view1 = channel_write_view(chan);
// equivalent to:
auto write_view2 = chan.write_view();

Views are useful when a library or framework that uses channels to communicate allows a user to provide messages that will then be consumed by a library-internal thread. Passing the user a channel_write_view instead of a channel& prevents any accidental misuse by trying to pop from the channel rather than push into it.

Iterators

Copper provides two type of iterator types: channel_pop_iterator and channel_push_iterator. They can be instantiated for every channel with non-void messages.

channel_pop_iterator is a std::input_iterator that pops elements from the underlying channel when incremented. It uses pop to extract elements, meaning it will block until an element can be popped successfully. It is considered to have reached its end when the channel is closed and contains no buffered messages.

A channel_pop_iterator can be constructed most easily by calling begin() and end() on a channel. Because of that, channels can not only be used in combination with std::algorithm but also classify as ranges in the context of std::ranges. It can also be used in a for-each loop.

// Does the producer send a "1" at any point?
std::find(chan.begin(), chan.end(), 1) != chan.end();
std::ranges::find(chan, 1) != chan.end();

// Do something until the channel is closed.
for (auto message : chan) {
    // ...
}

channel_push_iterator works similarly to std::back_insert_iterator. The statement *iter = val; is equivalent to chan.push(val);.

It can be constructed most easily by calling push_iterator() on a channel. While having less use cases than the channel_pop_iterator, a channel_push_iterator also allows support for some more std algorithms.

// Push all elements from a vector into my channel.
std::copy(v.begin(), v.end(), c.push_iterator());
std::ranges::copy(v, c.push_iterator());

Configuration

Copper offers some slight configuration options for different use cases.

COPPER_DISALLOW_MUTEX_RECURSION

By default, Copper uses std::recursive_mutex in some places to make sure that referencing a channel within a "push_func", "pop_func", or "select" will not causea deadlock.

chan.pop_func([&chan](int i) { chan.push(i); });

This can be disabled by adding the preprocesser definition COPPER_DISALLOW_MUTEX_RECURSION to use the less secure but faster std::mutex instead.

Complete Declarations Reference

A complete collection of all public types and functions. Declarations are not necessarily the actual implementation in code. They only represent their behavior and their usage, while striving to be more readable as a reference compared to the actual lengthy definitions.

enums
enum class channel_op_status { success, closed, unavailable };

enum class wait_type { forever, none, for_, until };
channels
template<typename T, template<typename> typename... Args>
using buffered_channel = channel<true, T, Args...>;

template<typename T, template<typename> typename... Args>
using unbuffered_channel = channel<false, T, Args...>;

// non-void messages
template<bool is_buffered,
        typename T,
        template<typename...> typename BufferQueue = std::queue,
        template<typename...> typename OpQueue = std::deque>
struct channel {
    using value_type = T;

    // Constructor, Assignment, Destructor
    channel() = default;
    explicit channel(size_t max_buffer_size);

    channel(channel&& other) noexcept;
    channel(const channel& other) = delete;

    ~channel();

    channel& operator=(const channel&) = delete;
    channel& operator=(channel&&) = delete;

    // pop
    template<wait_type wtype, typename... Args>
    [[nodiscard]] std::optional<T> pop_wt(
        Args&& ... args
    );

    [[nodiscard]] std::optional<T> pop();

    [[nodiscard]] std::optional<T> try_pop();

    template<typename Rep, typename Period>
    [[nodiscard]] std::optional<T> try_pop_for(
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] std::optional<T> try_pop_until(
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // pop_func
    [[nodiscard]] channel_op_status pop_func_wt(
        const std::function<void(T&&)>&,
        Args&& ... args
    );

    [[nodiscard]] channel_op_status pop_func(
        const std::function<void(T&&)>&
    );

    [[nodiscard]] channel_op_status try_pop_func(
        const std::function<void(T&&)>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] channel_op_status try_pop_func_for(
        const std::function<void(T&&)>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] channel_op_status try_pop_func_until(
        const std::function<void(T&&)>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // push
    template<wait_type wtype, typename... Args>
    [[nodiscard]] bool push_wt(
        T,
        Args&& ... args
    );

    [[nodiscard]] bool push(
        T
    );

    [[nodiscard]] bool try_push(
        T
    );

    template<typename Rep, typename Period>
    [[nodiscard]] std::optional<T> try_push_for(
        T,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] std::optional<T> try_push_until(
        T,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // push_func
    [[nodiscard]] channel_op_status push_func_wt(
        const std::function<T()>&,
        Args&& ... args
    );

    [[nodiscard]] channel_op_status push_func(
        const std::function<T()>&
    );

    [[nodiscard]] channel_op_status try_push_func(
        const std::function<T()>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] channel_op_status try_push_func_for(
        const std::function<T()>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] channel_op_status try_push_func_until(
        const std::function<T()>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // views & iterators

    [[nodiscard]] channel_pop_iterator<channel> begin();

    [[nodiscard]] channel_pop_iterator<channel> end();

    [[nodiscard]] channel_push_iterator<channel> push_iterator();

    [[nodiscard]] channel_read_view<channel> read_view();

    [[nodiscard]] channel_write_view<channel> write_view();


    // Other functons

    size_t clear();

    void close();

    [[nodiscard]] bool is_read_closed();

    [[nodiscard]] bool is_write_closed();
};


// void messages
template<bool is_buffered,
        template<typename...> typename BufferQueue = std::queue,
        template<typename...> typename OpQueue = std::deque>
struct channel<is_buffered, void, BufferQueue, OpQueue> {
    using value_type = void;

    // Constructor, Assignment, Destructor
    channel() = default;
    explicit channel(size_t max_buffer_size);

    channel(channel&& other) noexcept;
    channel(const channel& other) = delete;

    ~channel();

    channel& operator=(const channel&) = delete;
    channel& operator=(channel&&) = delete;

    // pop
    template<wait_type wtype, typename... Args>
    [[nodiscard]] bool pop_wt(
        Args&& ... args
    );

    [[nodiscard]] bool pop();

    [[nodiscard]] bool try_pop();

    template<typename Rep, typename Period>
    [[nodiscard]] bool try_pop_for(
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] sbool try_pop_until(
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // pop_func
    [[nodiscard]] channel_op_status pop_func_wt(
        const std::function<void()>&,
        Args&& ... args
    );

    [[nodiscard]] channel_op_status pop_func(
        const std::function<void()>&
    );

    [[nodiscard]] channel_op_status try_pop_func(
        const std::function<void()>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] channel_op_status try_pop_func_for(
        const std::function<void()>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] channel_op_status try_pop_func_until(
        const std::function<void()>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // push
    template<wait_type wtype, typename... Args>
    [[nodiscard]] bool push_wt(
        Args&& ... args
    );

    [[nodiscard]] bool push();

    [[nodiscard]] bool try_push();

    template<typename Rep, typename Period>
    [[nodiscard]] std::optional<T> try_push_for(
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] std::optional<T> try_push_until(
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // push_func
    [[nodiscard]] channel_op_status push_func_wt(
        const std::function<void()>&,
        Args&& ... args
    );

    [[nodiscard]] channel_op_status push_func(
        const std::function<void()>&
    );

    [[nodiscard]] channel_op_status try_push_func(
        const std::function<void()>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] channel_op_status try_push_func_for(
        const std::function<void()>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] channel_op_status try_push_func_until(
        const std::function<void()>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // views & iterators

    [[nodiscard]] channel_pop_iterator<channel> begin();

    [[nodiscard]] channel_pop_iterator<channel> end();

    [[nodiscard]] channel_push_iterator<channel> push_iterator();

    [[nodiscard]] channel_read_view<channel> read_view();

    [[nodiscard]] channel_write_view<channel> write_view();


    // Other functons

    size_t clear();

    void close();

    [[nodiscard]] bool is_read_closed();

    [[nodiscard]] bool is_write_closed();
};
select
[[nodiscard]] auto operator>>(channel<is_buffered, T, ...>&, const std::function<void(T&&)>);
[[nodiscard]] auto operator<<(channel<is_buffered, T, ...>&, const std::function<T()>);

[[nodiscard]] auto operator>>(channel<is_buffered, void, ...>&, const std::function<void()>);
[[nodiscard]] auto operator<<(channel<is_buffered, void, ...>&, const std::function<void()>);

[[nodiscard]] auto operator>>(channel<is_buffered, T, ...>&, T&);
[[nodiscard]] auto operator<<(channel<is_buffered, T, ...>&, T);

// copper::_ or copper::voidval for an element of voidval_t.
[[nodiscard]] auto operator>>(channel<is_buffered, void, ...>&, voidval_t);
[[nodiscard]] auto operator<<(channel<is_buffered, void, ...>&, voidval_t);

template<typename... Ops>
[[nodiscard]] channel_op_status select(
    Ops&& ... ops
);

template<typename... Ops>
[[nodiscard]] channel_op_status try_select(
    Ops&& ... ops
);

template<typename Rep, typename Period, typename... Ops>
[[nodiscard]] channel_op_status try_select_for(
    const std::chrono::duration<Rep, Period>& rel_time, Ops&& ... ops
);

template<typename Clock, typename Duration, typename... Ops>
[[nodiscard]] channel_op_status
try_select_until(
    const std::chrono::time_point<Clock, Duration>& timeout_time, Ops&& ... ops
);


template<typename... Args>
[[nodiscard]] channel_op_status vselect(
    Args&& ... args
);

template<typename... Args>
[[nodiscard]] channel_op_status try_vselect(
    Args&& ... args
);

template<typename Rep, typename Period, typename... Args>
[[nodiscard]] channel_op_status try_vselect_for(
    const std::chrono::duration<Rep, Period>& rel_time, Args&& ... args
);

template<typename Clock, typename Duration, typename... Args>
[[nodiscard]] channel_op_status
try_vselect_until(
    const std::chrono::time_point<Clock, Duration>& timeout_time, Args&& ... args
);
views
template<typenameChannelT>
struct channel_read_view {
using value_type = typename ChannelT::value_type;

    explicit channel_read_view(ChannelT& channel);

    // pop

    template<wait_type wtype>
    [[nodiscard]] auto pop_wt(
        Args&& ... args
    );

    [[nodiscard]] auto pop();

    [[nodiscard]] auto try_pop();

    template<typename Rep, typename Period>
    [[nodiscard]] auto try_pop_for(
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] auto try_pop_until(
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // pop_func

    template<wait_type wtype>
    [[nodiscard]] auto pop_func_wt(
        const std::function<void(value_type&&)>&
    );

    [[nodiscard]] auto pop_func(
        const std::function<void(value_type&&)>&
    );

    [[nodiscard]] auto try_pop_func(
        const std::function<void(value_type&&)>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] auto try_pop_func_for(
        const std::function<void(value_type&&)>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] auto try_pop_func_until(
        const std::function<void(value_type&&)>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // other functions

    [[nodiscard]] channel_pop_iterator<ChannelT> begin();

    [[nodiscard]] channel_pop_iterator<ChannelT> end();

    [[nodiscard]] channel_read_view<ChannelT> read_view();
}:

template<typename ChannelT>
struct channel_write_view {
using value_type = typename ChannelT::value_type;

    explicit channel_write_view(ChannelT& channel) : _channel(channel) {}

    // push
    template<wait_type wtype, typename... Args>
    [[nodiscard]] bool push_wt(
        value_type,
        Args&& ... args
    );

    [[nodiscard]] bool push(
        value_type
    );

    [[nodiscard]] bool try_push(
        value_type
    );

    template<typename Rep, typename Period>
    [[nodiscard]] std::optional<value_type> try_push_for(
        value_type,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] std::optional<value_type> try_push_until(
        value_type,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // push_func
    [[nodiscard]] channel_op_status push_func_wt(
        const std::function<value_type()>&,
        Args&& ... args
    );

    [[nodiscard]] channel_op_status push_func(
        const std::function<value_type()>&
    );

    [[nodiscard]] channel_op_status try_push_func(
        const std::function<value_type()>&
    );

    template<typename Rep, typename Period>
    [[nodiscard]] channel_op_status try_push_func_for(
        const std::function<value_type()>&,
        const std::chrono::duration<Rep, Period>& rel_time
    );

    template<typename Clock, typename Duration>
    [[nodiscard]] channel_op_status try_push_func_until(
        const std::function<value_type()>&,
        const std::chrono::time_point<Clock, Duration>& timeout_time
    );

    // Other functions

    [[nodiscard]] channel_push_iterator<ChannelT> push_iterator();

    [[nodiscard]] channel_write_view write_view();

    void close();

    size_t clear();
};
iterators
template<typename ChannelT>
struct channel_pop_iterator {
    using value_type = typename ChannelT::value_type;
    using reference = std::add_lvalue_reference_t<value_type>;
    using pointer = value_type*;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::input_iterator_tag;

    channel_pop_iterator();

    explicit channel_pop_iterator(ChannelT& channel);

    auto operator++(int)&;

    channel_pop_iterator& operator++()&;

    [[nodiscard]] reference operator*() const;

    [[nodiscard]] pointer operator->() const;

    [[nodiscard]] bool operator==(const channel_pop_iterator& other) const;

    [[nodiscard]] bool operator!=(const channel_pop_iterator& other) const;
};

template<typename ChannelT>
struct channel_push_iterator {
    using value_type = _pusher;
    using reference = value_type&;
    using pointer = void;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::output_iterator_tag;

    channel_push_iterator();

    explicit channel_push_iterator(ChannelT& channel);

    channel_push_iterator operator++(int)&;

    channel_push_iterator& operator++()&;

    [[nodiscard]] _pusher operator*() const;
};