Constant XXX captured by a closure before being initialized

Within an initializer:

    if maxSplits > 0 {
        cachedStart = self.base.firstSplitRange(omittingEmptySubsequences: omitEmptySubsequences, whereSeparator: self.isSeparator)
    } else if !self.base.isEmpty || !omitEmptySubsequences {
        cachedStart = self.base.startIndex..<self.base.endIndex
    } else {
        cachedStart = nil
    }

maxSplits is an initializer argument, while the other variables are already-initialized members. I get an "Constant 'self.cachedStart' captured by a closure before being initialized" on the use of omitEmptySubsequences in the else-if line. If I replace that with the argument it was initialized from (omittingEmptySubsequences), it works. Why? And it doesn't seem universal; uses of already-initialized base, isSeparator, and the first mention of omitEmptySubsequences were OK. The second use was part of a decision, but base was also in that test expression.

Any chance you could share the whole init or at least a little bit more code context?!

Where does omitEmptySubsequences originate from?

struct LazySplitCollection<Base: Collection> {
    let base: Base
    let maxSplitCount: Int
    let omitEmptySubsequences: Bool
    let isSeparator: (Base.Element) -> Bool
    let cachedStart: Range<Base.Index>?

    init(_ base: Base, maxSplits: Int, omittingEmptySubsequences: Bool, whereSeparator isSeparator: @escaping (Base.Element) -> Bool) {
        precondition(maxSplits >= 0)

        self.base = base
        maxSplitCount = maxSplits
        omitEmptySubsequences = omittingEmptySubsequences
        self.isSeparator = isSeparator

        if maxSplits > 0 {
            cachedStart = self.base.firstSplitRange(omittingEmptySubsequences: omitEmptySubsequences, whereSeparator: self.isSeparator)
        } else if !self.base.isEmpty || !omittingEmptySubsequences {
            cachedStart = self.base.startIndex..<self.base.endIndex
        } else {
            cachedStart = nil
        }
    }
}

The state of the else-if line is after the fix. Changing the last term back to omitEmptySubsequences should bring back the error. Collection.firstSplitRange is a custom extension method.

Okay I found the issue. The error is actually correct and it's even emitted at the right place. You are indeed capturing self before the last value could be initialized properly.

This is caused by this:

extension Bool {
    public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool
}

You could restructure it to something like this, but it's not prettier than your workaround:

switch maxSplits {
case 1...:
  self.cachedStart = self.base.firstSplitRange(omittingEmptySubsequences: omitEmptySubsequences, whereSeparator: self.isSeparator)
case _ where !self.omitEmptySubsequences:
  fallthrough
case _ where !self.base.isEmpty:
  self.cachedStart = self.base.startIndex ..< self.base.endIndex
default:
  self.cachedStart = nil
}
1 Like

This feature was supposed to copy (Objective-)C(++)'s short-circuit behavior on && and ||. Those languages make them work by fiat with compiler magic. We actually have implementations based on a language feature. But not using magic has consequences; anything that can't handle (auto-)closures will choke. I just tried the original code, but swapped the omit and base terms, and sure enough, the error returned on base (which is now the second term, i.e. the one under autoclosure.)

I just tried another workaround. For certain tests, we can use a comma as an AND term. Further, it's a built-in, so it uses compiler magic and not an @autoclosure, and so all terms can use already-initialized members. But remember your Computer Science rules on switching between OR and AND chains:

    if maxSplits > 0 {
        cachedStart = self.base.firstSplitRange(omittingEmptySubsequences: omitEmptySubsequences, whereSeparator: self.isSeparator)
    } else if self.base.isEmpty, omitEmptySubsequences {
        cachedStart = nil
    } else {
        cachedStart = self.base.startIndex..<self.base.endIndex
    }

(You flip the AND/OR, flip the NOTs, but the answer is the flip of the original.)

The problem was also observed in Autoclosure of self in initializers, and reported as SR-944 Cannot use 'self' in RHS of && before all properties are initialized.

2 Likes