usage differences between _variant_t, COleVariant,

2019-07-15 02:23发布

问题:

I am investigating several Visual Studio 2015 C++ project types which use ADO to access an SQL Server database. The simple example performs a select against a table, reads in the rows, updates each row, and updates the table.

The MFC version works fine. The Windows console version is where I am having a problem updating the rows in the recordset. The update() method of the recordset is throwing a COM exception with the error text of:

L"Item cannot be found in the collection corresponding to the requested name or ordinal."

with an HRESULT of 0x800a0cc1.

In both cases I am using a standard ADO recordset object defined as;

_RecordsetPtr       m_pRecordSet;   // recordset object

In the MFC version, the function to update the current row in the recordset is:

HRESULT CDBrecordset::UpdateRow(const COleVariant vPutFields, COleVariant vValues)
{
    m_hr = 0;
    if (IsOpened()) {
        try {
            m_hr = m_pRecordSet->Update(vPutFields, vValues);
        }
        catch (_com_error &e) {
            _bstr_t bstrSource(e.Description());
            TCHAR *description;
            description = bstrSource;
            TRACE2("  _com_error CDBrecordset::UpdateRow %s  %s\n", e.ErrorMessage(), description);
            m_hr = e.Error();
        }
    }

    if (FAILED(m_hr))
        TRACE3(" %S(%d): CDBrecordset::UpdateRow()  m_hr = 0x%x\n", __FILE__, __LINE__, m_hr);
    return  m_hr;
}

This function is called by using two COleSafeArray objects composed into a helper class to make it easier to specify the column names and values to be updated.

// Create my connection string and specify the target database in it.
// Then connect to SQL Server and get access to the database for the following actions.
CString  ConnectionString;
ConnectionString.Format(pConnectionStringTemp, csDatabaseName);

CDBconnector x;
x.Open(_bstr_t(ConnectionString));

// Create a recordset object so that we can pull the table data that we want.
CDBrecordset y(x);

//  ....... open and reading of record set deleted.

MyPluOleVariant thing(2);

thing.PushNameValue (SQLVAR_TOTAL, prRec.lTotal);
thing.PushNameValue (SQLVAR_COUNTER, prRec.lCounter);

hr = y.UpdateRow(thing.saFields, thing.saValues);

Because the Windows Console version is not using MFC, I am running into some definition issues which appear to be due to ATL COM class CComSafeArray being a template.

In the MFC source, COleSafeArray is a class derived from tagVARIANT which is a union that is the data structure for a VARIANT. However in ATL COM, CComSafeArray is a template that I am using as CComSafeArray<VARIANT> which seems reasonable.

However when I try to use a variable defined with this template, a class CDBsafeArray derived from CComSafeArray<VARIANT>, I get the following compilation error at the point where I call m_pRecordSet->Update():

no suitable user-defined conversion from "const CDBsafeArray" to "const _variant_t" exists

_variant_t seems to be a wrapper class for VARIANT and there does not seem to be a conversion path between CComSafeArray<VARIANT> and _variant_t however there is a conversion path between COleSafeArray and _variant_t.

What I have tried is to specify the m_psa member of the class which is a SAFEARRAY of type VARIANT and this compiles however I see the COM exception above when testing the application. Looking in the object with the debugger, the object specifying the fields to be updated appears to be correct.

So it appears I am mixing incompatible classes. What would be a SAFEARRAY wrapper class that will work with _variant_t?

回答1:

A Brief Overview of Microsoft VARIANT and SAFEARRAY

The VARIANT type is used to create a variable that may contain a value of many different types. Such a variable may be assigned an integer value at one point and a string value at another. ADO uses VARIANT with a number of different methods so that the values read from a database or written to a database can be provided to the caller through a standard interface rather than trying to have lots of different, data type specific interfaces.

