1

I have a ODBC wrapper interface that enables me to execute SQL queries in C++. In particular, I use the named parameter idiom for the select statements, for example:

Table.Select("foo").GroupBy("bar").OrderBy("baz");

To achieve this effect, the class Table_t returns a proxy object Select_t:

class Table_t
{
// ...

public:
  Select_t Select(std::string const &Stmt)
    { return {*this, Stmt}; }

  void Execute(std::string const &Stmt);
};

Select_t combines the basic statement with the additional clauses and executes the actual statement in the destructor:

class Select_t
{
private:
  Table_t &Table;
  std::string  SelectStmt,
               OrderStmt,
               GroupStmt;

public:
  Select_t(Table_t &Table_, std::string const &SelectStmt) :
    Table(Table_), SelectStmt(SelectStmt_) {}

  ~Select_t()
    { /* Combine the statements */ Table.Execute(/* Combined statement */); }

  Select_t &OrderBy(std::string const &OrderStmt_)
    { OrderStmt = OrderStmt_; return *this; }

  Select_t &GroupBy(std::string const &GroupStmt_)
    { GroupStmt = GroupStmt_; return *this; }
};

The problem is that Table.Execute(Stmt) may throw and I must not throw in a destructor. Is there a way I can work around that while retaining the named parameter idiom?

So far the only idea I came up with is to add an Execute function to Select_t, but I would prefer not to:

Table.Select("foo").GroupBy("bar").OrderBy("baz").Execute();
Pilar Latiesa
  • 675
  • 4
  • 10
  • Inserting `try {` and `} catch(...) { }` is not an option? – Scheff's Cat Dec 12 '19 at 10:03
  • Use a `try`/`catch` in the destructor, and ensure all types of exceptions are caught. If an exception is caught, don't rethrow it (and also, obviously, don't throw another exception). One option is the function-try-block. – Peter Dec 12 '19 at 10:05
  • You could have an extra member: `Table.Select("foo").GroupBy("bar").OrderBy("baz").Go()` and execute the statement in `Go()`. Oh You already thought of that! – Richard Hodges Dec 12 '19 at 10:06
  • @MaxLanghof yes, just realised my error. – Richard Hodges Dec 12 '19 at 10:07

2 Answers2

3

Throwing "inside" a destructor is not a problem; the problem is exceptions escaping from a destructor. You need to catch the ODBC exception, and decide how to communicate the error by another interface.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • I would like the exception escape the destructor and be properly handled elsewhere. – Pilar Latiesa Dec 12 '19 at 10:08
  • @PilarLatiesa If the destruction is caused by another exception, that would terminate your application. – molbdnilo Dec 12 '19 at 10:45
  • @molbdnilo Sorry. I wan't clear enough. I want the expression `Table.Select("foo").GroupBy("bar").OrderBy("baz")` to be able to throw exceptions. My problem is that with my current design it is not. – Pilar Latiesa Dec 12 '19 at 10:56
  • 1
    @PilarLatiesa: What molbdnilo is saying is that if `GroupBy` throws a `std::bad_alloc` somehow, the C++ runtime destructs all temporaries including the half-done `Select_t`. Being half-done, it's reasonable to assume `Execute` fails, which gets you the second exception and a termination. – MSalters Dec 12 '19 at 11:06
  • Thanks for the clarification @MSalters. Yes, my current design is a disaster. I assume it is impossible to rework it so that it doesn't rely on the destructor. – Pilar Latiesa Dec 12 '19 at 11:27
  • 1
    @Pilar I wouldn't call it a disaster. That is just a problem that can happen. A normal way to deal with this is to just catch and ignore the exception in the destructor. If a user wants to deal specially with this, they have to call `Execute` directly. That is exactly as `std::fstream` does it with `close`, see [here](https://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor#). I think a good idea would be to set a failbit or something like that in `Table` then the user can also check in the aftermath if everything worked. – n314159 Dec 12 '19 at 11:42
1

Actually, separating the concerns of the query object and its execution might be a good idea.

Lazy invocation can be very useful.

The Execute function could reasonably be a free function.

For example:

auto myquery = Table.Select("foo").GroupBy("bar").OrderBy("baz");

auto future_result = marshal_to_background_thread(myquery);
//or
auto result = Execute(myquery);

This would lend itself to re-use with respect to prepared statements.

e.g.

auto myquery = Prepare(Table.Select("foo").Where(Equals("name", Param(1))).OrderBy("baz"));


auto result = Execute(myquery, {1, "bob"});
result = Execute(myquery, {1, "alice"});

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 2
    `Execute` can even be made implicit in many cases, by implementing it as an implicit conversion from `PreparedStatement` to `Result`. (i.e. `PreparedStatement::operator Result() { return Execute(); }`. – MSalters Dec 12 '19 at 10:18
  • @MSalters That's actually beautiful. – Richard Hodges Dec 12 '19 at 10:19
  • Even though I like and appreciate the idea, that would cause my code to be plagued with `Execute` calls. Part of the motivation of the wrapper was to reduce the verbosity of `SQLBindParameter`, `SQLPrepare`, `SQLExecute`, etc. – Pilar Latiesa Dec 12 '19 at 10:26
  • @PilarLatiesa Have a think about MSalter's idea. The execution could happen in `operator=(Result&, Query&)`. Throwing away the result of a query is always a bad idea so the assignment could be made mandatory with `[[nodiscard]]` – Richard Hodges Dec 12 '19 at 11:04
  • I undestand but it doesn't fit very well in my current design. Now my variables are mapped to a `Table_t`, and I only have to call `Select` to "read" them, or `Insert` to insert them, or `Update`, etc. I find it very convenient, yet I have this huge issue of a potentialy throwing destructor. That's why it would be weird having to write `Execute(Table.Select("foo"))` but not `Execute(Table.Insert("foo"))` (`"foo"` in both cases is a "WHERE" clause). – Pilar Latiesa Dec 12 '19 at 11:34
  • What about `ResultSet result = Table.Select("foo");` - the execution can happen in the constructor of ResultSet, which means that if there's an exception, the ResultSet does not exist (by language definition) – Richard Hodges Dec 12 '19 at 11:37
  • That's similar to the Visual Basic ODBC wrapper. So maybe I must rethink my design altogether, though I liked the simmetry of `Table.Select` and `Table.Insert`. Probably, `Insert` should be `Table.Insert(ResultSet, "WHERE")`, but I would need to think how to map my C++ variables to SQL columns. – Pilar Latiesa Dec 12 '19 at 11:46
  • @PilarLatiesa Even an insert returns number of rows affected. It has a result (if not a result set). – Richard Hodges Dec 12 '19 at 11:47
  • I ended up using a variant of your idea. I removed the potentialy throwing instruction from the destructor. The statement is stored in the `Table_t` and when it is queried for results the select statement is executed lazily. I'm quite satisfied for this compromise solution. Thanks so much. – Pilar Latiesa Dec 13 '19 at 07:23
  • @PilarLatiesa great. Glad to help. – Richard Hodges Dec 13 '19 at 07:36