0

I've run into this problem a few times now in Rust. For a minimal example, consider the following:

struct Config {
    aa: u32,
    bb: f64,
    cc: i32,
}

struct Inner<'a> {
    // inner's methods need to reference aa and bb
    config: &'a Config,
}

struct Outer<'a> {
    // outer's methods need to reference aa, bb, and cc
    config: Config,
    inner: Inner<'a>,
}

fn main() {
    // what would actually happen here is we'd read a config file to populate 'config'

    let config = Config {aa: 5, bb: 0.3, cc: -8};
    let inner = Inner {config: &config};

    // this works fine, although now we have two copies of the config...
    let outer = Outer {
        config: Config {aa: 5, bb: 0.3, cc: -8},
        inner
    };

    // do some stuff with outer
}

This compiles and would achieve the desired behaviour, unless outer's config was ever modified; then we'd have different configs driving the outer and inner components. But suppose I want to have a constructor function for Outer (which I do). If I write something like this, it won't compile:

impl<'a> Outer<'a> {
    fn new() -> Outer<'a> {
        // what would actually happen here is we'd read a config file to populate 'config'
        let config = Config {aa: 5, bb: 0.3, cc: -8};

        let inner = Inner {config: &config};

        return Outer {config, inner};
    }
}

There's a chicken-and-egg problem here. Outer needs to be instantiated with an existing inner and config, but the Inner type needs a reference to the same config. inner has to be built with a reference to the config inside outer, so it needs to be instantiated after outer; but outer needs to be instantiated with an existing Inner struct, so inner needs to be instantiated before outer.

If these three structs could be instantiated at the same time, there would be no problem. All references would last as long as they need to, and all the program logic would run fine under Rust. But I don't think Rust allows such a thing.

I've found a few ways of solving this, all of which seem kludgey and inelegant. I could:

  1. Use a reference-counting pointer for config:
struct Inner {
    config: Rc<Config>,
}
struct Outer {
    config: Rc<Config>,
    inner: Inner,
}

I don't like this because I don't actually need any reference-counting features while using the Outer struct; and there's no advantage to having config on the heap. It's logical for config to be owned by outer, and it shouldn't ever last any longer than outer.

  1. Make Outer have an option of its Inner so that it can be instantiated with None, then set with Some(inner):
struct Outer<'a> {
    config: Config,
    inner: Option<Inner<'a>>,
}

impl<'a> Outer<'a> {
    fn new() -> Outer<'a> {
        let config = Config {aa: 5, bb: 0.3, cc: -8};

        let outer = Outer {config, None};
        let inner = Inner {config: &outer.config};
        outer.inner = Some(inner);
        return outer;
    }
}

I don't like this because in the logic this program is supposed to implement, there's no case where an Outer should ever lack an Inner. The only time it ever needs to be None is at this point in new(). All the other methods of Outer must now needlessly call unwrap() on something that will always be Some.

Is there some elegant idiomatic way of dealing with this issue in Rust other than what I've outlined here?

  • 1
    In this particular case, I can't see why `Inner` doesn't own the `Config` and `Outer` access it via `self.inner.config`? – eggyal Jun 30 '21 at 14:57
  • *no advantage to having `config` on the heap* — I disagree. Program configuration that is created exactly once in `main` and never needs to be mutated is one of the few times I reach for [`Box::leak`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak). You can then pass around the `&'static Config` to your heart's desire. – Shepmaster Jun 30 '21 at 15:06
  • @eggyal That's another solution that works but is distasteful. Outer is the only part that needs all of the configuration, while Inner doesn't. Logically the config "belongs" to `Outer`, and Inner just provides some shared functionality used by multiple Outer types. – Andrew Holliday Jun 30 '21 at 15:09
  • 1
    Perhaps then that is indicative that the configuration should be split into separate structures: `Outer` owning those that only it requires, and `Inner` owning those that both require. `Outer` would still access the latter via its contained `Inner`. But of course this may be leaking an implementation detail into some externally observable abstraction. – eggyal Jun 30 '21 at 15:11
  • @Shepmaster I'm not familiar with `Box::leak`. I'll have to take a look. Thanks. – Andrew Holliday Jun 30 '21 at 15:18

0 Answers0