Can inits of noncopyable types be throwing?

I want to model a struct that wraps access to a SQLite database as a non-copyable struct. Opening or creating a database can fail, so naturally, I want the init to be able to throw errors. However, this causes the following error: "Conditional initialization or destruction of noncopyable types is not supported; this variable must be consistently in an initialized or uninitialized state through every code path". This error doesn't explicitly forbid throwing initializations, and I couldn't find anything in the corresponding evolution proposal. Is there a way to make this work?

1 Like

It ought to be supported, but there is a known issue that causes this error to be raised. Let me see if I can look up the workaround.

3 Likes

I remember seeing this, but my attempt to reproduce it right now is not working. Do you have an example that triggers it?

Here's a somewhat minimal sample. The error goes away if you comment out the last two lines of the init.

struct SQLite: ~Copyable {
	let dbPath: String
	let db: OpaquePointer

	init(dbPath: String) throws {
		self.dbPath = dbPath

		var db: OpaquePointer? = nil
		try sqlite {
			sqlite3_open_v2(
				dbPath,
				&db,
				SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
				nil
			)
		}

		self.db = db!
		try sqlite { sqlite3_busy_timeout(db, 5 * 1000 /* 5s */) }
        try sqlite { sqlite3_extended_errcode(db) }
	}
}

func sqlite(_ fn: () -> Int32) throws {
	let result = fn()
	if result != SQLITE_OK {
        throw SQLiteError.generic(result)
	}
}

enum SQLiteError: Error, Equatable {
    case generic(Int32)
}
1 Like

Can you move the stored property initializations to the end of the function? In that way all throwing sites would occur before any part of the struct is initialized.

Interestingly, it's the interleaving of throwing sites and property storing that is the issue. This fails:

  init(dbPath: String) throws {
    self.dbPath = dbPath

    let db: OpaquePointer? = nil
    try sqlite { 1 }

    self.db = db!

    try sqlite { 2 }
    try sqlite { 3 }
  }

But this works:

  init(dbPath: String) throws {
    self.dbPath = dbPath

    let db: OpaquePointer? = nil
    try sqlite { 1 }
    try sqlite { 2 }
    try sqlite { 3 }

    self.db = db!
}
1 Like

That fixed it. Thanks! Perhaps the error message could hint at this. Maybe even with notes pointing to the property initializations.

I'm pretty sure there's a compiler bug there =)

1 Like