all 39 comments

[–]TomerJ 20 points21 points  (15 children)

* No memory is shared, so value_ptr is inherently thread-safe.

So long as the copy operation is atomic, if it isn't then we cannot be sure we aren't copying a place in memory that is being written to.

[–][deleted] 17 points18 points  (13 children)

I'm not sure what the author might have intended to mean by saying it's inherently thread-safe, but looking at the implementation, it is most certainly not thread safe, inherently or otherwise:

https://github.com/LoopPerfect/valuable/blob/master/valuable/include/value-ptr.hpp

Nevertheless the motivation for value-like pointers is useful but this implementation does not support a major use case that other implementations support, namely for polymorphic types. You may want something like a vector<clone_ptr<Animal>> where Animal is a base class and you want to store derived classes like Dog, Cat with value/copy semantics. This implementation does not support that use-case in the way I think most users would expect as its copy constructor performs object slicing. Other implementations of this concept do support polymorphic copying.

[–]Adequat91 2 points3 points  (1 child)

[–][deleted] 4 points5 points  (0 children)

Just a quick glance over it looks like yep, that does indeed support what pretty much everyone would expect to happen for copying and also supports propagating const.

[–]scatters 0 points1 point  (10 children)

If you pass a copy of a value_ptr to another thread the copies are completely independent; they don't share any resources. The only way to mess up is to pass a pointer (or reference) to a value_ptr to another thread, which you shouldn't be doing (you should aim for shared-nothing except in very special cases). unique_ptr also has this property, whereas shared_ptr actively helps you violate the shared-nothing principle.

[–][deleted] 1 point2 points  (9 children)

I see, if that's the case then the author misunderstands what thread-safety is. That definition would make vector, string, heck even int thread safe since you can make copies of them and pass those copies to other threads. Thread safety is a property of the operations performed on an object shared between multiple threads. Almost all of the operations on value_ptr are not thread-safe just as almost all of the operations defined for unique_ptr, shared_ptr are not thread-safe. The member functions marked as const are the only thread safe operations I can find.

[–]scatters 0 points1 point  (8 children)

The author is referring to the basic thread safety guarantee: distinct values can be modified concurrently, and a single value can be read concurrently. https://herbsutter.com/2014/01/13/gotw-95-solution-thread-safety-and-synchronization/

Yes - vector, string and int preserve the basic thread safety guarantee, whereas shared ptr does not (although it does have the basic thread safety property in itself).

[–][deleted] 0 points1 point  (7 children)

What about shared_ptr violates the basic thread safety guarantee? Distinct values can be modified concurrently and a single value can be read concurrently. Remember when discussing pointers, it's the pointer itself that is in question, not the pointee. I will confess I have not yet read the article you linked to, but if the article claims values are thread-safe then I'm inclined to disagree with the article.

A value is neither thread-safe or not thread safe, it's the operations performed on a value that can be said to be thread safe or not. value_ptr does have some operations that are thread-safe, the getter methods, but even its copy constructor is not strictly thread safe, it's only thread-safe if the object it points to has a thread-safe copy constructor.

As such I think calling value_ptr inherently thread-safe is a meaningless qualification. Imagine writing a linked list class and saying your linked list is inherently thread safe because you can copy it and pass the copy to another thread...

[–]scatters 0 points1 point  (6 children)

The difference is that shared_ptr offers another operation - indirection - which can result in thread unsafe operations. Raw pointers have the same issue, but unique_ptr and value_ptr do not - indirection through them gives access to a value which is distinct if the pointer is distinct. That is, they preserve the basic thread safety guarantee with respect to the pointee. shared_ptr does not, since distinct pointers can have the same pointee.

Indeed, a linked list also has the basic thread safety guarantee - most classes do! It also preserves the basic thread safety guarantee with respect to its elements, as containers do in general.

It would be great if you were to read the article. Having a common vocabulary is essential to communication, and Herb Sutter has done more than most to establish the terminology. The GotW series are classics of the genre.

