I'm writing a class CValue
in C++, which is supposed to be an analog of values in JavaScript. CValue
object can be almost every type, depending on initialization/assignment: primitive value (bool
, int
, double
, string objects, etc.), array, or even an object (a hash map with property names as keys and CValue
as values). All work just fine, but I have some troubles with arrays and object element access. I will explain the problem for arrays.
To access elements of an array, CValue
has the following method:
CValue CValue::operator[](int index)
Notice, that I return CValue
, not CValue&
- that is because the object on which this operator was called may not be an array. In that case I just return empty CValue
, which is like undefined
from JS. When I call operator[]
on a valid array value and given index is correct, then I return a reference-like CValue
object, which contains a pointer to actually stored element in the array under the hood. This value is not a copy of original array element, because I want to be able to change it like so:
arr[0] = 42;
If I were returning a copy of array element, then, obviously, I would change its copy, not the original element.
So I only need to return reference-like CValue
objects when I access elements of arrays (or objects). In other scenarios I just make a copy of another value. For example:
CValue var1 = 42;
CValue var2 = var1; // var1 = 42, var2 = 42
var2 = 100; // var1 = 42, var2 = 100
And now, finally, I can describe the problem to you. When I write
CValue var = arr[0];
compiler thinks, that it is a great opportunity for him to use copy elision, and instead of using copy constructor (which will make a copy of value) it just replaces newly created value with reference-like CValue
object, returned from operator[]
. This leads to following unwanted behaviour:
arr[0] = 42;
CValue var = arr[0]; // var = 42, arr[0] = 42
var = 100; // var = 100, arr[0] = 100
I can use variable var
to fully access first element of arr
. But I just wanted to make a copy of it. So my question is:
How can I prevent copy elision here to avoid this unwanted begaviour?
NOTE: I know I can disable it completely in compiler flags, but I don't want to do this, because copy elision is quite useful overall.
Minimal Reproducible Example:
I use MSVS 2019 compiler. The MRE is quite big, but I cut it down as much as I could.
// cvalue.h
#include <memory>
#include <vector>
class IBaseValue;
class CPrimitive;
class CArray;
// wrapper around IBaseValue (see below) with specific value type
class CTypedValue
{
public:
enum ValueType
{
vtUndefined,
vtInteger,
vtArray
};
public:
CTypedValue();
public:
std::shared_ptr<IBaseValue> m_value;
ValueType m_type;
};
// Container for CTypedValue which is also allows to treat typed value as reference
class CTypedValueContainer
{
public:
CTypedValueContainer();
CTypedValueContainer(const CTypedValueContainer& other);
CTypedValueContainer& operator=(const CTypedValueContainer& other);
public:
std::shared_ptr<CTypedValue> m_typedValue;
bool m_isReference;
};
// Main class
class CValue
{
public:
CValue();
CValue(int value);
CValue(const CValue& other);
~CValue();
CValue& operator=(const CValue& other);
static CValue createArray(int size);
bool IsUndefined();
operator int();
CValue operator[](int index);
private:
CTypedValueContainer* m_internal;
};
// Base class for value types
class IBaseValue
{
public:
IBaseValue() = default;
virtual ~IBaseValue() = default;
};
// Primitive value type (only with int type for MRE)
class CPrimitive : public IBaseValue
{
public:
CPrimitive(int value);
int toInt();
private:
int m_value;
};
// Array value type
class CArray : public IBaseValue
{
public:
CArray(int size);
CValue& get(int i);
private:
std::vector<CValue> m_values;
};
// cvalue.cpp
#include "cvalue.h"
CPrimitive::CPrimitive(int value) : m_value(value)
{
}
int CPrimitive::toInt()
{
return m_value;
}
CArray::CArray(int size) : m_values(size)
{
}
CValue& CArray::get(int i)
{
return m_values[i];
}
CTypedValue::CTypedValue() : m_type(vtUndefined)
{
}
CTypedValueContainer::CTypedValueContainer() : m_typedValue(new CTypedValue()), m_isReference(false)
{
}
CTypedValueContainer::CTypedValueContainer(const CTypedValueContainer& other) : CTypedValueContainer()
{
*this = other;
}
CTypedValueContainer& CTypedValueContainer::operator=(const CTypedValueContainer& other)
{
if (other.m_isReference)
m_typedValue = other.m_typedValue;
else
*m_typedValue = *other.m_typedValue;
return *this;
}
CValue::CValue() : m_internal(new CTypedValueContainer)
{
}
CValue::CValue(int value) : CValue()
{
m_internal->m_typedValue->m_value = std::make_shared<CPrimitive>(value);
m_internal->m_typedValue->m_type = CTypedValue::vtInteger;
}
CValue::CValue(const CValue& other) : m_internal(new CTypedValueContainer(*other.m_internal))
{
}
CValue::~CValue()
{
delete m_internal;
}
CValue CValue::createArray(int size)
{
CValue ret;
ret.m_internal->m_typedValue->m_value = std::make_shared<CArray>(size);
ret.m_internal->m_typedValue->m_type = CTypedValue::vtArray;
return ret;
}
CValue& CValue::operator=(const CValue& other)
{
*m_internal = *other.m_internal;
return *this;
}
bool CValue::IsUndefined()
{
return m_internal->m_typedValue->m_type == CTypedValue::vtUndefined;
}
CValue::operator int()
{
if (m_internal->m_typedValue->m_type != CTypedValue::vtInteger)
return 0;
return static_cast<CPrimitive*>(m_internal->m_typedValue->m_value.get())->toInt();
}
CValue CValue::operator[](int index)
{
if (m_internal->m_typedValue->m_type != CTypedValue::vtArray)
return CValue();
CValue& value = static_cast<CArray*>(m_internal->m_typedValue->m_value.get())->get(index);
value.m_internal->m_isReference = true;
return value;
}
// main.cpp
#include <iostream>
#include "cvalue.h"
int main()
{
// Normal copy
CValue var1 = 42;
CValue var2 = var1;
std::cout << "var1 = " << (int)var1 << ", var2 = " << (int)var2 << std::endl;
var2 = 100;
std::cout << "var1 = " << (int)var1 << ", var2 = " << (int)var2 << std::endl;
// Copy of array element
CValue arr = CValue::createArray(1);
arr[0] = 42;
CValue var = arr[0];
std::cout << "var = " << (int)var << ", arr[0] = " << (int)arr[0] << std::endl;
var = 100;
std::cout << "var = " << (int)var << ", arr[0] = " << (int)arr[0] << std::endl;
return 0;
}
OUTPUT:
Here some output that this program produce. I compiled it with MinGW GCC compiler, since it has opportunity to disable copy elisions.
g++ main.cpp cvalue.cpp
:
var1 = 42, var2 = 42
var1 = 42, var2 = 100
var = 42, arr[0] = 42
var = 100, arr[0] = 100
g++ main.cpp cvalue.cpp -fno-elide-constructors
:
var1 = 42, var2 = 42
var1 = 42, var2 = 100
var = 42, arr[0] = 42
var = 100, arr[0] = 42
Second output is exactly what I want to achieve.
Despite the fact that many in the comments denied that incorrect behavior arises due to copy elision, it is so. You can check out OUTPUT section in my original question to see that.
To avoid that, I had to change program architecture, adding CValueRef
class, that implementing reference-like behaviour for CValue
, instead of mixing all this functionality in CValue
. Seems, that there is no other way, but returning an object of another class from operator[]
, because in line
CValue var = arr[0];
var
initializes as reference-like object, returned from that operator, instead of initializing as copy of it.
Thanks a lot to @Caleth and @Swift - Friday Pie.