In the example you give
void open_db(db* conn)
{
// Open database connection and store it in the conn
}
int main()
{
db* conn;
open_db(conn);
}
the variable, conn
, in main
is an uninitialized pointer.
You then pass a copy of that to open_db
. You are not passing the address of the pointer, you are passing the uninitialized value as an address-of.
This requires a read of an uninitialized address in order to populate the copy used in db_conn
.
The compiler is free to recognize this and either perform the read with potential undefined behavior consequences (it is feasible that the program might crash performing such a read) or the compiler might elide the copy and just let db_conn
s conn
parameter be differently undefined.
Based on other comments I've read, I believe you're trying to be clever and say "Aha! But I always initialize conn
inside db_conn
and never read it without initializing it".
Ok... That's ... perverse.
void db_conn(db* conn)
{
db* new_conn = db_connection_helper();
if (!new_conn) {
log_error("Couldn't open database");
return;
}
log_success("Opened database");
conn = new_conn;
configure_db_connection(conn); // first read: guaranteed initialized
setup_stored_procedures(conn);
}
In this function, conn
was passed by value, so conn
is a copy of whatever argument was passed to us. Any assignments made to it in the body of db_conn
are invisible to the caller.
Indeed, the optimizer is quite likely to treat this code
conn = new_conn;
configure_db_connection(conn); // first read: guaranteed initialized
setup_stored_procedures(conn);
as
configure_db_connection(new_conn); // first read: guaranteed initialized
setup_stored_procedures(new_conn);
We can easily see this in the assembly
typedef struct db_t {} db;
extern db* db_conn_helper();
extern void db_configure(db*);
void db_conn1(db* conn)
{
db* new_conn = db_conn_helper();
if (!new_conn)
return;
conn = new_conn;
db_configure(conn);
}
void db_conn2(db* conn)
{
db* new_conn = db_conn_helper();
if (!new_conn)
return;
db_configure(new_conn);
}
produces
db_conn1(db_t*):
subq $8, %rsp
call db_conn_helper()
testq %rax, %rax
je .L1
movq %rax, %rdi
addq $8, %rsp
jmp db_configure(db_t*)
.L1:
addq $8, %rsp
ret
db_conn2(db_t*):
subq $8, %rsp
call db_conn_helper()
testq %rax, %rax
je .L5
movq %rax, %rdi
addq $8, %rsp
jmp db_configure(db_t*)
.L5:
addq $8, %rsp
ret
So this means that if your code tries to use db
in main, you're still seeing undefined behavior:
int main() {
db* conn; // uninitialized
db_conn(conn); // passes uninitialized value
// our 'conn' is still uninitialized
query(conn, "SELECT \"undefined behavior\" FROM DUAL"); // UB
}
Perhaps you either mean't
conn = db_conn(); // initializes conn
or
db_conn(&conn); // only undefined if db_conn tries to use *conn
this requires db_conn
to take db**
.
db* conn => uninitialized
db** &conn => initialized pointer to an uninitialized db* pointer