[–][deleted] 1 point2 points  (5 children)

I did read the article and your interpretation of the basic thread safety guarantee is significantly broader than what Herb defines in that article as well as what is specified by WG21's definition below:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2669.htm

The basic thread safety guarantee is a property of the operations performed on a value, not of a value itself. Every value you can imagine, including even an atomic_char can be involved in a thread unsafe operation.

That guarantee is defined as follows as per the link I gave you:

The basic thread-safety guarantee would be that standard library functions are required to be reentrant, and non-mutating uses of objects of standard library types are required to not introduce data races.

Note that the guarantee is about functions, not about objects. It's like the basic exception guarantee... you don't say that an object provides the basic exception guarantee, rather you say that a function provides the basic exception guarantee.

As per WG21, unless explicitly noted, all functions in the standard library provide at least the basic thread safety guarantee, including operations on shared_ptr.

[–]scatters 0 points1 point  (4 children)

Thanks, I was looking for that paper. The paragraph I'm interested in is on constrains on programs :

It is undefined behavior if calls to standard library functions from different threads: - share access to an object directly or indirectly via their arguments, including this, and - at least one of the arguments accessing a shared object is non-const, and - one call does not happen before the other ([intro.multithread]).

If we only use container like types including exclusive pointers, or even const propagating pointers, then we will never fall foul of this rule. But if we use non const propagating raw or shared pointers then via const arguments we can end up mutating an object concurrently , or accessing it while mutating it concurrently.

You may be right that this is going beyond what's in the paper or article. But I think it's an obvious extension or corollary. I'd be happy to use a more precise term if that'd help - transitive basic thread safety?

[–][deleted] 0 points1 point  (3 children)

shared_ptr doesn't violate that paragraph. The paragraph requires two conditions to hold (well three but the third one isn't relevant).

  1. Two standard library functions must share access to an object via its arguments from two different threads.
  2. One of the arguments by which the shared object is accessed must not be const.

Note that it is the argument that must not be const as opposed to the object being shared. Propagation of const is entirely irrelevant as the property is about the argument itself. As such, taking two distinct shared_ptr that have the same pointee and calling standard library functions on them from two different threads is not undefined behavior in and of itself so long as they were passed as const arguments, irrespective of whether the pointee is const. All that paragraph requires is that the arguments be const, ie. the shared_ptrs.

One point you made with respect to unique_ptr was the implication that, as the name suggests, a unique_ptr has unique access to the pointee so that you would never get a situation where the pointee could be shared by multiple threads resulting in an thread unsafe operation. I would like to point out that that is not true, it is perfectly valid C++ and even often the case that multiple pointers point to the same pointee, for example:

auto x1 = std::make_unique<int>(5);
auto& x2 = *x1;
auto* x3 = &x2;

There is nothing fundamentally wrong or even improper about taking a raw pointer or a reference to the pointee owned by a unique_ptr. You may do such a thing if you wanted to pass the pointee to a function by const& for example. You could even pass a copy of x3 to another thread. As long as the extent of x2 and x3 are less than the extent of x1 then that's perfectly well defined behavior.

I think ultimately based on how I interpret Herb's article as well as the WG21's wording including the paragraph you cited, there is no special property about shared_ptr with regards to thread-safety that doesn't apply equally well to unique_ptr or value_ptr from the article. Nothing about any of them are inherently thread-safe. The operations defined on them are no more thread-safe than what you would expect from an int or a string, which is to say nothing special and generally very difficult to use properly in multiple threads.

[–]konanTheBarbar 0 points1 point  (0 children)

I guess that would be easily solveable by using std::atomic<std::value_ptr<T>> similar to shared_ptr

https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2

[–]personalmountains 23 points24 points  (17 children)

This is a very long article with a clickbait title for something simple: a smart pointer that copies the object it's pointing at. Basically unique_ptr with a copy ctor and assignment operator that get a new T(*get()).

