Reputation: 10740
If I define a shared_ptr
and a const shared_ptr
of the same type, like this:
std::shared_ptr<int> first = std::shared_ptr<int>(new int);
const std::shared_ptr<int> second = std::shared_ptr<int>();
And later try to change the value of the const shared_ptr
like this:
second = first;
It cause a compile error (as it should). But even if I try to cast away the const part:
(std::shared_ptr<int>)second = first;
The result of the code above is that second
ends up being Empty, while first
is untouched (eg ref count is still 1).
How can I change the value of a const shared_ptr
after it was originally set? Is this even possible with std's pointer?
Thanks!
Upvotes: 0
Views: 1531
Reputation: 275490
It is undefined behavior to modify in any way a variable declared as const
outside of its construction or destruction.
const std::shared_ptr<int> second
this is a variable declared as const
.
There is no standard compliant way to change what it refers to after construction and before destruction.
That being said, manually calling the destructor and constructing a new shared_ptr
in the same spot might be legal, I am uncertain. You definitely cannot refer to said shared_ptr
by its original name, and possibly leaving the scope where the original shared_ptr
existed is illegal (as the destructor tries to destroy the original object, which the compiler can prove is an empty shared pointer (or a non-empty one) based on how the const
object was constructed).
This is a bad idea even if you could make an argument the standard permits it.
const
objects cannot be changed.
...
Your cast to a shared_ptr<int>
simply creates a temporary copy. It is then assigned to, and the temporary copy is changed. Then the temporary copy is discarded. The const shared_ptr<int>
not being modified is expected behavior. The legality of assigning to a temporary copy is because shared_ptr
and most of the std
library was designed before we had the ability to overload operator=
based on the r/lvalue-ness of the left hand side.
...
Now, why is this the case? Actual const
ness is used by the compiler as an optimization hint.
{
const std::shared_ptr<int> bob = std::make_shared<int>();
}
in the above case, the compiler can know for certain that bob
is non-empty at the end of the scope. Nothing can be done to bob
that could make it empty and still leave you with defined behavior.
So the compiler can eliminate the branch at the end of the scope when destroying bob
that checks if the pointer is null.
Similar optimizations could occur if you pass bob
to an inline
function that checks for bob
's null state; the compiler can omit the check.
Suppose you pass bob to
void secret_code( std::shared_ptr<int> const& );
where the compiler cannot see into the implementation of secret_code
. It can assume that secret code will not edit bob.
If it wasn't declared const
, secret_code
could legally do a const_cast<std::shared_ptr&>
on the parameter and set it to null; but if the argument to secret_code
is actually const
this is undefined behavior. (Any code casting away const is responsible for guaranteeing that no actual modification of an actual const
value occurs by doing so)
Without const
on bob
, the compiler could not guarantee:
{
const std::shared_ptr<int> bob = std::make_shared<int>();
secret_code(bob);
if (bob) {
std::cout << "guaranteed to run"
}
}
that the guaranteed to run
string would be printed.
With const
on bob
, the compiler is free to elimiate the if
check above.
...
Now, do not confuse my explanation asto why the standard states you cannot edit const
stack variables with "if this doesn't happen there is no problem". The standard states you shall not do it; the consequences if you do it are unbounded and can grow with new versions of your compiler.
...
From comments:
For deserialize process, which is actually a type of constructor that deserialize object from file. C++ is nice, but it got its imperfections and sometimes its OK to search for less orthodox methods.
If it is a constructor, make it a constructor.
In C++17 a function returning a T
has basically equal standing to a real constructor in many ways (due to guaranteed elision). In C++14, this isn't quite true (you also need a move constructor, and the compiler needs to elide it).
So a deserialization constructor for a type T
in C++ needs to return a T
, it cannot take a T
by-reference and be a real constructor.
Composing this is a bit of a pain, but it can be done. Using the same code for serialization and deserialization is even more of a pain (I cannot off hand figure out how).
Upvotes: 3