26

While trying to wrap my head around the problem shown in this question I found myself stuck on the following sentence from [util.smartptr.shared]/4:

[...] Changes in use_count() do not reflect modifications that can introduce data races.

I don't understand how I should read that, and what conclusions shall I draw. Here are a few interpretations:

  • Invoking use_count() does not introduce data races (but this should be guaranteed by the const-ness of that function alone, together with the corresponding library-wide guarantees)
  • The value returned by use_count() is not influenced by ("does not reflect"?) the outcome of operations that require atomicity or synchronization (but what are these relevant operations?)
  • use_count() is executed atomically, but without preventing reordering by the CPU or the compiler (i.e. without sequential consistency, but then why not mentioning the particular model?)

To me, none of the above seems to follow from that sentence, and I am at loss trying to interpret it.

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • By "invoking `use_count()`" do you make multiple threads calling it simultaneously, but no use of that `shared_ptr` object in any other way? – Ben Voigt Jan 23 '15 at 16:04
  • @BenVoigt: Yes (I guess), although that "interpretation" is just a desperate attempt at pulling out something meaningful from that sentence. I don't even have a clear idea of what that "something" is. – Andy Prowl Jan 23 '15 at 16:07
  • Does this quote from [here](http://open-std.org/JTC1/SC22/WG21/docs/papers/2010/n3118.html) clarify it at all for you: `Requiring shared_ptr and weak_ptr to always synchronize the use count makes it potentially slow and is inconsistent with the general approach to leave the synchronization to the user of a facility. ` – Shafik Yaghmour Jan 23 '15 at 16:08
  • I think it's using `use_count()` to refer to the actual use count. i.e., suppose two `shared_ptr`s `a` and `b` share ownership of the same object; if thread 1 makes a new `shared_ptr` from `a` and simultaneously thread 2 resets `b`, this doesn't result in a data race even though both operations will change the use count. – T.C. Jan 23 '15 at 16:10
  • @ShafikYaghmour: It does provide a further explanation (thank you for that), but I still don't understand what this means or is meant to imply in practice. – Andy Prowl Jan 23 '15 at 16:14
  • Libraries issue [896](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3018.html#896) discusses this sentence and how it came about. – Shafik Yaghmour Jan 23 '15 at 16:18
  • @ShafikYaghmour: I read that, and I'm still confused. I also find the expression "changes in `use_count()`" (which comes up out of the blue) is very bad, because `use_count()` does not change anything. I think I get what that means, but still... – Andy Prowl Jan 23 '15 at 16:26
  • When reading the standard, try to forget the words, the grammar, everything (otherwise your mind will crash), and then read people's minds. The C++ standard is terribly written. – curiousguy Aug 02 '15 at 03:21
  • @ShafikYaghmour " "_Requiring shared_ptr and weak_ptr to always synchronize the use count makes (...) is inconsistent with the general approach to leave the synchronization to the user of a facility_" " Actually it's perfectly consistent with: user is responsible for synchronisation on its own objects, library is responsible for synchronisation on its own objects – curiousguy Aug 02 '15 at 03:50

3 Answers3

11

The current wording derives from library issue 896, which also addressed the question of whether shared_ptr should be thread safe in the sense that distinct shared_ptrs owning the same object can be accessed (in particular, copied and destructed) from distinct threads simultaneously. The conclusion of that discussion was that shared_ptr should be thread-safe; the mechanism to guarantee this was to pretend that shared_ptr member functions only access the shared_ptr object itself and not its on-heap control block:

For purposes of determining the presence of a data race, member functions access and modify only the shared_ptr and weak_ptr objects themselves and not objects they refer to.

Here "objects they refer to" means the control block.

However this raises an issue; if we pretend that distinct shared_ptrs owning the same object don't access the control block, then surely use_count() cannot change? This is patched by making use_count() a magic function that produces a result out of thin air:

Changes in use_count() do not reflect modifications that can introduce data races.

That is, use_count() can change from one call to the next, but that does not mean that a data race (or potential data race) has occurred. This is perhaps clearer from the previous wording of that sentence:

[Note: This is true in spite of that fact that such functions often modify use_count() --end note]

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • So is this equivalent to saying "Operations that influence the result of `use_count()` do not introduce data races?" – Andy Prowl Jan 23 '15 at 17:15
  • 2
    @AndyProwl well, it's possible to introduce a data race using those operations by operating on the same `shared_ptr` from different threads, but I think we're on the same page here. The key is that `use_count()` can be observed to have changed without that implying that a data race has occurred. – ecatmur Jan 23 '15 at 17:30
  • @ectamur "Here "objects they refer to" means the control block " - I disagree - the objects they refer to is the actual object the underlying point is refering to. the use_count is already part of the control block. – excalibur Jan 23 '15 at 19:01
  • @excalibur `use_count()` reports on control block state, but is itself a method on shared-ownership smart pointer objects. I agree that "objects they refer to" includes the owned object, but it must also mean the control block as that is how the related smart pointer objects communicate, and they must do so without introducing a data race. – ecatmur Jan 23 '15 at 21:11
  • 3
    I think one piece of info that would add to this excellent answer is the following from [res.on.data.races]: _"7 Implementations may share their own internal objects between threads if the objects are not visible to users and are protected against data races."_ This guarantee does not apply to the control block because `use_count()` means part of the control block is "visible to users", so the statement being discussed is needed in order to specify that sharing the control block between threads is guaranteed not to introduce races. – Jonathan Wakely Jan 24 '15 at 19:49
  • This is ridiculous. The control block has no official existence. As an implementation detail, we cannot say anything about it! The whole std discussion looks like garbage. Do we have to discuss the implication of calling any other function which implies a race condition? (like mutex functions) – curiousguy Aug 02 '15 at 03:31
  • @JonathanWakely "_This guarantee does not apply to the control block_" Preposterous. The control block has no official existence. "_because use_count() means part of the control block is "visible to users"_" Nonsense – curiousguy Aug 02 '15 at 03:32
  • @curiousguy don't confuse "data race" (which had a formal definition in the standard) with race condition. Mutex operations are defined not to be data races, so no, we don't need to discuss them. Despite not being mentioned explicitly, the control block or something shared between objects has to exist, and it has to be changed. The wording means those changes must be done safely to avoid data races. – Jonathan Wakely Aug 02 '15 at 09:27
  • @JonathanWakely Many things "have to" exist; the standard is not written in terms of "have to". It would be a major change in the way the standard is specified. But then, the standard is an horrible Frankenstein monster-like assembly of incompatible pieces of specs written in different formalisms. – curiousguy Aug 02 '15 at 17:37
  • 2
    @curiousguy no, the Standard uses the term "shall", which when applied to the implementation says what the implementation "has to" do. You can think of my use of "control block" as a shorthand for "whatever internal implementation detail is used to implement `use_count`"; the effect is unchanged. – ecatmur Aug 03 '15 at 09:34
  • 1
    @curiousguy, most of the things that have to exist but are left unspecified do not require additional thread-safety than is required by other parts of the standard library. This is an exception, so it need to be called out specifically, because the blanket wording that covers the whole library isn't enough to make `shared_ptr` safe to use as intended. If you have a suggestion to specify it more cleanly then please file a defect report, whining on SO won't improve things. – Jonathan Wakely Aug 03 '15 at 15:16
  • @ecatmur "_the term "shall", which when applied to the implementation says what the implementation "has to" do_" Yes, except where the term "shall" says what the implementation cannot do: "For purposes of determining the presence of a data race, member functions shall access and modify **only** the shared_ptr and weak_ptr objects themselves and not objects they refer to" There is no "the implementation of `shared_ptr` shall manage a control block". – curiousguy Aug 04 '15 at 08:29
  • 1
    @curiousguy well, member functions themselves don't determine the presence of a data race; that's the job of the abstract machine. To be 100% correct, that sentence should probably have inserted: "*[...] member functions shall [be considered to] access and modify [...]*". But most people are OK with inferring that kind of thing. – ecatmur Aug 04 '15 at 08:53
2

It means that the code in use_count() is either lock-free or uses mutexs to lock critical sections. In other words you can call it from threads without worring about race conditions.

Paul Evans
  • 27,315
  • 3
  • 37
  • 54
  • So if I understand it correctly, "do not reflect modifications that can introduce data races" is just an obscure way of saying "is executed atomically" or "shall not cause data races"? The "reflect modifications" part is puzzling me. – Andy Prowl Jan 23 '15 at 16:12
  • @AndyProwl Yes but some people might find "atomically" obscure if the code in fact uses mutexes. – Paul Evans Jan 23 '15 at 16:15
  • Then what about "shall not cause data races"? If that's what they meant, I think they should (no, they would) have written it that way. It's still not clear to me. – Andy Prowl Jan 23 '15 at 16:17
  • @AndyProwl Yes, that's more readable to me. There's plenty in the standard that could have been worded better (and probably always will be). – Paul Evans Jan 23 '15 at 16:19
  • But that's the point: I can't believe they were not able to come up with "does not cause a data race" if that's what they meant to express. These people are not newbies, there must be a reason why they didn't phrase it that way. – Andy Prowl Jan 23 '15 at 16:23
  • The statement is not talking about the thread-safety or atomicity of the `use_count()` function itself. As Andy said in the question, its constness already says `use_count()` doesn't introduce races. The statement is a saying that the modifications which _can be observed to have happened_ by calling `use_count()` were race-free modifications. – Jonathan Wakely Jan 24 '15 at 19:30
  • 1
    @Jonathan I wish that sentence in the Standard was phrased the way you wrote it. At least to me it would have been much clearer since the beginning. – Andy Prowl Jan 28 '15 at 14:49
  • @PaulEvans "_some people might find "atomically" obscure if the code in fact uses mutexes_" Really? See rename(2) in linux "If newpath already exists, it will be **atomically** replaced" In specifications, "atomic" is routinely used to indicate that no observer can ever see an half-complete operation, not that C or C++ atomic types are used. – curiousguy Aug 03 '15 at 18:23
2

I think when we add the previous sentence the intent becomes a little clearer:

For purposes of determining the presence of a data race, member functions shall access and modify only the shared_ptr and weak_ptr objects themselves and not objects they refer to. Changes in use_count() do not reflect modifications that can introduce data races.

So, the last sentence is just emphasizing the same point as the first sentence. For example, if I copy a shared_ptr, its use-count will be incremented to reflect the fact that the shared_ptr has been copied--therefore the result from use_count() will be changed--but this is not allowed to access (and, especially, not allowed to modify) the pointee object, so it can never introduce a data race in use of that pointee object.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Just as a note, according to @ecatmur's answer `"objects they refer to" means the control block.`, not the pointee object (e.g. the `int` pointed to by a `shared_ptr`). If that's what you meant, sorry for misinterpreting :) – Andy Prowl Jan 23 '15 at 17:01
  • 1
    @AndyProwl: I think his answer is basically mistaken. The fact that a shared_ptr has pointer to a block on the heap that contains the use-count and the pointer to the pointee object is (at least officially) an implementation detail, and its manipulation is of essentially no consequence to the user. What would matter to the user would be if manipulations of the `shared_ptr` could introduce data races in use of the user's object. – Jerry Coffin Jan 23 '15 at 17:11
  • Er, are you claiming that if the `shared_ptr` introduces data races in stuff that isn't the user object it is ok? "The fact that a shared_ptr has pointer to a block on the heap (...) is (at least officially) an implementation detail, and its manipulation is of essentially no consequence to the user." This is what the standard bit says: whatever happens, causes no data races, neither in the user data, nor elsewhere. – R. Martinho Fernandes Jan 23 '15 at 17:13
  • @R.MartinhoFernandes: No, of course not. I'm saying they're attempting to specify the interface, not the implementation. It's up to the implementor to implement it in a way that doesn't introduce any data races internally. The interface says that it can't introduce any data races in the use of the pointee object. – Jerry Coffin Jan 23 '15 at 17:15
  • It also says the changes to make `use_count` work cannot introduce data races (*at all*). (There's no reason to single out `use_count` if the intent is what you describe) – R. Martinho Fernandes Jan 23 '15 at 17:16
  • 1
    @R.MartinhoFernandes: yes, emphasis on "introduce". Of course it can't *contain* a data race either, (that would be an obvious bug) but that's an entirely separate question. The point here is that it can't *introduce* a data race--i.e., as long as you use of the pointee object doesn't include any data races, the use of the `shared_ptr` won't add any. – Jerry Coffin Jan 23 '15 at 17:21