Implementing trait by forwarding all trait methods to inner value

Continuing the discussion from Problem with "duplicating" a (Tcp)Stream and lifetimes:

I repeatingly stumble upon a pattern where I need to implement a trait by forwarding all methods of the trait to an inner value and/or through dereference.

Case 1:

In order to split a TcpStream (which implements both Read and Write) into an owned read and an owned write part (each of them implementing Read or Write respectively), I need to store the TcpStream in an Arc and create two newtypes which implement the Read and Write traits by forwarding all method calls to the borrowed, dereferenced inner value (that is (&*self.0)) (as &TcpStream also implements Read and Write).

Note: If I only want to call methods on my newtype, it would be sufficient to implement std::ops::Deref, but I want to pass the newtype to a function f<R: Read>(reader: R), which requires that the newtype actually implements Read (or Write, respectively).

The newtypes would look as follows:

use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;

struct TcpStreamReadHalf(Arc<TcpStream>);
struct TcpStreamWriteHalf(Arc<TcpStream>);

impl Read for TcpStreamReadHalf { /* lots of boilerplate code here */ }
impl Write for TcpStreamWriteHalf { /* lots of boilerplate code here */ }

As I pointed out in the referenced discussion thread:

Case 2:

When I define a trait and implement it on str, then it is not implemented by String, even if my trait only operates on &self. This pushed me to either implement my trait twice (for str as well as for String) or to resort to an ugly call syntax:

Case 3:

I had a trait (let's call it TraitOne) that abstracts I/O operations on some data structure. A concrete type implemented it in memory and a different type will implement it by storing the data in a database. Both types will implement TraitOne in order to allow access. Then, I created another trait (TraitTwo), which offers a snapshot function that was planned to return some sort of non-mutable handle to access the current state:

trait TraitTwo {
    fn snapshot(&self) -> TraitOne;
}

However, I later added a memory-based storage that holds the data structure in an Arc<RwLock<T>> (where T: TraitOne) and that provides a snapshot function, returning an RwLock guard (RwLockReadGuard) as smart-pointer to the memory-based structure. In order to be able to implement TraitTwo as defined above, I need to implement TraitOne on a newtype over RwLockReadGuard<T>, which again requires me to provide implementations on all methods of TraitOne, resulting in lots of boilerplate code.

I circumvented the problem by using Nightly Rust with Generic Associated Types (#![feature(generic_associated_types)]) and writing my TraitTwo as follows:

trait TraitTwo {
    type Inner: TraitOne;
    type Snapshot<'a>: Deref<Target = Self::Inner>;
    fn snapshot(&self) -> Self::Snapshot<'_>;
}

That avoids boilerplate code in case of my RwLock guard, as I can simply set type Snapshot<'a> = RwLockReadGuard<'a, MemoryBasedType>;, but it makes my whole interface more complicated, as I will have a separate handle object that I need to dereference when I need a structure that implements TraitOne.

Now that explanation was a bit fuzzy. If it caused confusion, just ignore Case 3 and look at the other two cases above.

The common pattern:

The common pattern is that I had several cases where I needed to implement a trait just by forwarding all method calls to an inner value or through dereference, or a combination thereof.

I found the delegate crate, but also there, I seem to have to list all methods (and if methods with default implementations are added in future, I'd miss them and don't even get a warning!).

Is there already an existing solution to this problem?

I don't think it's currently possible to automate this. Even the authors of #[derive] macros have to know which functions of a trait they should generate – this part is not automatic. The problem is that knowing what functions a trait has requires type checking, but at that time, we are way past the point of being able to generate additional code.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.