Unfortunately, like all of the talks at Going Native 2013, it was constrained by a very tight time schedule. Fortunately for us, though, Sean Parent gave a much more thorough talk last year at C++Now called Value Semantics and Concepts-based Polymorphism. It covers the same material and will probably answer your questions. I'll take a shot at explaining anyway....
There are two types of semantics that a type can have:
It is possible to go on for many pages about how the two differ and when one is preferable to the other. Let's simply say that code using value types can be more easily reasoned about.
That is to say that nothing unpredictable happens at any point to an instance of a value type -- something that cannot be guaranteed with a reference type since the value that is referenced is shared between other parts of your code that hold a reference to it.
In other words: reference types are less predictable because they can be changed by a distant piece of code. For example, a function that you call could change the value referenced out from under you. Or, even worse, if threading is involved, the reference type could be changed at any time by another thread that happens to operate on the value referenced. For this reason, Sean Parent makes the statement that a shared_ptr
is as good as a global variable when it comes to being able to reason about code that uses one.
With all of that said, we should be prepared to answer the question at hand.
For a value type T
, why does a shared_ptr<const T>
act like a value type even though it is a pointer type?
Because we cannot make changes to the const T
that is pointed to, everything that was said about pointer/reference types being less predictable no longer applies. We no longer have to worry about the T
being changed unexpectedly because it is a const value type.
If we did want to make a change to the T
, we would have to make a copy of it, leaving others that hold a shared_ptr<const T>
unaffected by our actions. Moreover, the copy can even be hidden inside a value type using a mechanism called Copy-on-write, which seems to be what Sean Parent ultimately did.
I think I've answered the question as Sean Parent would (and did in the linked C++Now presentation), but lets go a bit further with an addendum.....
(Thanks to @BretKuhns for bringing this up and providing an example in the comments.)
There is one nagging thing wrong with this whole notion. Saying that shared_ptr<const T>
behaves like a value type is not necesarily correct unless we know that all living pointers/references to that instance of T
are const
. This is because the const
modifier is a one way street -- holding a shared_ptr<const T>
may prevent us from modifying the instance of T
, but does not prevent somebody else from modifying the T
through a pointer/reference to non-const
.
Knowing this, I would be weary of making the broad statement that shared_ptr<const T>
is as good as a value type unless I knew that all living pointers to it are const
. But, knowing such a thing requires global knowledge of the code around all usages of shared_ptr<const T>
-- something that would not be a problem for a value type. For that reason, it might make more sense to say something like: A shared_ptr<const T>
can be used to support value semantics.
On a side note, I was actually at Going Native 2013 -- maybe you can see the back of my head in the front left.
I am giving 3 examples. In all three cases I create a variable a
with the content "original value"
. Then I create another variable b
by saying auto b = a;
and after this statement I assign a
the content "new value"
.
If a
and b
have value semantics, I expect b
's content to be the "original content"
. And in fact exactly this happens with string
and shared_ptr<const string>
. The conceptual meaning of auto b = a;
is the same with these types. Not so much with shared_ptr<string>
, b
will have the content "new value"
.
Code (online demo):
#include <iostream>
#include <memory>
#include <string>
using namespace std;
void string_example() {
auto a = string("original value");
auto b = a; // true copy by copying the value
a = string("new value");
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
void shared_ptr_example() {
auto a = make_shared<string>("original value");
auto b = a; // not a copy, just and alias
*a = string("new value"); // and this gonna hurt b
cout << "a = " << *a << endl;
cout << "b = " << *b << endl;
cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
void shared_ptr_to_const_example() {
auto a = make_shared<const string>("original value");
auto b = a;
//*a = string("new value"); // <-- now won't compile
a = make_shared<const string>("new value");
cout << "a = " << *a << endl;
cout << "b = " << *b << endl;
cout << boolalpha << "&a == &b ? " << (&a==&b) << endl;
}
int main() {
cout << "--------------" << endl;
cout << "string example" << endl;
string_example();
cout << "------------------" << endl;
cout << "shared_ptr example" << endl;
shared_ptr_example();
cout << "---------------------------" << endl;
cout << "shared_ptr to const example" << endl;
shared_ptr_to_const_example();
}
Output:
--------------
string example
a = new value
b = original value
&a == &b ? false
------------------
shared_ptr example
a = new value
b = new value
&a == &b ? false
---------------------------
shared_ptr to const example
a = new value
b = original value
&a == &b ? false
Having said that, I wish he had had a little more time: There are a few things I am still wondering about after that presentation. I am pretty convinced it was only the lack of time, he seems like a excellent presenter.
What he means is that they can be used to emulate value semantics.
The main defining trait of value semantics is that two objects with the same content are the same. Integers are value types: a 5 is the same as any other 5. Compare that to reference mechanics, where objects have an identity. A list a
containing [1, 2] is not the same as a list b
containing [1, 2], because appending 3 to a
does not have the same effect as appending 3 to b
. The identity of a
is different than the identity of b
.
This tends to be intuitive... it just sounds strange when put in words. Nobody makes it 3 days in C++ without getting some intuitive sense of value types vs. reference types.
If you have a mutable value type and you want to copy it, you have to actually copy the contents of the object. This is expensive.
The trick Sean is referring to is that if an object is immutable, then you don't have to copy the whole object, you can just refer to the old one. This is MUCH faster.
He seems to be assuming that the existence of a shared_ptr<const T>
implies that all handles to the object are also shared_ptr<const T>
(which is to say, read-only).
This is, of course, no more true than the existence of a raw const T*
constitutes proof that the object is const
.
Demo: http://ideone.com/UuHsEj
Probably you're mistaking "immutability" for meaning const T
-- in the question you said they're the same, but they aren't.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With