And using pointers instead of references.
And using pointers instead of references.
int foobar;
f(foobar);
g(&foobar);
Well, in C, "g" is the only function that might modify foobar. In C++ however, the reference type f(int& x); can make f also modify foobar.For some coders, it is preferable to have this explicit & reminder on their code, to see cases like "g" rather than possible-modifications that occur with "f"-style code.
Personally speaking, I kinda switch between the two styles depending on mood / the situation. I'd think that the dereferences on "atomics" however need to be incredibly explicit however, so "atomic" code will almost certainly be "g" style, rather than "f".
I would argue this sometimes-there single character is incredibly brittle for the claimed benefits, and illustrative of the unwillingness of large parts of the C++ community to use any reasonable tools that would do a better job instead (e.g. very basic syntax highlighting would be far more consistent in indicating mutability).
There's a HUGE difference from a "int& foo" and a "int* foo". ESPECIALLY if I'm doing incredibly sophisticated atomic operations, memory-barrier, and atomic operations (IE: atomic<int>& vs atomic<int>*).
Take a guess which one I'm more comfortable passing around.
This isn't a case of "param->Operation1(1)" like you're talking about. This is a simple integer being used in some of the most complex, sophisticated, low-level optimizations known to the modern programmer. Knowing when, where, why, and how these things are dereferenced is the entire damn point of memory-barriers and atomics. I want _NO_ surprises, at all.
At least, within the context of "C++ Concurrency Models". To be honest, this is the stuff that causes me to run away screaming. But if I'm forced to write code involving the words "compare-and-swap", "atomic", or "barrier", you damn well bet that I'm going to be doing it with "int*" rather than "int&".
--------
Every style of code has its place. When dealing with atomics, I definitely prefer pointers over references. If you're running around in object-oriented land, you probably don't care and have modest benefits to the reference. But the minute you start studying "A-B-A" problems and the difference between "load-acquire" vs "load-consume" and the possible memory-orderings that can happen, you suddenly want to become extremely explicit over your dereferencing.
--------
"When" did the dereference occur? Is it compatible between Thread#1 and Thread#2? Are there any weird "gotchas" that could occur? Has the dependencies you need been ordered correctly in all threads that are active?
If you're unaware of what's going on here, give this a readover. https://en.cppreference.com/w/cpp/atomic/memory_order
I promise you, you'll have some headaches. This is by far the hardest subject in all of low-level programming I've come across. And being more explicit about when the dereferences happen is very useful.
atomic<int>* blah;
a = *blah; // This is relatively fast.
*blah = b; // This is slow: store-release.
blah = c; // This is fast, no store-release operation going on. Just a pointer-assignment
Anyone dipping down to this level is interested in the performance characteristics of memory barriers. Indeed, the entire point of this exercise is to get faster than std::mutex lock / unlock after all, and squeezing the last ounce of performance out of your code.Indeed, one can argue that when using atomics, the more explicit "store" and "load" functions should be used instead.
a = blah->load(); // These two examples are
blah->store(b); // arguably the most explicit, preferred choiceYes, I have. And yes, I am familiar with the C++ concurrency/memory models discussed here. And, to be clear, nothing in the article is doing anything that couldn't be done just as clearly with std::atomic<bool>& as with std::atomic<bool>*. Obviously, if you actually want to be able to assign to the pointer, then you need the pointer form.
> Indeed, one can argue that when using atomics, the more explicit "store" and "load" functions should be used instead.
Yes, this is the style I strongly prefer. Of course, here it doesn't matter between
a = blah->load();
blah->store(b);
or a = blah.load();
blah.store(b);
and choosing to use the pointer form is often just introducing nullability where you don't need it._Nonnull int* whatever;
Tada! Nullability avoided. https://releases.llvm.org/3.8.0/tools/clang/docs/AttributeRe...
You still get a reminder it's a pointer by the * and -> notation.
void func(ParamType* param) {
...
auto res_1 = param->Operation1(1);
auto res_2 = param->Operation2(2);
...
}
the presence of -> doesn't tell you which method mutates. The only thing that would tell you that would be knowing which method is const. // In #include<deep/deep/deep/include.h>
#define if while
// somewhere else
if(1){
puts("Help, I'm stuck in an infinite loop!");
}
There's a reason why macros should be all-caps when used, and IMO, C++ programmers should prefer templates. Macros are too powerful, and need to be used extremely cautiously in my experience.Pointing out that macros can totally mess up your expectations isn't exactly news to anybody who does C/C++ programming.
void func(ParamType* param) {
assert(param != nullptr);
...
}
because passing via pointer introduced undesired nullability. And if that null pointer check isn't present, or is done wrong, you've easily introduced the potential for undefined behavior into the codebase. C++ specifically gives you mutable references to express the desired behavior in these cases, and using a nullable (and memory-unsafe nullable at that) pointer is actively less clear (you now need to document the non-null precondition separately, rather than in the signature itself).Small quibble, C++ has the capability to be low level enough that the potential for UB is always there. Even with a reference, you could be dealing with a completely random chunk of memory that someone told the compiler to interpret as the data type in question.
The quibble is the presentation of this as introducing the possibility of UB (the word introduced implies it wasn't already there), instead of used as a technique for minimizing the already present potential of UB.
> Pointers can be presumed in some code bases not to be null with nullability being the unusual and documented scenario.
Just because you can train your users to assume/document when nullability is allowed, doesn't make it a better option than using a language construct that would make it explicit.
Pointers are a better option because they make the mutability apparent at the call site. Codebases with that convention work great. There is no special training or documentation overhead involved.
> C++ specifically gives you mutable references to express the desired behavior in these cases
where "these cases" refers to the common case of (a) having a mutable argument, (b) which you want passed by reference, and (c) do not want to be nullable, that is exactly the semantics achieved by passing via mutable reference. I am not claiming this is why Stroustrup added them (his own explanation for why references were added is "References are useful for several things, but the direct reason I introduced them in C++ was to support operator overloading."), but that the language provides a construct which provides exactly for the desired behavior.
> Pointers are a better option because they make the mutability apparent at the call site.
At the first call site, yes (sometimes, see below). Not at any further calls.
> Codebases with that convention work great. There is no special training or documentation overhead involved.
As someone who works with a large codebase following this style, I beg to differ. Codebases with this convention are littered with
ReturnType func(ParamType* param) {
assert(param != nullptr);
...
}
because mutable-arguments-via-pointer has introduced undesired nullability, where the intended behavior could have been expressed more directly as ReturnType func(ParamType& param) {
...
}
and, it inevitably combines all three types of methods ReturnType func1(const ParamType* param) {
if(param != nullptr) {
...
}
ReturnType func2(ParamType* param) {
assert(param != nullptr);
...
}
ReturnType func3(ParamType* param) {
if (param != nullptr) {
...
}
mingling nullability and mutability, none of which is obvious from func1(&foo);
func2(&bar);
func3(&baz);
and you still end up needing documentation to figure out what's happening.So document it regardless instead https://releases.llvm.org/3.8.0/tools/clang/docs/AttributeRe...
There are also a number of cases where passing a pointer into a template function can be more concise. E.g. since pointers are iterators, if you have a generic function that iterates over things and you just want it to look at one thing (or zero) for testing, you can usually do this with pointer arithmetic.
Hm slightly tangential but could you install some kind of page-fault handler on a reference such that the control flow jumps on write back to the last place it was accessed? Not that you’d want to do that in the real world, but it would be an interesting “pub-sub” type control flow..
I agree that not having explicit out-parameters makes this a trade-off. It is possible to use an out-parameter wrapper type to get the best of both worlds, but nobody does this.