[C++] sqlite3_open() double-pointer passing.

I'm sorta new at C++, and I'm trying to use it more in favor of other languages since it's so ubiquitous. Right now I'm trying to parse a text file and dump information into a SQLite Database for later querying.

Where I'm running into a problem is that the sqlite3_open() statement seems to take a double pointer to a sqlite3 object. I'm not entirely familiar with how to use pointers in the first place, so I've been tweaking around with code to desperately make it just work.

I've finally gotten it to compile, but I feel like something about this is wrong. The arguments, according to documentation, for the sqlite3_open() function are sqlite3_open(const char *filename, sqlite3 **ppDb);. And this is what I finally got to work. Is this how it should be done, or am I missing something?

sqlite3 *db;
sqlite3_open(DB_NAME, &db);

Thanks for any help.

7 comments · 100% upvoted · other discussions

comments

dacian88

no that's fine, in terms of types

&(T) -> T*

if you replace T with your type (T=sqlite3*)

&(sqlite3*) -> sqlite3**
Rhomboid

SQLite is a C library, and C doesn't have references, so any time a function wants to modify a caller's argument, a layer of indirection must be added. If the caller has some value of type T, the function needs to take T *, and the caller will generally pass the address of that T object when calling the function (though not always; the caller could otherwise already have a pointer to it from other means.)

This applies no matter what type T is. In your example, T is sqlite3 *, so the function takes T *, or sqlite3 **.

In other words, the caller is saying, "pass me the address of your pointer so that I can fill it in and make it point to a valid instance of sqlite3 that I'm about to allocate and initialize." If the function had taken sqlite *, there would be no way for it to modify the caller's pointer, just as a function that takes int has no way of modifying the caller's int value; that would be a completely useless function since the caller's pointer is uninitialized and can't be read from or dereferenced, only written to, so the argument passed would be completely useless.

Generally speaking it would be easier for the function to just return a pointer to the newly created object, along the lines of malloc(), but in that case you have very limited capability to signal errors. You can either return a valid pointer or NULL; there's no way to signal what went wrong, which is why the standard library has to rely on techniques like the errno global variable which is an unfortunate mess. By taking the address of a pointer and filling it in, this function can use the return value to signal not just success or failure, but the reason for failure, which you should know from the documentation contains a rich set of descriptive failure reasons.

MythGuy [OP]

Thanks for the info dump. So if I'm understanding what you said correctly, my variable declaration hasn't set up a valid sqlite3 structure, so when I pass db into sqlite3_open, its needs the reference to a pointer of the object, so that it can redirect it to a new location in memory where the function will have created a valid sqlite3 struct. It seems the function is treating db as immutable, and is just reinitializing the variable in a new area in memory and shifting the pointer to the new area.

Am I following?

Rhomboid

my variable declaration hasn't set up a valid sqlite3 structure

Er... I wouldn't quite put it that way. You've declared a pointer that is capable of pointing to a sqlite3 structure, but that's it. Declaring a pointer to some type T does not have anything to do with actually creating an object of type T. There's no structure to initialize.

In fact, you couldn't create a sqlite3 struct if you wanted to, because this library uses a technique to hide the implementation details of what's inside that struct. In the public headers, sqlite3 is never defined, only forward-declared as being the name of a type. That means that the type sqlite3 * is an opaque pointer. It's opaque because the compiler hasn't been told the layout or contents of the struct being pointed to, so any action that would need that information is invalid. For example, you can't dereference such a pointer, or try to access any fields of the struct, or try to ask for the size of the struct, etc. In essence, a pointer to sqlite3 is completely unusable by you except as a handle or token that you can pass to the library. The library knows the layout of the struct at the other end of the pointer, but that information isn't present in the public headers and so it isn't available to you as the client of the library. This technique saves the library implementer from having to worry about ABI issues and backwards compatibility. If no client code can ever manipulate the struct, then its layout is free to change without harm between versions. It also prevents people from peeking inside the internals of the library and modifying things that they shouldn't.

its needs the reference to a pointer of the object, so that it can redirect it to a new location in memory where the function will have created a valid sqlite3 struct

Again, C does not have references so I would avoid use of that word. The function needs the address of the pointer so it can modify that pointer to point at the newly created sqlite3 struct.

It seems the function is treating db as immutable, and is just reinitializing the variable in a new area in memory and shifting the pointer to the new area.

No. db is very much mutable. It is being modified by the function. Again, db is only a pointer to a struct, it does not contain (and cannot contain) any part of the struct itself. "shifting the pointer" means completely rewriting the contents of db, because a pointer is all that db is; there is nothing else to it.

MythGuy [OP]

Agh! I think I'm getting hung up on thinking that db would contain the sqlite3 struct because I'm used to variables actually holding the information they are needed for. Again, not familiar with using pointers, so it's taking some work to try to wrap my brain around it.

I do think I'm starting to get it though. The struct is hidden to me, only usable through sqlite3 API calls. Because of this, most API functions require passing a sqlite3 pointer (sqlite3*) so that it has the memory location for the struct that it needs to work with. The initial open call requires a pointer to the pointer so that it can be changed to point to a new location in memory where a new sqlite3 struct is because is doesn't currently point to a valid struct yet (since it's initially pointing to a location in memory that's not been touched yet by the sqlite API).

I think I managed to think through that without conflating to pointer with the actual struct.

Rhomboid

Correct. This type of handle pattern is very common in C libraries, so it's good to understand it. For example, glance at a few of the libcurl functions and you'll see that they all deal with this CURL * type as a handle that gets passed around. In that case, they decided to have the "init" function simply return the pointer instead of filling in a caller's variable as I alluded to earlier. But in this case, there's much less that can fail when merely initializing a handle; most of the meaningful failures occur when setting options or performing the action, and those functions return a status code.

[deleted]

Don't think of things as "double pointers" - there are really no such types. What you have here:

  sqlite3 *db;

is a pointer to a sqlite3 struct. and what you have in the function call:

 sqlite3_open(DB_NAME, &db);

is a pointer to a pointer to a sqlite3 struct. The db pointer is set by the function call to point to a sqlite3 struct that the open function allocates.