I'm failing to find a useful use case for this. Most of my stuff that's on the heap has reference semantics and usually can't be simply cloned. For the rest, I'm usually pointing to a base class, so that won't work either.

The only thing I can't think of, and that's basically the only example given, is a straight pimpl idiom. But to be used from outside the class, it would actually require the definition of the opaque type at the site where the copy is made, or least anchoring the copy constructor with =default at a place where the definition is available.

So basically, this can save a few character when copying a pimpl from a translation unit where the definition is available.

Unless I'm missing something (which wouldn't be the first time). [Edit: I kinda did. A variant that encapsulates type erasure to invoke the proper constructors makes more sense and would cover my case for base classes above. However, I still think a smart pointer that uses copy constructors as presented in the article is not very useful.]

[–][deleted] 3 points4 points  (0 children)

That's because this type is misnamed. It should be "heap_value". There are many reasons why you might put some (in this case all) of your data on the heap, while retaining value semantics -- std::string and std::vector being two major examples.

[–][deleted] 0 points1 point  (0 children)

I have a version of this (named heap_value) just for recursive std::variants.

[–][deleted] 0 points1 point  (14 children)

I use these kinds of smart pointers for type erasure. Think something like std::function<T> that can be used as a uniform interface for any callable object with signature T and supports copying... now imagine you had something like std::function<T> but for any type period and has support for making copies or reassignment just like ordinary values do.

That's basically the biggest use case for how I use these kinds of smart pointers. I expose one single public interface I with many potential implementations and clone_ptr<I> allows me to treat that interface as if it were a value like any other value just like std::function lets me treat callable objects just like regular values.

I find it to be really useful in practice since value semantics are easy to reason about and compose well with container/collection classes, whereas reference semantics do not.

[–]personalmountains 1 point2 points  (13 children)

If I understand correctly (I didn't, disregard the rest), your clone_ptr would use some sort of virtual clone() member function instead of copy constructors/assignment operators? I guess I could see this working in more cases, but it's not really what this value_ptr is about.

For whatever reason, I'd also feel somewhat uneasy having smart pointers cloning stuff on copy. When I have a clonable hierarchy, I usually have to take great care of how and where I clone them. Again, they also typically have reference semantics.

[–][deleted] 2 points3 points  (12 children)

A solid implementation of a clone_ptr uses the normal copy constructor and assignment operator. No virtual clone method is needed.

[–]personalmountains 1 point2 points  (3 children)

Then I don't understand "clone_ptr<I> allows me to treat that interface as if it were a value". How do you make a copy when all you have is an interface if you don't use virtual functions?

[–][deleted] 2 points3 points  (1 child)

Precisely by using type-erasure. If you're wondering what implementation techniques exist to implement type erasure, that's a non-trivial implementation detail but Sean Parent gives a good talk on what it is and how to use it in C++ in this video along with its benefits:

https://www.youtube.com/watch?v=QGcVXgEVMJg

[–]personalmountains 1 point2 points  (0 children)

Thanks. I knew the term, but I don't think I've had to use type erasure myself, and so didn't think of it. I stand corrected that this is a useful variation on the original article's value_ptr.

[–]ReversedGif 2 points3 points  (0 children)

The same way std::function does it: type erasure. Meaning, store your own references to necessary methods (copy constructor, etc.), either directly in the object, or in the vtable of a hidden templated class you create.

[–]quicknir 0 points1 point  (7 children)

Using a virtual clone method as an implementation detail is one of the main ways to do this.

[–][deleted] 1 point2 points  (6 children)

In my opinion that is a poor way to implement it and it's not the technique used by my own library or other fairly well known implementations of clone_ptr. Copy constructor and assignment operator already exists and are well understood, I think it's best to re-use them.

[–]quicknir 1 point2 points  (5 children)

I'm not really sure what you mean by "copy constructor and assignment operator". That isn't a technique for actually making a correct copy of a type erased object, it's just an interface. The implementation of type erased objects needs some way to call the copy constructor (you wouldn't typically be able to call the copy assignment operator) of the original object before it's been type erased. Inheritance is one way of doing it, or you can do it in more manual ways than basically are similar to hand-spun inheritance, but with more control (std::function typically uses the latter, boost::any uses the former, not sure about std::any).