Microsoft specifies the VARIANT type which is represented as a C/C++ struct which contains a number of fields. The two primary parts of this struct are a field that contains a value representing the type of the current value stored in the VARIANT and a union of the various value types supported by a VARIANT.

In addition to VARIANT another useful type is SAFEARRAY. A SAFEARRAY is an array which contains array management data, data about the array such as how many elements it contains, its dimensions, and the upper and lower bounds (the bounds data allows you to have arbitrary index ranges).

The C/C++ source code for a VARIANT looks something like the following (all of the component struct and union members seem to be anonymous, e.g. __VARIANT_NAME_2 is #defined to be empty):

typedef struct tagVARIANT VARIANT;

struct tagVARIANT
    {
    union 
        {
        struct __tagVARIANT
            {
            VARTYPE vt;
            WORD wReserved1;
            WORD wReserved2;
            WORD wReserved3;
            union 
                {
                LONGLONG llVal;
                LONG lVal;
                BYTE bVal;
                SHORT iVal;
//  ... lots of other fields in the union
            }   __VARIANT_NAME_2;
        DECIMAL decVal;
        }   __VARIANT_NAME_1;
    } ;

COM uses the VARIANT type in COM object interfaces to provide the ability to pass data through the interface back and forth and doing any kind of data transformation needed (marshaling).

The VARIANT type supports a large variety of data types one of which is SAFEARAY. So you can use a VARIANT to pass a SAFEARRAY over an interface. Rather than having an explicit SAFEARRAY interface you can instead specify a VARIANT interface that will recognize and process a VARIANT that contains a SAFEARRAY.

There are several functions provided to manage the VARIANT type some of which are:

VariantInit()
VariantClear()
VariantCopy()

And there are several functions provided to manage the SAFEARRAY type some of which are:

SafeArrayCreate()
SafeArrayCreateEx()
SafeArrayCopyData();

Three different Microsoft VARIANT classes: MFC, ATL, Native C++

Microsoft has provided several different frameworks over the years and one of the goals of these frameworks and libraries has been the ability to work easily with COM objects.

We will look at three different versions of VARIANT classes for C++ in the following: (1) MFC, (2) ATL, and (3) what Microsoft calls native C++.

MFC is a complex framework developed early in the C++ life to provide a very comprehensive library for Windows C++ programmers.

ATL is a simpler framework developed to assist people creating COM based software components.

The _variant_t seems to be a standard C++ class wrapper for VARIANT.

The ADO _RecordsetPtr class has the Update() method that accepts a _variant_t object which looks like:

inline HRESULT Recordset15::Update ( const _variant_t & Fields, const _variant_t & Values ) {
    HRESULT _hr = raw_Update(Fields, Values);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _hr;
}

MFC provides a set of classes for working with COM objects with the classes for the VARIANT type being COleVariant and COleSafeArray. If we look at the declaration for these two classes we see the following:

class COleVariant : public tagVARIANT
{
// Constructors
public:
    COleVariant();

    COleVariant(const VARIANT& varSrc);
//   .. the rest of the class declaration
};

class COleSafeArray : public tagVARIANT
{
//Constructors
public:
    COleSafeArray();
    COleSafeArray(const SAFEARRAY& saSrc, VARTYPE vtSrc);
//  .. the rest of the class declaration
};

If we look at the ATL versions of these classes what we find is CComVariant and CComSafeArray however CComSafeArray is a C++ template. When you declare a variable with CComSafeArray you specify the type of the values to be contained in the underlying SAFEARRAY structure. The declarations look like:

class CComVariant : public tagVARIANT
{
// Constructors
public:
    CComVariant() throw()
    {
        // Make sure that variant data are initialized to 0
        memset(this, 0, sizeof(tagVARIANT));
        ::VariantInit(this);
    }
//  .. other CComVariant class stuff
};

// wrapper for SAFEARRAY.  T is type stored (e.g. BSTR, VARIANT, etc.)
template <typename T, VARTYPE _vartype = _ATL_AutomationType<T>::type>
class CComSafeArray
{
public:
// Constructors
    CComSafeArray() throw() : m_psa(NULL)
    {
    }
    // create SAFEARRAY where number of elements = ulCount
    explicit CComSafeArray(
        _In_ ULONG ulCount,
        _In_ LONG lLBound = 0) : m_psa(NULL)
    {
// .... other CComSafeArray class declaration/definition
};

The _variant_t class is declared as follows:

class _variant_t : public ::tagVARIANT {
public:
    // Constructors
    //
    _variant_t() throw();

    _variant_t(const VARIANT& varSrc) ;
    _variant_t(const VARIANT* pSrc) ;
//  .. other _variant_t class declarations/definition
};

So what we see is a small difference between how the three different frameworks (MFC, ATL, and native C++) do VARIANT and SAFEARRAY.

Using the Three VARIANT Classes Together

All three have a class to represent a VARIANT which is derived from the struct tagVARIANT which allows all three to be used interchangeable across interfaces. The difference is how each handles a SAFEARRAY. The MFC framework provides COleSafeArray which derives from struct tagVARIANT and wraps the SAFEARRAY library. The ATL framework provides CComSafeArray which does not derive from struct tagVARIANT but instead uses composition rather than inheritance.

The _variant_t class has a set of constructors which will accept a VARIANT or a pointer to a VARIANT as well as operator methods for assignment and conversion that will accept a VARIANT or pointer to a VARIANT.

These _variant_t methods for VARIANT work with the ATL CComVariant class and with the MFC COleVariant and COleSafeArray classes because these are all derived from struct tagVARIANT which is VARIANT. However the ATL CComSafeArray template class does not work well with _variant_t because it does not inherit from struct tagVARIANT.

For C++ this means that a function that takes an argument of _variant_t can be used with the ATL CComVariant or with the MFC COleVariant and COleSafeArray but can not be used with an ATL CComSafeArray. Doing so will generate a compiler error such as:

no suitable user-defined conversion from "const ATL::CComSafeArray<VARIANT, (VARTYPE)12U>" to "const _variant_t" exists

See User-Defined Type Conversions (C++) in the Microsoft Developer Network documentation for an explanation.

The simplest work around for a CComSafeArray seems to be to define a class that derives from CComSafeArray and to then provide a method that will provide a VARIANT object that wraps the SAFEARRAY object of the CComSafeArray inside of a VARIANT.

struct CDBsafeArray: public CComSafeArray<VARIANT>
{
    int                     m_size;
    HRESULT                 m_hr;

    CDBsafeArray(int nSize = 0) : m_size(nSize), m_hr(0)
    {
        // if a size of number of elements greater than zero specified then
        // create the SafeArray which will start out empty.
        if (nSize > 0) m_hr = this->Create(nSize);
    }

    HRESULT CreateOneDim(int nSize)
    {
        // remember the size specified and create the SAFEARRAY
        m_size = nSize;
        m_hr = this->Create(nSize);
        return m_hr;
    }

    // create a VARIANT representation of the SAFEARRAY for those
    // functions which require a VARIANT rather than a CComSafeArray<VARIANT>.
    // this is to provide a copy in a different format and is not a transfer
    // of ownership.
    VARIANT CreateVariant() const {
        VARIANT  m_variant = { 0 };            // the function VariantInit() zeros out so just do it.
        m_variant.vt = VT_ARRAY | VT_VARIANT;  // indicate we are a SAFEARRAY containing VARIANTs
        m_variant.parray = this->m_psa;        // provide the address of the SAFEARRAY data structure.
        return m_variant;                      // return the created VARIANT containing a SAFEARRAY.
    }
};

This class would then be used to contain the field names and the values for those fields and the ADO _RecordsetPtr method of Update() would be called like:

m_hr = m_pRecordSet->Update(saFields.CreateVariant(), saValues.CreateVariant());