3
votes

I understand that in the C++ realm it is advocated to use smart pointers. I have a simple program as below.

/* main.cpp */
#include <iostream>
#include <memory>
using namespace std;

/* SQLite */
#include "sqlite3.h"

int main(int argc, char** argv)
{
    // unique_ptr<sqlite3> db = nullptr; // Got error with this
    shared_ptr<sqlite3> db = nullptr;

    cout << "Database" << endl;
    return 0;
}

When I compile with unique_ptr line got an error message:

error C2027: use of undefined type 'sqlite3'
 error C2338: can't delete an incomplete type

When I compile with shared_ptr line it is successful. From several questions and answers my understanding is that unique_ptr should be preferred as I do not intended to have objects sharing resources. What is the best solution in this case? Use shared_ptr or go back to the old approach of bare pointers (new/delete)?

4
You'll find that new/delete approach is not an option either.eerorika

4 Answers

7
votes

The general approach is in @SomeProgrammerDudes's answer (accept it). But to address your concerns I'm posting this.

You shouldn't go back to raw new and delete. Neither because sqlite3 is an opaque type nor because the overhead of std::shared_ptr. You use, as the other answer specified, a std::unique_tr.

The only difference is how you setup the custom deleter. For std::unique_ptr it's part of the type definition, not a run-time parameter. So you need to do something like this:

struct sqlite3_deleter {
  void operator()(sqlite3* sql) {
    sqlite3_close_v2(sql);
  }
};

using unique_sqlite3 = std::unique_ptr<sqlite3, sqlite3_deleter>;
4
votes

sqlite3 is an opaque structure (much like FILE from C). All you have is its declaration, not its definition. That means you can't use it in a std::unique_ptr directly without a custom deleter.

1
votes
#include <memory>
#include <stdexcept>

/* sqlite 3 interface */
struct sqlite3 {};
extern void sqlite3_close(sqlite3*);
extern int sqlite3_open(sqlite3**);

/* our boilerplate */
struct closer
{
    void operator()(sqlite3* p) const
    {
        sqlite3_close(p);
    }
};

using sqlite3_ptr = std::unique_ptr<sqlite3, closer>;

/* handy maker function */
sqlite3_ptr make_sqlite()
{
    sqlite3* buffer = nullptr;
    int err = sqlite3_open(&buffer);
    if (err) {
        throw std::runtime_error("failed to open sqlite");
    }
    return sqlite3_ptr(buffer);
}

int main()
{
    auto mysqlite = make_sqlite();
}
1
votes

Solution with shared_ptr

I'm learning C++ and SQLite, so I had this question too. After reading this post, I tried some answers from it. The result is a working example and a small analysis.

  • First create a custom deleter for the smart pointer.
  • Then, create an empty share_ptr including the custom deleter
  • Then, create an empty pointer for the DB handler (sqlite3 * DB;)
  • Afterwards, open/create the DB.
  • Link the raw pointer to the shared one.
  • After the shared_ptr goes out of scope, it will delete the raw pointer too.

This is rather inefficient (see conclusion), but is the only way I manged to use smart pointers with sqlite3, so I decided to post this as an answer.

#include <iostream>
#include<sqlite3.h>
#include<memory>

//Custom deleter
auto del_sqlite3 = [](sqlite3* pSqlite)
{
    std::cout << "Calling custom deleter." << std::endl;
    sqlite3_close_v2(pSqlite);
};

int main()
{
//Uncomment to run
//const char* dir = "C:\\test\\db_dir\\test.db"
openOrCreateDB(dir);
return 0;
}


int openOrCreateDB(const char* dirName)
{
    std::shared_ptr<sqlite3> DB(nullptr, del_sqlite3);//custom deleter
    auto pDB = DB.get();
    {
        int exit = sqlite3_open(dirName, &pDB);
        DB.reset(pDB);// Replace nullptr with pDB and link
     }
    return 0;
}

Why smart pointers with sqlite3?

The main reason to use a smart pointer is to automate memory management and avoid memory leaks. So, this happens if we are thinking in allocating memory on the free store, using new and delete.

But I failed with all my attempts to allocate a database handler in the free store.

Fail 1: using sqlite3* DB = new sqlite3;

int openOrCreateDB(const char* dirName)
{
    sqlite3* DB = new sqlite3;//E0070: Incomplete type not allowed
    int exit = sqlite3_open(dirName, &DB);
    sqlite3_close(DB);
    return 0;
}

Fail 2: using share_ptr

static int openOrCreateDB(const char* dirName)
{
   
    std::shared_ptr<sqlite3> DB(new sqlite3, del_sqlite3);// Incomplete type not allowed
    auto pDB = DB.get();
    {
        int exit = sqlite3_open(dirName, &pDB);
        DB.reset(pDB);
     }
    
    return 0;
}

Fail 3: using make_shared

I didn't even try. In Meyers' Effective Modern C++, Item 21 it is clear that you can't use make_shared to construct a smart pointer on the heap with the custom deleter.

Conclusion

Maybe I'm doing something wrong, but it seems that SQLite does not like to allocate database handlers (sqlite3 objects) on the heap. So why use a smart pointer anyway? Even if you allocate the db handler on the stack, smart pointers uses more memory and more lines of code.

The other reason to use smart pointers is to manage ownership. But, in sqlite3, the workflow is quite repetitive: In a routine:

  • Create a DB handler.
  • Open DB, execute SQL statements, etc.
  • Finalize statement
  • Finalize DB connection.

So I can't see why should we pass arround a DB handler outside this workflow.

My recommendation is to keep using raw pointers and destroying them with sqlite3_close(sqlite3 * ptr).