Atomic<>::operator bool()?


#1

The only thing missing that makes Atomic almost a drop-in replacement for a basic type is the bool operator (zero evaluates to false, non zero to true).


#2

+1! (that means, “I agree!”)


#3

I say “almost” because the postfix increment and decrement operators are unavailable (due to how atomics are implemented).


#4

No, no, no! Implicit casts to bool are one of my pet hates!

I really hate seeing crap like this:

int a = 1; if (a) ...

The ‘if’ statement takes a bool parameter, so that’s what you should give it. You should always write:

…because that’s what you actually want to happen. It makes it clearer for the reader and avoids unexpected results if the object is actually a more complex class than a simple primitive. I recently read a good article by Matthew Wilson that gave the same advice, and backed it up with some well-reasoned arguments as to why it’s good practice to do so.

So… no! If your atomic value isn’t an Atomic, then it’s your responsibility to decide what values are “true” and which ones are “false”! No doubt a lot of people will just still do it by writing rubbish like

Atomic<int> a; if (a.get()) ...

…but unfortunately I can’t prevent that!

(Sorry, this post turned into a bit of a rant!)


#5

Implicit boolean conversions are something that I’ve come around 360 on over the course of a long career.

When I was young, I did it because it saved me space. When I was older, I decided that, as Jules said, “if” takes a boolean and I should always have an expression there that resulted in a boolean. This went on for a long time - and then I was in a job where I got to read hundreds of thousands of lines of the best code I’d ever seen, and I found out that all of these killer engineers used implicit bool conversion consistently for arithmetic and pointer operations even in C++ - but then, I learned Python which actively discourages NOT using implicit bool conversions and explains very clearly in their documentation that that is because this results in less mental overhead for the programmer.

And it’s true. Each character of code you write is code you’ll read 10 times. Less is more. As long as you are completely unambiguous in your intentions, simplicity trumps all.

Compare these two samples:if (const char* error = doSomething(someObject)) { LOG(ERROR) << "doSomething didn't do " << error; return false; } and const char* error = doSomething(someObject); if (error != NULL) LOG(ERROR) << "doSomething didn't do " << error; return false; }

Now, it’s not just that the second segment is a little bit longer - but in fact the first code is better because the variable “error” has exactly the correct scope and I can then re-use this code anywhere I like - leading to sweet constructions like…

[code]if (const char* error = doSomething(someObject)) {
LOG(ERROR) << "doSomething didn’t do " << error;
return false;
}

if (const char* error = doSomethingElse(someObject)) {
LOG(ERROR) << "doSomethingElse didn’t do " << error;
return false;
}

return true;[/code]where it’s really easy to add new conditions or move things around without ever worrying about those encapsulated local variables “error”.

Anyway (here comes the massive snark) you should be the last person to complain, because you allow the number 0 to autoconvert to the pointer named NULL in hundreds of places in the program! :stuck_out_tongue: The number 0 is an int in C++ and a different animal from NULL, the name of a pointer which on all modern systems would reinterpret_cast into the number 0.

Unlike the “implicit boolean conversion,” this is actually a bad idea (very slightly). The main reason is that on some systems, sizeof(int) != sizeof(void*) - and on those systems if you take sizeof of some symbol that’s supposed to be a pointer and it evaluates into the integer 0, then you’ll get the wrong number!

This never happens in Juce (as far as I can see). in Juce, 0 is only used to initialize pointers, and that’s guaranteed to work forever on any system Juce will ever be built for.

But it’s also conceptually a bad idea because “0” and “NULL” are only loosely related. When I see a NULL in code, I know exactly what’s going on - this is an empty pointer. If I see a 0 in Juce, I don’t know whether I’m going to arithmetic or addressing and I have to look up at the definition of the variable or method to know - in my code, if there’s 0 it uniquely means the number 0 and NULL means that special pointer.

So, mote, beam, etc. :twisted:


#6

Hey that’s all fine but the only reason I brought it up is because I was converting a lot of single-threaded code that used int, to use Atomic to be thread-safe. Even though it was 20 or 30 odd places that I had to change, I thought it there would be some use to making Atomic<> more of a drop-in replacement.

It might satisfy some theoretical desires to enforce some particular order, but for the use-case of converting some code to use Atomic instead of a basic type, it’s extra work with no gain. Now don’t get me wrong, I’m the last person anyone would ever accuse of sacrificing form over function, but I’m just saying…really? We’re talking about a language feature here.


#7

And I’m very much looking forward to replacing them all with nullptr! That’ll be a fun afternoon of search-and-replacing…

My use of the 0 rather than NULL started out as a personal preference, but on reading all the c++ gurus, it does seem to be what most people recommend as the best practice until nullptr is available.

But something that really makes me cringe when I see it (surprisingly often!) in other people’s code is the use of 0L as a null pointer!

When I wrote the Atomic class I specifically didn’t want it to be a drop-in replacement! I didn’t want code like this:

…to just compile with no problems, otherwise it’d defeat the whole purpose! That’s why I deliberately didn’t provide a postfix operator++, and why it has a get() method rather than an implicit cast to get the value.

The fact that you’re forced to go through and think about each place that uses it is a good thing - an Atomic certainly shouldn’t be treated in the same way you treat a normal int.


#8

Are you saying that Juce is going to require C++0x in order to compile? I’m still stuck with Visual Studio 2008 :frowning: Although the Intel compiler has an option to allow C++0x compilation. (And doesn’t gcc have it too?). I’m very much looking forward to the auto keyword myself.

Fine, you sold me on it.


