0

I'm working with MFC's CRecordset class. I have overridden the virtual method DoBulkFieldExchange.

In general, my overridden methods are called and working just fine. However, I have a situation where the base class DoBulkFieldExchange method is called.

In an unrelated method of my derived CRecordset class, I call AfxThrowDBException. This calls the CRecordset destructor. And, during cleanup, DoBulkFieldExchange is called in the base class and not in my derived class. In this case, it causes an assert as the base class is not expecting the default version to be called with this configuration.

I know my derived class is set up correctly because it gets called. So what are the circumstances where the base class' method is called instead?

Here's my custom CRecordset class:

class CRS : public CRecordset
{
public:
    int m_nId;
    TCHAR m_szName[CUSTOMER_NAME_MAXLENGTH + 1];

    int* m_pnIds;
    long* m_pnIdLengths;
    LPTSTR m_pszNames;
    long* m_pnNameLengths;

public:
    CRS(CDatabase* pDatabase = NULL)
        : CRecordset(pDatabase)
    {
        m_nFields = 2;

        m_nId = 0;
        m_szName[0] = '\0';

        m_pnIds = NULL;
        m_pnIdLengths = NULL;
        m_pszNames = NULL;
        m_pnNameLengths = NULL;
    }

    CString GetDefaultSQL()
    {
        return CCustomerData::m_szTableName;
    }

    void DoFieldExchange(CFieldExchange* pFX)
    {
        pFX->SetFieldType(CFieldExchange::outputColumn);
        RFX_Int(pFX, _T("Id"), m_nId);
        RFX_Text(pFX, _T("Name"), m_szName, CUSTOMER_NAME_MAXLENGTH);
    }

    void DoBulkFieldExchange(CFieldExchange* pFX)
    {
        pFX->SetFieldType(CFieldExchange::outputColumn);
        RFX_Int_Bulk(pFX, _T("Id"), &m_pnIds, &m_pnIdLengths);
        RFX_Text_Bulk(pFX, _T("Name"), &m_pszNames, &m_pnNameLengths, (CUSTOMER_NAME_MAXLENGTH + 1) * 2);
    }
};

And here's the code that uses it. ExecuteSqlQuery just calls CRS::Open() with the given database.

CRemoteDatabase db;
db.Open();
auto prs = db.ExecuteSqlQuery<CRS>(NULL, CRecordset::forwardOnly, CRecordset::useMultiRowFetch);

while (!prs->IsEOF())
{
    // The call to GetFieldValue is producing an 'Invalid cursor position'
    // error, which causes AfxThrowDBException to be called. This
    // indirectly calls the destructor, which then calls the base-class
    // DoBulkFieldExchange method, which in turn ASSERTs. Why doesn't
    // it call my derived method?
    CString sValue;
    prs->GetFieldValue((short)CUSTOMER_ID, sValue);
}
Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466

1 Answers1

1

A base class method might be called instead of a derived class method when the base class destructor calls a virtual function. In this case, the derived class is already destroyed and no virtual method can be called. (More info on this question).

Back to your question:

From MFC code, dbcore.cpp using VS 2019 (14.22.27905):

CRecordset::FreeRowset() calling DoBulkFieldExchange, and looks like that is some cases FreeRowset() is called by the destructor of CRecordset.

This is the comment from CRecordset::FreeRowset code.

Calling virtual function, DoBulkFieldExchange, here is bad because Close then FreeRowset may get called from destructor. There is no simple choice however if RFX_Bulk functions do a memory allocation. The net result is that users MUST call Close explicitly (rather than relying on destructor) if using multi row fetches, otherwise they will get a memory leak. If rowset already allocated, delete old rowset buffers

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
EylM
  • 5,967
  • 2
  • 16
  • 28
  • So, it's happening because it's called from the destructor and the derived class has already been destructed? If I call `Close()` before throwing the exception, then I don't get the ASSERT. But then I lose the error information as well. Kind of a mess, but I think this is the right answer. – Jonathan Wood Aug 28 '19 at 15:56
  • BTW, that's a pretty good answer. There's a 250 point bounty on [this question](https://stackoverflow.com/questions/57649696/custom-crecordset-class-does-not-call-dofieldexchange-when-usemultirowfetch-is), if you're interested. It's related to the same code. – Jonathan Wood Aug 28 '19 at 16:00
  • Yes, the destructors of base classes are called in the reverse order of the completion of their constructor. So if your base class is calling a virtual function, the derived object destructor is already been called, so no functions of the derived class could be called. – EylM Aug 28 '19 at 16:01