Asked  7 Months ago    Answers:  5   Viewed   32 times

Why does this code compile?

fn get_iter() -> impl Iterator<Item = i32> {
    [1, 2, 3].iter().map(|&i| i)
}

fn main() {
    let _it = get_iter();
}

[1, 2, 3] is a local variable and iter() borrows it. This code should not compile because the returned value holds a reference to a local variable.

 Answers

72

In your example, [1, 2, 3] is not treated as local variable, but as static one!

Let's take a look at this code:

fn foo() -> &'static [i32] {
    &[1, 2, 3]
}

This works!

Some time ago, RFC 1414: Rvalue Static Promotion was merged: "Promote constexpr rvalues to values in static memory instead of stack slots". This means that basically all literals you write can live forever. Thus, things like let _: &'static i32 = &42; also work!

If we avoid using a literal array, we can see the expected error:

fn bar() -> impl Iterator<Item = i32> {
    vec![1, 2, 3].iter().map(|&i| i)
}

Here we get the "v does not live long enough" error.

This isn't limited to integers or arrays; it applies broadly to any literal that is composed solely of literals:

fn promote_integer() -> &'static i32 {
    &42
}
fn promote_float() -> &'static f64 {
    &42.42
}
fn promote_str() -> &'static str {
    "Hello World!"
}
struct Foo(char);

fn promote_struct() -> &'static Foo {
    &Foo('x')
}

Beyond literals, this also works for a tiny number of functions in the standard library, but these were likely a mistake. Deciding on if the result of arbitrary const functions can be automatically promoted to static is still an open topic.

Tuesday, June 1, 2021
 
Webroots
answered 7 Months ago
66

Why does this code work? I'm using C# 8 with Visual Studio 2019.

You've answered your own question! It's because you're using C# 8.

The rule from C# 1 through 7 was: a simple name cannot be used to mean two different things in the same local scope. (The actual rule was slightly more complex than that but describing how is tedious; see the C# specification for details.)

The intention of this rule was to prevent the sort of situation that you're talking about in your example, where it becomes very easy to be confused about the meaning of the local. In particular, this rule was designed to prevent confusions like:

class C 
{
  int x;
  void M()
  {
    x = 123;
    if (whatever)
    {
      int x = 356;
      ...

And now we have a situation where inside the body of M, x means both this.x and the local x.

Though well-intentioned, there were a number of problems with this rule:

  • It was not implemented to spec. There were situations where a simple name could be used as, say, both a type and a property, but these were not always flagged as errors because the error detection logic was flawed. (See below)
  • The error messages were confusingly worded, and inconsistently reported. There were multiple different error messages for this situation. They inconsistently identified the offender; that is, sometimes the inner usage would be called out, sometimes the outer, and sometimes it was just confusing.

I made an effort in the Roslyn rewrite to sort this out; I added some new error messages, and made the old ones consistent regarding where the error was reported. However, this effort was too little, too late.

The C# team decided for C# 8 that the whole rule was causing more confusion than it was preventing, and the rule was retired from the language. (Thanks Jonathon Chase for determining when the retirement happened.)

If you are interested to learn the history of this problem and how I attempted to fix it, see these articles I wrote about it:

https://ericlippert.com/2009/11/02/simple-names-are-not-so-simple/

https://ericlippert.com/2009/11/05/simple-names-are-not-so-simple-part-two/

https://ericlippert.com/2014/09/25/confusing-errors-for-a-confusing-feature-part-one/

https://ericlippert.com/2014/09/29/confusing-errors-for-a-confusing-feature-part-two/

https://ericlippert.com/2014/10/03/confusing-errors-for-a-confusing-feature-part-three/

At the end of part three I noted that there was also an interaction between this feature and the "Color Color" feature -- that is, the feature that allows:

class C
{
  Color Color { get; set; }
  void M()
  {
    Color = Color.Red;
  }
}

Here we have used the simple name Color to refer to both this.Color and the enumerated type Color; according to a strict reading of the specification this should be an error, but in this case the spec was wrong and the intention was to allow it, as this code is unambiguous and it would be vexing to make the developer change it.

I never did write that article describing all the weird interactions between these two rules, and it would be a bit pointless to do so now!

Saturday, June 26, 2021
 
mopsyd
answered 6 Months ago
61

I would say that requiring this to make the program ill-formed (that is, make this a compilation error) would complicate the standard considerably for little benefit. You'd have to exactly spell out in the standard when such cases shall be diagnosed, and all compilers would have to implement them.

If you specify too little, it will not be too useful. And compilers probably already check for this to emit warnings, and real programmers compile with -Wall_you_can_give_me -Werror anyway.

If you specify too much, it will be difficult (or impossible) for compilers to implement the standard.

Consider this class (for which you only have the header and a library):

class Foo
{
  int x;

public:
  int& getInteger();
};

And this code:

int& bar()
{
  Foo f;
  return f.getInteger();
}

Now, should the standard be written to make this ill-formed or not? Probably not, what if Foo is implemented like this:

#include "Foo.h"

int global;

int& Foo::getInteger()
{
  return global;
}

At the same time, it could be implemented like this:

#include "Foo.h"

int& Foo::getInteger()
{
  return x;
}

Which of course would give you a dangling reference.

My point is that the compiler cannot really know whether returning a reference is OK or not, except for a few trivial cases (returning a reference to a function-scope automatic variable or parameter of non-reference type). I don't think it's worth it to complicate the standard for that. Especially as most compilers already warn about this as a quality-of-implementation matter.

Tuesday, August 3, 2021
 
MM1
answered 4 Months ago
MM1
44

This is exactly the purpose of impl Trait, which has been available in stable Rust since version 1.26.

use std::ops::Deref;

impl<T> Outer<T> {
    fn get_inner_ref<'a>(&'a self) -> impl Deref<Target = T> + 'a {
        self.inner.borrow()
    }
}

The Rust compiler knows that the actual implementation is Ref<T> but lets you avoid having to write it explicitly, and callers of this function can only use functionality provided by the Deref trait.

As long as the actual value that you return is of a type that implements Deref<Target = T>, you are free to change that implementation later without breaking any code that uses it. For example, you could return &T or one of several other reference types, including your own custom type, as in the other linked question.

Thursday, August 5, 2021
 
Justin Charles
answered 4 Months ago
12

Right now "Rustc can't "deal" with conditional borrowing returns". See this comment from Gankro on issue 21906.

It can't assign a correct lifetime to the borrow if only one execution path terminates the loop.

I can suggest this workaround, but I'm not sure it is optimal:

fn c(i: &[u8]) -> Option<(usize, usize)> {
    Some((0, i.len()))
}

fn a(&mut self) -> &[u8] {
    let parse_result;
    loop {
        self.b();

        match Test::c(&self.v) {
            Some(r) => {
                parse_result = r;
                break;
            }
            _ => {}
        }
    }
    let (start, end) = parse_result;
    &self.v[start..end]
}

You can construct result of parsing using array indexes and convert them into references outside of the loop.

Another option is to resort to unsafe to decouple lifetimes. I am not an expert in safe use of unsafe, so pay attention to comments of others.

fn a(&mut self) -> &[u8] {
    loop {
        self.b();

        match Test::c(&self.v) {
            Some(r) => return unsafe{ 
                // should be safe. It decouples lifetime of 
                // &self.v and lifetime of returned value,
                // while lifetime of returned value still
                // cannot outlive self
                ::std::slice::from_raw_parts(r.as_ptr(), r.len())
            },
            _ => continue,
        }
    }
}
Thursday, August 26, 2021
 
crellee
answered 4 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share