#9

Not in the near future, sadly - it’s going to take a long time for c++11 to be ubiquitous enough.

But something I’d like to do fairly soon would be to implement a couple of things that could be back-ported - for older C compilers, nullptr could be done as a macro “((void*) 0)”, and noexcept could be defined to be “nothrow()”, which would mean the library could take advantage of those features if the compiler supports c++11, but would still be fine if it doesn’t.


#10

Not really. The big advantage of nullptr is that it’s a special type, and as such it can be used in place a macro fails, or when the overloading differ by int/pointer type (see http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/nullptr )

Basically:

// Example from: 
#define NULL ((void *)0) 
std::string * str = NULL; // Can't automatically cast void * to std::string *
void (C::*pmf) () = &C::func;
if (pmf == NULL) {} // Can't automatically cast from void * to pointer to member function.

or this:

void func(int);
void func(double *);
int main()
{
  func (static_cast <double *>(0)); // calls func(double *) as expected
  func (0); // calls func(int) but double * may be desired because 0 IS also a null pointer
}

None of those will be solved by a macro, and if you start using nullptr, you’ll start using these case implicitely (seen no more warning will happen), and then the older compiler won’t work.
If you don’t use the specific use cases, then there is no point in changing all code to nullptr.


#11

I wasn’t suggesting I was going to write a macro that would provide the same functionality as nullptr - obviously that’s not possible, otherwise they wouldn’t need to add it to the language!

And I’m not talking about making anyone else use it, all I meant was that it’d be nice to prepare my code internally for nullptr. Even just “#define nullptr 0” would provide all the backwards-compatibility that it needs, and then the code will compile as it currently does in older compilers, but I’ll be able to run it through a c++11 compiler to get some extra type-checking.


#12

I personally think you should just use NULL, which nearly every C and C++ compiler has as (((void*) 0). To me there’s a clear difference… NULL represents the empty pointer in C and C++, nullptr in C++0x.

My guess is that it’ll be five years before Juce can actively support C++0x (I imagine Juce already compiles under it…) :frowning:

The fact is that you can’t really build with C++0x at all on Mac OS/X without enough hacks to XCode and your Mac that you’re basically painting targets on both feet. The roadmap for OS/X is that gcc is end-of-life and clang will be the future compiler, and the one to support C++0x. This is IMHO the correct plan but also IMHO it’ll be at least two years before Klang is ready for production work on XCode.

That day is the first day that people can start to throw away their old compilers for cross-platform work. It will be years more before you can be sure that most developers are using a compiler that does actively support C++0x.

DERAIL OVER. Let’s go back to Atomic::operator bool() const;

So aesthetics don’t hack it as a way to decide whether to include an API feature or not! What you need are axioms…

I have evolved a strict hierarchy of desirable features in an API.

Number one, that trumps everything, is correctness. The API shouldn’t just do the right thing, it should not lead you into error. At Google where I used to work, they are extremely careful this way - in most circumstances, they ban method and function overloading, default parameters AND operator() functions. I chafed initially, but I grew to understand that the reason they did this was entirely because these specific constructs had been implicated in many bugs in the past, and the rule really doesn’t prevent you from doing anything you like - what those three rules have in common is that you know exactly what is going off for each method or function call.

I’m not quite that strict now I “graduated” but still nothing trumps correctness - and clarity is part of correctness because if your code or API is unclear, then it’s unclear also if it’s correct or if you are using it correctly.

After that, we get “terseness and convenience”. This is important simply because your programming brain is a finite resource, and if you are writing an API the brain of your users is even more limited than that, because you are in charge of your API whereas they have many other fish to fry and your API is just one of them.

The “implied bool conversion” is a definite advantage for “terseness and convenience”. So logically speaking, an API should adopt that convention if you can do so without sacrificing correctness and clarity!

So we’re down to the question - are there cases where a non-beginner C++ programmer would either find the implied bool conversion either unclear, or be led into a trap that would not otherwise happen?

My claim is that this is not the case. As long as the concept of “empty” is well-defined for a given type, it makes complete sense to have !x to be defined as a synonym for “x.empty()”.

After some thought the only possible emptiness trap I can think of is NULL vs “” - we must all have run into this bug where someone else sometimes passes you NULL and sometimes “” - but this isn’t changed at all by the presence or absence of the implicit bool conversion.

So another vote for the Atomic::operator bool() const!


#13

(I’ve just never liked using NULL… just aesthetically, I guess. Can’t really make a concrete argument against it)

Sadly, you’re quite right about how long it’ll be until c++11 is widely adopted, sadly. But I do think it’ll be handy for me to be able to change all my throw()s to noexcepts and 0’s to nullptrs so that I can occasionally shove the codebase through a real c++11 compiler and make use of the extra checking that it’ll perform on those constructs.

As for Atomic, my argument against a cast to bool is the same as the reason I didn’t provide an implicit cast to any other type - I deliberately want these conversions to be inconvenient, and I don’t want them to go unnoticed. When you’re dealing with atomics, you need to be acutely aware of each operation that is performed on the value, because the order in which they happen will affect your program’s correctness - so I want every operation on that object to be clearly marked in the source code, and not hidden behind an implicit cast.


#14

Sorry for the off topic but …xcode is using g++ isn’t it ? Because I believe at the moment, g++ has the best support for C++1x …
http://wiki.apache.org/stdcxx/C++0xCompilerSupport


#15

I thought noexcept / nothrow were deprecated in the latest C++0x draft? That’s what they’re saying in ##C++ at least (Freenet).