Accept IntoIterator<Item = T> where T must not be a reference

Consider the following function:

fn foo<T>(iter: impl IntoIterator<Item = T>) {
    //
}

If I pass in an appropriate container object by value then within foo the type of T will be the owned (non-reference) type of that containers owned elements. E.g.:

let a = [true, false, true];
foo(a); // Within foo T is bool.

let v = vec![1, 2, 3];
foo(v); // Within foo T is i32.

However, if I pass in an object/iterator that produces references then within foo the type of T will be the reference to the iterators element type:

let a = [true, false, true];
foo(a.iter()); // Within foo T is &bool.

My question is, is there a way to constrain the type of T so that foo will only accept iterators that provide owned access to their elements (i.e. the first type above)? I'm thinking about this in the context of a function that wants to take ownership of the elements in the provided iterator, perhaps by adding them into it's own new collection. However if the iterator only provides references then it can't do that, it can only create a new collection of references to those elements (as far as I know anyway).

Thanks

You could add where T: 'static, but that won't actually prevent references, just the presence of a non-'static lifetime (so Item = &'static str would still be accepted, say).

There's no trait for ownership (it's hard to even nail down a definition; it's more of a human concept). Other types than references can be borrowing types, too. What makes you care if someone utilizes your generic function with non-owned items? If it all works with borrowed items, then, well... why not?

6 Likes

IMO, lifetime is just a part of the type system. If a function is generic over T, then it should rely on the trait bounds on T. A trait serves as a protocol between the function and its callers, any type implementing the trait should nicely fit into your function, no matter it's primitive type, object or reference. (Yes, it's perfectly okay to implement your custom trait for a primitive type.)

Consider a type with its new function:

struct Dummy<T> {
    vec: Vec<T>,
}

impl<T> Dummy<T> {
    fn new(iter: impl IntoIterator<Item=T>) -> Self {
        let mut vec = Vec::new();
        for item in iter {
            vec.push(item);
        }
        Self { vec }
    }
}

When T: Display, I can define:


use std::fmt::Display;

impl<T: Display> Dummy<T> {
    fn display(&self) {
        for item in self.vec.iter() {
            println!("{}", item);
        }
    }
}

Then I can do

let a = format!("a string");
let b = format!("b string");
let c = format!("c string");
  
let v: Dummy<String> = Dummy::new([a, b, c]);
v.display();

As well as:

let d = format!("d &str");
let e = format!("e &str");
let f = format!("f &str");
  
let v: Dummy<&'_ str> = Dummy::new([d.as_str(), e.as_str(), f.as_str()]);
v.display();

In either case, Dummy::new and Dummy::display are completely unaware of whether T is reference or not, or how long are they borrowed. For Dummy::new, it just know T is Sized, so its size is known at compile time, and the container Vec<T> can be created for T, and Vec::new::<T>() is callable. For Dummy::display, it just know T is std::fmt::Display, so it has a fmt method defined on T, which is required by macro println!.

4 Likes

This seems kind of ill-defined[1] condition to me.

Ownership is not a property of a type, it is a relationship between an owner and owned object, for example:

This array owns the numbers (let's forget about Copy for now)

let array1 = [1, 2, 3];

This array owns the references, through which it borrows the local variables

let a = 1;
let b = 2;
let c = 3;
let array2 = [&a, &b, &c];

Now which of the following do you want to support and why?

// you said you want this
foo(array1);  // T = i32 and that is what is in the array

// but not this
foo(array1.iter());  // T = &i32 but the array contains i32

// so how about this?
foo(array2);  // T = &i32 and that is what is in the array

// probably not this?
foo(array2.iter());  // T = &&i32 but the array contains &i32

// and how about ...
foo(array1.iter().cloned());  // T = i32 but the array is not consumed
foo(array2.iter().cloned());  // T = &i32 but the array is not consumed

If you are asking for an iterator that transfers ownership from a collection to the caller, that is not possible simply because the iterator may not come from a collection.

Or yet in other words, an iterator always owns its Items. And that is all you can tell about ownership when all you have is that iterator. Relationship between those Items and the original collection (if there is any) is simply out of your reach.


  1. or trivial, depending on how you interpret it ↩︎

3 Likes