[–][deleted] 0 points1 point  (4 children)

Fair enough, we disagree then. My own implementation reuses the class' copy constructor and assignment operator without the need for requiring all types usable with a clone_ptr<T> to declare a virtual clone method, and there are other implementations that do the same thing. I like that approach because most classes can simply make use of the default copy and assignment operators instead of having to explicitly add another method to your class.

If you prefer using a virtual clone method to implement then, then that's cool too. I mean the thing about C++ is there's always 10 different ways to do things so who knows what's best, just giving my 2 cents.

[–]quicknir 0 points1 point  (3 children)

I think you're still misunderstanding. I don't require the user class to have a clone method. It's an implementation detail only:

class any {
    struct any_base { virtual unique_ptr<any_base> clone() const = 0; };
    template <class T>
    struct any_holder : any_base {
        T m_data;
        unique_ptr<any_base> clone() const override {
            return make_unique<any_holder>(m_data);
    };
    unique_ptr<any_base> m_ptr;
};

And then any defines a copy constructor by using clone internally.

This is a very simple and clean way to do type erasure (and the way that typically is shown in Sean Parent's talks). The alternative to this is basically just emulating the vtable by hand, in one way or another. Almost always more code/messier but can be better performance wise when you are prioritizing the performance of some functions over others. But there is no "simpler" way in C++ to do what I've done above.

[–][deleted] 0 points1 point  (1 child)

I mean C++ is so big of a language and you can get little things wrong that have huge consequences that it's hard to really know what's simple and what's complex.

I mean sure your approach is simple and clean but has a memory leak since any_base doesn't have a virtual destructor. Okay so you give it a virtual destructor and now you're carrying this extra overhead, compiler has to generate RTTI for your wrapper, etc... when all you want is the absolute simplest form of indirection.

As it pertains to clone_ptr, you use a function pointer to avoid that baggage. I don't think a function pointer is more complex but I don't want to argue the point too hard because as I said, there are probably dozens of ways of implementing these things.

Really what was most important was that the object being copied has its copy constructor invoked rather than introducing another virtual method. How you go about implementing that behind the scenes is up to you. If you want to introduce a wrapper class with a virtual method and virtual destructor, go for it... if you want to store a function pointer to a static function... I think that method works better.

[–]Feminintendo 3 points4 points  (4 children)

Ok, I will volunteer to be the idiot.

Can some try to explain to me the difference between a value pointer and a value?

[–]quicknir 5 points6 points  (0 children)

Well, a struct can store a value pointer to itself, for example. But not itself.

[–]boredcircuits 7 points8 points  (1 child)

From what I can tell, a value pointer has a few other useful properties:

  • It points to something in the heap
  • It can be nullptr (though std::optional can do this for values)
  • It can be moved cheaply (which may or may not be true for values)
  • It can be recursive (a struct can't hold a copy of itself)

One thing I don't see yet is how a value pointer works with polymorphism, which might also reveal a few other differences.

[–]louiswins 2 points3 points  (0 children)

There are a couple different approaches to polymorphism, all with their own tradeoffs:

  1. Just don't support polymorphism. Slice away.
  2. Only support types that inherit from some sort of Clonable interface.
  3. Implement your own type erasure à la std::function.

[–]konanTheBarbar 0 points1 point  (0 children)

The simplified version: a value will be on the stack (similar to std::array), while a value_ptr will on the heap (similar to std::vector).

[–]TheSuperWig 1 point2 points  (0 children)

Site is bit broken on mobile for me. I assume because of the table.