std::string assignment issue in VS2019, where it is working fine in VS2015

507 views Asked by At

I am having an issue std::string assignment with the below code in VS2019, where it is working fine in VS2015. I am using Visual Studio 2019 (v142) as Platform Toolset in VS2019 and Visual Studio 2015 (v140) as Platform Toolset in VS2015.

class Test
{
public:
    Test()
    {
        m_DisplayName = "";
        m_Type = "";
    }
    std::string m_DisplayName;
    std::string m_Type;
};

void CMFCApplication2Dlg::TestFunction()
{
    // Sample Data setting in vector
    std::vector<Test> vTest;
    Test obj;
    obj.m_DisplayName = "Nam1gd";
    obj.m_Type = "t1";
    vTest.emplace_back(obj);

    Test obj2;
    obj2.m_DisplayName = "Nam2";
    obj2.m_Type = "t2";
    vTest.emplace_back(obj2);

    VARIANT vtResult_o;
    VariantInit(&vtResult_o);
    vtResult_o.vt = VT_ARRAY | VT_UI1;
    int usrCount = vTest.size();
    ULONG nBufferSize = usrCount * sizeof(Test);

    // Define a safe array of usrCount Item and Starting index as 0 
    SAFEARRAYBOUND safeBounds = { nBufferSize, 0 };

    //Create the Safe Array passing it the bounds 
    SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, &safeBounds);
    Test* vTestArray = nullptr;
    SafeArrayAccessData(pSafeArray, (void**)&vTestArray);

    // Data setting to Safe array
    for (int nIdx = 0; nIdx < usrCount; nIdx++)
    {
        // ******VALUES ASSIGNING CORRECTLY in VS 2015 ******/
        // ******JUNK VALUE COMES IN THIS ASSIGNMENT in VS 2019 ******/
        vTestArray[nIdx].m_DisplayName = vTest[nIdx].m_DisplayName;
        vTestArray[nIdx].m_Type = vTest[nIdx].m_Type;
    }

    /* decrement the lock count*/
    SafeArrayUnaccessData(pSafeArray);
    vtResult_o.parray = pSafeArray;
}

Since the string value in the vTest[nIdx].m_DisplayName is small it is stored in the small buffer in s._Bx._Buf. In VS2015 while assigning, the small buffer in the source string is copied to the destination string's small buffer.

 for (int nIdx = 0; nIdx < usrCount; nIdx++)
 {
     // ******VALUES ASSIGNING CORRECTLY in VS 2015 ******/
     // ******JUNK VALUE COMES IN THIS ASSIGNMENT in VS 2019 ******/
     vTestArray[nIdx].m_DisplayName = vTest[nIdx].m_DisplayName;
     vTestArray[nIdx].m_Type = vTest[nIdx].m_Type;
 }

Source string However, in VS2019, it is observed that the during assignment the destination's buffer ( s._Bx._Buf) is updated with junk value and the actual value is updated to the heap s._Bx._ptr.

Ideally since s._Bx is a union either the s._Bx._Buf or s._Bx._ptr should be present. Please find the image below. Destination

[Workaround]

However if I do a std::string casting in VS2019 it is assigning the std::string's small buffer in the source to the small buffer in the destination.

for (int nIdx = 0; nIdx < usrCount; nIdx++)
{
    vTestArray[nIdx].m_DisplayName =(std::string)(vTest[nIdx].m_DisplayName);
    vTestArray[nIdx].m_Type = (std::string)(vTest[nIdx].m_Type);
}

I will appreciate if some one can help me to understand why this difference in VS2015 and VS2019!

1

There are 1 answers

6
user17732522 On

You are treating the array you obtained as if it contained alive Test objects, which it doesn't. That causes undefined behavior.

Instead of assigning directly, which pretends that there is already an alive object, you should use placement-new to create and start the lifetime of the Test object explicitly first:

auto obj = new(vTestArray + nIdx) Test;
obj->m_DisplayName = vTest[nIdx].m_DisplayName;
obj->m_Type = vTest[nIdx].m_Type;

and don't forget to destroy the objects when you are done with them with explicit destructor calls:

for (int nIdx = 0; nIdx < usrCount; nIdx++) {
    vTestArray[nIdx].~Test();
}

(Technically there might be some minor issue here requiring some additional std::launder call on vTestArray if we read the standard strictly and SafeArrayAccessData would not be considered to produce a "pointer to a suitable created object" under the C++20 implicit object creation rules.)


There is also a more minor issue in that the cast in SafeArrayAccessData(pSafeArray, (void**)&vTestArray); is wrong. You cannot reinterpret a pointer to Test* as a pointer to void*. That is an aliasing violation. You should use an intermediate pointer of the correct type:

void* vTestArrayVoid = nullptr;
SafeArrayAccessData(pSafeArray, &vTestArray);
auto vTestArray = reinterpret_cast<Test*>(vTestArrayVoid);

(Please note that there should not be a std::launder call here against my advice in a previous edit, since there is no Test object in its lifetime yet.)


I am also not sure what the intended use of the array is, but you should be aware that the strings will generally allocate additional memory that is not part of the array. So this is not a reliable way to e.g. share memory between processes.

But if you don't have some specific use case in mind (and I might just not be aware here of the obvious if it is Microsoft-specific) then there would be no reason to make it so complicated when std::vector, potentially reference-counted via std::shared_ptr, already does all of this automatically.

Doing it manually in the way I suggested is also not exception-safe, which a std::vector would be. If an exception is thrown during construction of one of the Test objects, then you will forget the destruction of the already-constructed Test objects with this naive implementation.